From 408f70e69f9c5e49cb4377f4886ac2001bfa67a2 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Fri, 2 Oct 2026 08:53:47 -0500 Subject: [PATCH] fix(runner): preserve stock Codex base instructions (#14920) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The native Runner connects Paperclip tasks to Codex app-server. > - Paperclip passed its runtime context as `baseInstructions`. > - That field replaces the stock Codex base prompt. > - This pull request sends the same Paperclip context as additive developer instructions. > - Codex keeps its stock prompt and still receives Paperclip task instructions and tools. ## Linked Issues or Issue Description **What happened?** The native Codex driver and Rust provider sent Paperclip context through `baseInstructions` on thread start and resume. Codex used this text in place of its stock base instructions. Direct-chat resume also sent an empty replacement base. The Runner Lab session path used the same replacement field. **Expected behavior** Codex should retain its stock base prompt. Paperclip should add its runtime context through `developerInstructions`. Other provider facades should retain their current instruction handling. **Steps to reproduce** 1. Create a native Codex session through Paperclip Runner. 2. Inspect the `thread/start` request in the native provider trace. 3. Resume the session and inspect `thread/resume`. 4. Before this fix, these paths set `baseInstructions`. After this fix, the Codex paths set `developerInstructions` and omit `baseInstructions`. **Paperclip version or commit** Reproduced against master at `cad26c6bfb736039c8ed5743da650a44792a083c`. **Deployment mode** Built from source. Native Codex app-server and runnerd paths. A local protocol probe used codex-cli 0.153.4 and a localhost Responses stub. No duplicate fix or matching public issue was found in the GitHub search. ## What Changed - Send additive developer instructions on Codex start and resume in the TypeScript driver, Rust provider, and Runner Lab session path. - Carry the additive fragment through runnerd, including runtime asset path mapping. - Preserve existing instruction fields for other provider facades, including OpenCode. - Add start/resume/direct-chat regression coverage and check the actual Rust provider request. - Document the historical option and trace field names. Record progress and follow-ups in the working checklist. ## Verification - `pnpm -r typecheck` — passed. - `pnpm build` — passed. - Targeted Codex driver lifecycle, driver, and live-session Vitest suites — 139 tests passed. - `cargo test --manifest-path packages/paperclip-runner/runner/Cargo.toml --locked -p paperclip-runner-core --test codex_provider` — 91 passed, 2 ignored subprocess helpers. - Real app-server probe: a localhost Responses stub captured identical 14,732-character stock base instructions on fresh start and cold resume. Both requests retained the Paperclip marker in developer input. Both stub turns completed. No paid inference was used. - Runnerd transport Vitest suite — 182 tests passed. - The initial `pnpm test:run` attempt reported local dependency-loading, embedded PostgreSQL startup, and macOS `/var` versus `/private/var` path failures. It was stopped after those failures. Loading-suite reruns passed 1,428 tests after the build; native interaction/finalization reruns passed 38 tests. A seven-suite diagnostic rerun passed 463 tests and isolated the remaining path and PostgreSQL setup failures. - With `TMPDIR=/private/tmp`, workspace, gateway, interaction, and attachment suites passed all 356 tests. The remaining environment-image and native-session-resumption suites passed all 44 tests with the same canonical temp path. All affected suites passed on rerun. The original full local command was stopped after failures and is not claimed as passing. - All 55 PR checks passed at `83281439456181396f3707eecda5d2ebc90bd14d`. Greptile scored 5/5 with no open review threads. - No paid live campaign or Product E2E browser suite was run. This change has protocol and regression coverage; it does not claim improved task quality. ## Risks - Stock Codex behavior may differ from behavior under the previous Paperclip replacement prompt. Restoring that behavior is the intended change. - Existing Codex threads retain their saved replacement base prompt. They need a provider session reset to receive the stock base. This PR does not reset active sessions or alter recovery rules. - The legacy `baseInstructions` option and trace field names remain for compatibility. They now describe the additive Paperclip fragment for Codex. - The separate Codex-through-ACP dependency patch remains a follow-up in the harness coverage checklist. This PR covers native app-server execution. ## Model Used OpenAI Codex, GPT-6. The exact runtime model variant and context window are not exposed in this session. Used reasoning, repository inspection, code editing, shell execution, and test tools. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- ...10-02-stock-harness-paperclip-checklist.md | 219 ++++++++++++++++++ .../paperclip-runner/docs/tutorials/codex.md | 11 + .../crates/runner-core/src/codex_provider.rs | 9 +- .../runner-core/tests/codex_provider.rs | 28 +++ .../paperclip-runner/src/contracts/codex.ts | 2 + .../codex/codex-app-server-driver-impl.ts | 12 +- .../codex-app-server-driver.lifecycle.test.ts | 55 ++++- .../codex/codex-app-server-driver.test.ts | 5 +- .../src/drivers/codex/codex-driver-types.ts | 2 + .../src/live/live-session.test.ts | 12 +- .../paperclip-runner/src/live/live-session.ts | 4 +- .../src/live/runnerd-codex-transport.test.ts | 5 +- .../src/live/runnerd-codex-transport.ts | 2 +- 13 files changed, 350 insertions(+), 16 deletions(-) create mode 100644 doc/plans/2026-10-02-stock-harness-paperclip-checklist.md diff --git a/doc/plans/2026-10-02-stock-harness-paperclip-checklist.md b/doc/plans/2026-10-02-stock-harness-paperclip-checklist.md new file mode 100644 index 0000000000..f71067ff07 --- /dev/null +++ b/doc/plans/2026-10-02-stock-harness-paperclip-checklist.md @@ -0,0 +1,219 @@ +# Stock harness, with Paperclip: working checklist + +Created: 2026-10-02. Status: item 1 implemented for native Codex app-server; +PR validation in progress. Other implementation items remain open. + +Goal: keep the agent's stock harness behavior and add only what it needs to work +with Paperclip. Apply this across legacy adapters, the new Runner, and their +configuration variants. + +We will work through the numbered items one at a time with Dotta. For each item, +record the proposed behavior and Dotta's direction, make a bounded change, and +verify the affected paths before moving on. New findings get stable IDs in the +ledger below so they do not disappear into conversation history. + +**Current item: 1 — validate and review the native Codex instruction fix.** + +## Agreed direction and boundaries + +- Preserve vendor base instructions. Paperclip context should be additive where + the harness supports it. +- Reduce the default hire operating manual and review every role/team template + for reduction or removal. Prefer a tiny default, potentially one paragraph. +- Keep a minimal coordination boundary; load uncommon procedures on demand. +- Move bookkeeping into the runtime where it can own the operation reliably. +- Check capability and configuration behavior across all harness paths. +- Paperclip owns MCP configuration. Missing personal integrations are acceptable + and intentional; restoring them is not a goal of this work. +- Configuration changes and configuration-driven session resets are acceptable. +- Preserve Paperclip authentication, assigned skills, workspace access, tool + authorization, company boundaries, checkout, approvals, budget hard stops, + pause/cancel behavior, audit records, and usable task/artifact delivery. +- Restore appropriate repository instruction context without accidentally + importing unrelated host configuration or displacing Paperclip-owned MCPs. +- Decide explicitly how changed defaults affect existing agents and sessions; + do not assume existing custom instructions should be rewritten. + +## 1. Preserve the native Codex base instructions + +- [x] Trace every instruction path: backend composition, TypeScript driver, + runnerd bridge, and Rust provider; include fresh threads, resume, and recovery. +- [x] Record the chosen additive mechanism and the minimum Paperclip context it + needs to carry. Include fallback/direct execution paths. +- [x] Remove default replacement of stock base instructions across those paths. +- [x] Verify the actual app-server request and retained session instructions, + including resume; checking only a prompt builder is insufficient. +- [ ] Verify Paperclip task context, tools, auth, assigned skills, and completion + still work. Record applicable regressions and eval results. + +Starting points: [Codex backend](../../packages/paperclip-runner/src/backends/codex-native-backend.ts), +[app-server driver](../../packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts), +[runnerd transport](../../packages/paperclip-runner/src/live/runnerd-codex-transport.ts), +[Rust provider](../../packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs). + +Decision: use additive `developerInstructions` on Codex start and resume in the +TypeScript driver, Runner Lab/eval sessions, and Rust provider. Retain existing +instruction fields for other provider facades. Keep the Paperclip fragment and +its historical option/trace names unchanged in this bounded fix. + +Transition: pre-change Codex threads retain their saved replacement base prompt +and need a provider session reset. Do not reset active sessions automatically. +The separate Codex-through-ACP dependency patch remains a coverage follow-up +under item 6; this change covers the native app-server path. + +## 2. Reduce the default operating manual and shared prompt layers + +- [ ] Inventory what an agent actually receives: hire instructions, shared + prompt template, wake context, runtime prompt, bootstrap, and loaded skills. + Separate always-present text from content loaded on demand. +- [ ] Agree on the tiny common contract and the coordination details that each + runtime still requires. Legacy API coordination and native semantic tools + need appropriate instructions for their respective interfaces. +- [ ] Remove repeated workflow rules and stock coding/style/autonomy guidance. +- [ ] Move detailed planning, hiring, artifacts, and exceptional procedures to + discoverable references or tools where feasible. +- [ ] Check fresh and resumed task/chat flows for instruction duplication and + contradictory completion or waiting rules. + +Starting points: [default hire instructions](../../server/src/onboarding-assets/default/AGENTS.md), +[shared adapter utilities](../../packages/adapter-utils/src/server-utils.ts), +[native runtime contract](../../packages/paperclip-runner/src/contracts/runtime-context.ts), +[Paperclip operational skill](../../skills/paperclip/SKILL.md). + +Decision: exact retained paragraph and on-demand boundaries pending. + +## 3. Review every hiring and role template + +- [ ] Cover default hires, CEO, chief of staff, coder, QA, UX, security, CTO, and + every other shipped team/role instruction set. +- [ ] Trace onboarding, API/UI/CLI hiring, team imports, and agent-creation skill + rules so a removed manual is not regenerated through another entry point. +- [ ] For each template, decide: remove it, retain a tiny role description, or + keep specific domain guidance with a stated reason. +- [ ] Review forced delegation, mandatory memory workflows, procedural review + routing, comment requirements, and old governance instructions. +- [ ] Update hiring references and draft-review requirements alongside templates. + Native Runner agents must not be required to follow legacy skill/API procedures + that their runtime intentionally does not expose. +- [ ] Decide rollout for new hires, existing managed bundles, imported teams, + and custom agent instructions. +- [ ] If substantial behavioral instructions remain, identify the behavior they + should improve and add appropriate eval coverage. A tiny role paragraph may + need only creation/configuration coverage. + +Starting points: [onboarding assets](../../server/src/onboarding-assets/), +[default bundle service](../../server/src/services/default-agent-instructions.ts), +[agent routes](../../server/src/routes/agents.ts), +[hiring skill and references](../../skills/paperclip-create-agent/), +[teams catalog](../../packages/teams-catalog/catalog/). + +Decision: per-template disposition and migration policy pending. + +## 4. Fix repository context while retaining Paperclip configuration + +- [ ] Map instruction discovery and precedence for each harness: repository + AGENTS.md/CLAUDE.md or equivalent, project/local settings, isolated homes, and + explicit Paperclip instruction injection. +- [ ] Distinguish repository instructions from settings that also load MCPs, + plugins, credentials, or skills; choose selective loading/injection as needed. +- [ ] Verify repository instructions are available in the correct workspace, + including worktrees, remote execution, and resumed sessions. +- [ ] Verify assigned skill discovery/pinning, authentication, Paperclip MCP + ownership, tool authorization, and configuration-change invalidation. +- [ ] Record intentional isolation separately from accidental lost context. + +Claude starting points: [local adapter](../../packages/adapters/claude-local/src/server/execute.ts), +[ACP patch](../../patches/@agentclientprotocol__claude-agent-acp@0.73.0.patch), +[runtime sandbox](../../packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts). + +Decision: repository-context loading mechanism per harness pending. Personal MCP +isolation is approved and should remain. + +## 5. Let the runtime own bookkeeping + +- [ ] Map which runtime owns checkout, status transitions, completion comments, + artifacts, waiting/review states, and recovery; identify manual duplicates. +- [ ] Keep one valid completion path per runtime and preserve meaningful user + questions, approval requests, dependency waits, and final deliverables. +- [ ] Remove agent instructions for operations the runtime already performs; + retain necessary coordination for legacy/external adapters. +- [ ] Verify durable task state, visible final answer, artifact access, audit, + and restart/recovery behavior through the real product paths. + +Decision: exact runtime responsibilities and legacy compatibility pending. + +## 6. Complete the harness and configuration coverage audit + +This matrix is an inventory seed, not a claim that every path has been audited +or qualified. Expand it from the registry and provider/profile definitions. +For every numbered change, record applicability here or an explicit reason it +does not apply. + +| Execution family | Paths to cover | Audit status | +| --- | --- | --- | +| Legacy Codex / Claude | Local adapters, managed auth and isolated configuration | Initial inspection only | +| Other legacy local adapters | ACPX, OpenCode, Pi, Cursor, Gemini, Grok, Kimi, Hermes | Pending | +| Other adapter transports | Cursor Cloud, Hermes/OpenClaw gateways, process, HTTP, external adapter plugins | Pending | +| Runner Codex | App-server driver, runnerd bridge, Rust provider, direct/fallback paths | Additive instruction fix implemented; PR validation pending | +| Runner ACPX | Enabled profiles, especially Claude/Grok; declared or pending profiles tracked separately | Claude initial inspection; remaining audit pending | +| Runner OpenCode | Native provider and configuration paths | Pending | +| Hosted/remote providers | Claude Managed and AWS AgentCore; identify their own baseline rather than assuming CLI semantics | Pending | + +- [ ] Inventory supported profiles and qualification status from the + [adapter registry](../../server/src/adapters/registry.ts) and + [provider resolver](../../server/src/services/native-runtime/provider-profile.ts). +- [ ] Cover local/remote environments, fresh/resumed sessions, managed/custom + hires, task/chat/planning flows, and auth/configuration variants as applicable. +- [ ] Audit restrictions on tools, skills, subagents, memory, and other harness + capabilities. Trace effective provider configuration, not just intermediate + configuration objects; document intentional limits and decide accidental ones. +- [ ] Check shared fixes reach every relevant adapter; document provider-specific + exceptions instead of silently extending a Codex/Claude assumption. + +## Verification and evals — apply to each item + +- [ ] Map existing coverage before adding cases; use [doc/evals.md](../evals.md). + Keep Runner protocol evals and Product E2E evals distinct. +- [ ] Start with narrow deterministic checks for instruction layering, effective + configuration, skill/auth delivery, and session behavior where appropriate. +- [ ] Use Runner evals for provider/session/tool protocol changes; use Product + E2E for real hiring, repository context, task lifecycle, and artifact delivery. +- [ ] Review existing context-integrity, hiring, completion-updates, and blocker + suites for reusable coverage; record gaps rather than claiming coverage from + a similarly named case. +- [ ] Compare task quality as well as Paperclip protocol compliance when claiming + that fewer instructions improve agent performance. Keep model, effort, + permissions, tools, and fixture comparable. +- [ ] Select narrow live cells when implementation is ready; retain revisions, + profile/environment, grader, retries, usage/cost, and failure classification. +- [ ] Run the relevant checks for each change and the repository's required full + verification before a PR-ready handoff. Record unrun checks and their reasons. + +## Findings ledger + +Confirmed mechanics below do not by themselves establish an effect on task quality. + +| ID | Finding | Work item / disposition | +| --- | --- | --- | +| F1 | Native Codex sent Paperclip text as `baseInstructions`. Probes on codex-cli 0.153.4 showed replacement; additive `developerInstructions` retained the stock base on start and cold resume. | 1; app-server fix implemented, PR validation pending | +| F2 | Default hires and role templates prescribe substantial operating procedures; common prompt and wake layers add further coordination text. | 2–3; mechanics confirmed, performance effect unmeasured | +| F3 | Hiring references require legacy Paperclip skill/comment procedures, while native Runner intentionally omits that operational skill and uses semantic tools. | 2–3, 5; reconcile runtime contracts | +| F4 | Local Claude appends instructions; Runner Claude preserves the Claude Code preset. Runner isolation excludes project/local settings, which can also exclude repository instruction discovery. | 4; selective context fix to design | +| F5 | Some Codex capability settings differ between the direct driver and daemon path; an intermediate configuration does not prove the final provider behavior. | 6; effective-path audit pending | +| F6 | Omitting or nulling `baseInstructions` on an old Codex thread's resume preserves its saved replacement; an empty string produces an empty base. | 1; document the required provider session reset; no automatic migration in this PR | +| F7 | The isolated Codex-through-ACP dependency patch also sets `baseInstructions` on start/resume. It is a separate path from the native app-server backend. | 6; follow-up patch/profile audit pending | + +Append new findings with evidence, affected paths, and the numbered item that +will address them. Record intentional behavior explicitly rather than as a bug. + +## Decision and completion log + +| Date | Decision / outcome | Evidence / follow-up | +| --- | --- | --- | +| 2026-10-02 | Dotta approved preserving stock instructions, smaller defaults/templates, minimal coordination, runtime bookkeeping, and coverage across harnesses. | Implementation details to work through one item at a time. | +| 2026-10-02 | Paperclip-owned MCP isolation and configuration changes/session resets are acceptable. | Preserve Paperclip auth and assigned skills while fixing repository context. | +| 2026-10-02 | Dotta requested implementation and a PR for item 1. Native app-server paths now use additive developer instructions. | 139 targeted TypeScript tests and 91 Rust provider tests passed; repository typecheck/build passed. Remaining test/review results to record. | +| 2026-10-02 | Verified actual Codex instruction layering using a localhost Responses stub, without paid inference. | codex-cli 0.153.4 sent identical 14,732-character stock base instructions on start and cold resume, with the Paperclip marker retained in developer input. This is protocol evidence, not a task-quality eval. | + +For each completed item, add the chosen behavior, changed paths, verification +results, remaining exceptions, and follow-ups here before checking it off. diff --git a/packages/paperclip-runner/docs/tutorials/codex.md b/packages/paperclip-runner/docs/tutorials/codex.md index 47b57433d7..e4c0d1126d 100644 --- a/packages/paperclip-runner/docs/tutorials/codex.md +++ b/packages/paperclip-runner/docs/tutorials/codex.md @@ -73,6 +73,17 @@ test "$(cat "$codex_workspace/hello.txt")" = "hello from Codex runner" ## Step 3: Inspect the exact model boundary +Paperclip sends its runtime instructions as additive `developerInstructions` +on Codex thread start and resume. Codex retains its stock base instructions. +The historical `context.baseInstructions` trace field contains the Paperclip +fragment, not the full Codex base prompt. The driver option with the same name +also supplies this additive fragment. + +Threads created before this change retain their saved replacement base prompt +when resumed. Reset those provider sessions to apply the stock base instructions; +adding developer instructions does not repair an already saved base prompt. +This change does not alter session recovery or reset active sessions automatically. + ```sh jq '.context | { protocolVersion, diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs b/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs index 437e0ea01a..95bc8579d5 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs @@ -1038,12 +1038,19 @@ impl CodexProvider { "model": config.model, "approvalPolicy": config.approval_policy, "runtimeWorkspaceRoots": [config.cwd], - "baseInstructions": config.instructions, "dynamicTools": dynamic_tools, }); let params_object = params .as_object_mut() .expect("Codex thread parameters are an object"); + // Codex's baseInstructions replaces its stock prompt. OpenCode + // uses the same protocol facade but keeps its existing contract. + let instruction_field = if config.provider == "codex" { + "developerInstructions" + } else { + "baseInstructions" + }; + params_object.insert(instruction_field.to_owned(), json!(config.instructions)); if provider.permission_profile == "paperclip-runner-external-sandbox" { // The execution target (for example Daytona) is the OS sandbox. // Codex must not try to create nested user/network namespaces, diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/codex_provider.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/codex_provider.rs index dac280fdf3..601fb38394 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/codex_provider.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/codex_provider.rs @@ -6290,6 +6290,34 @@ fn skill_wire_requests(directory: &Path) -> Vec { .collect() } +#[test] +fn runtime_instructions_are_additive_for_codex_on_start_and_resume() { + let directory = temporary_directory("instruction-channel-wire"); + let log = directory.join("requests.ndjson"); + let config = provider_config(&directory, &["--request-log", log.to_str().unwrap()]); + let mut provider = CodexProvider::start(&config, None).unwrap(); + let thread_id = provider.thread_id().to_owned(); + provider.shutdown().unwrap(); + let mut resumed = CodexProvider::start(&config, Some(&thread_id)).unwrap(); + resumed.shutdown().unwrap(); + let frames = skill_wire_requests(&directory); + for method in ["thread/start", "thread/resume"] { + let frame = frames + .iter() + .find(|frame| frame["method"] == method) + .unwrap(); + assert_eq!( + frame["params"]["developerInstructions"], config.instructions, + "{method}" + ); + assert!( + frame["params"].get("baseInstructions").is_none(), + "{method}" + ); + } + fs::remove_dir_all(directory).unwrap(); +} + #[test] fn skill_instructions_flag_reaches_start_and_resume_and_preserves_absent_config() { for flag in [Some(true), Some(false), None] { diff --git a/packages/paperclip-runner/src/contracts/codex.ts b/packages/paperclip-runner/src/contracts/codex.ts index 605fd29d52..b8db8eb86b 100644 --- a/packages/paperclip-runner/src/contracts/codex.ts +++ b/packages/paperclip-runner/src/contracts/codex.ts @@ -50,6 +50,8 @@ export interface CodexModelContextSnapshot { collaborationMode: "default" | "plan"; sandbox: unknown; approvalPolicy: unknown; + /** Historical trace field for the Paperclip instruction fragment, supplied + * to Codex as developerInstructions rather than replacing its stock base. */ baseInstructions: string; instructionSources: string[]; instructionPolicy: { diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts index da221eff7f..62151df5ea 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts @@ -199,6 +199,14 @@ export class CodexAppServerDriver implements HarnessDriver { return this.#options.baseInstructions ?? CODEX_SKILLLESS_BASE_INSTRUCTIONS; } + #instructionParams(instructions = this.#baseInstructions()): Record { + // baseInstructions replaces Codex's stock prompt. Other providers use this + // driver as a protocol facade and retain their existing instruction field. + return (this.#options.driverIdentity?.kind ?? DRIVER_KIND) === DRIVER_KIND + ? { developerInstructions: instructions } + : { baseInstructions: instructions }; + } + async descriptor(): Promise { const unsupported = Object.entries(this.#caps) .filter(([, supported]) => !supported) @@ -280,7 +288,7 @@ export class CodexAppServerDriver implements HarnessDriver { ...(this.#direct() ? {} : { - baseInstructions: this.#baseInstructions(), + ...this.#instructionParams(), completionContract: { revision: this.#options.taskEnvelope.completionContract.revision, @@ -408,7 +416,7 @@ export class CodexAppServerDriver implements HarnessDriver { this.#options.includeSkillInstructions ?? false, this.#options.environment, ), - baseInstructions: this.#direct() ? "" : this.#baseInstructions(), + ...this.#instructionParams(this.#direct() ? "" : this.#baseInstructions()), approvalPolicy: this.#options.approvalPolicy ?? "never", ...(this.#options.model ? { model: this.#options.model } : {}), dynamicTools: this.#providerDynamicTools(), diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.lifecycle.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.lifecycle.test.ts index 91ec041ee1..81e8a3def9 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.lifecycle.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.lifecycle.test.ts @@ -477,7 +477,7 @@ describe("Codex app-server Codex driver", () => { }); }); - it("places Paperclip runtime instructions in Codex's system channel and enables only selected skill instructions", async () => { + it("adds Paperclip developer instructions without replacing the Codex base and enables selected skills", async () => { const transport = new FakeCodexTransport(); const baseInstructions = [ "You are running as a Paperclip agent.", @@ -499,17 +499,68 @@ describe("Codex app-server Codex driver", () => { (call) => call.method === "thread/start", ); expect(threadStart?.params).toMatchObject({ - baseInstructions, + developerInstructions: baseInstructions, config: { "skills.include_instructions": true, include_apps_instructions: false, }, }); + expect(threadStart?.params).not.toHaveProperty("baseInstructions"); expect(JSON.stringify(threadStart?.params.input ?? null)).not.toContain( baseInstructions, ); }); + it.each(["task", "prepared", "direct"] as const)("preserves stock Codex instructions on %s recovery", async (conversationMode) => { + const originalTransport = new FakeCodexTransport(); + const recoveryTransport = new FakeCodexTransport(); + const baseInstructions = "Paperclip coordination and assigned instruction paths."; + const driver = makeDriver([originalTransport, recoveryTransport], { + conversationMode, + baseInstructions, + includeSkillInstructions: true, + }); + const original = await driver.openSession({ + runId: "run-additive-recovery", + normalizedSessionId: "normalized-additive-recovery", + workingDirectory: WORKSPACE, + }); + const snapshot = await original.snapshot(); + recoveryTransport.readResponse = { + thread: { id: snapshot.driverSessionId, sessionId: snapshot.providerSessionId, cwd: WORKSPACE, turns: [] }, + }; + await original.close({ reason: "verify additive recovery" }); + + const recovered = await driver.recoverSession(snapshot); + expect(recovered.recovered).toBe(true); + const started = originalTransport.calls.find((call) => call.method === "thread/start")!.params; + const resumed = recoveryTransport.calls.find((call) => call.method === "thread/resume")!.params; + expect(started).not.toHaveProperty("baseInstructions"); + expect(resumed).not.toHaveProperty("baseInstructions"); + expect(started.developerInstructions).toBe(conversationMode === "direct" ? undefined : baseInstructions); + expect(resumed.developerInstructions).toBe(conversationMode === "direct" ? "" : baseInstructions); + expect(resumed.dynamicTools).toEqual(started.dynamicTools); + expect(resumed.config).toEqual(started.config); + await recovered.session?.close({ reason: "verified additive recovery" }); + }); + + it("retains the instruction field for non-Codex provider facades", async () => { + const transport = new FakeCodexTransport(); + const driver = makeDriver([transport], { + baseInstructions: "Provider-specific runtime context.", + driverIdentity: { kind: "opencode_server", displayName: "OpenCode", version: "1" }, + }); + const session = await driver.openSession({ + runId: "run-facade-instructions", + normalizedSessionId: "normalized-facade-instructions", + workingDirectory: WORKSPACE, + }); + const started = transport.calls.find((call) => call.method === "thread/start")!.params; + expect(started.baseInstructions).toBe("Provider-specific runtime context."); + expect(started).not.toHaveProperty("developerInstructions"); + await session.close({ reason: "verified facade instructions" }); + }); + it("passes the common typed-event contract and reports one provider turn terminal", async () => { const transport = new FakeCodexTransport(); const driver = makeDriver([transport]); diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.test.ts index 3f68152e45..2f667d72ee 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.test.ts @@ -572,7 +572,7 @@ describe("Codex app-server Codex driver", () => { }); }); - it("places Paperclip runtime instructions in Codex's system channel and enables only selected skill instructions", async () => { + it("adds Paperclip developer instructions without replacing the Codex base and enables selected skills", async () => { const transport = new FakeCodexTransport(); const baseInstructions = [ "You are running as a Paperclip agent.", @@ -592,12 +592,13 @@ describe("Codex app-server Codex driver", () => { const threadStart = transport.calls.find((call) => call.method === "thread/start"); expect(threadStart?.params).toMatchObject({ - baseInstructions, + developerInstructions: baseInstructions, config: { "skills.include_instructions": true, include_apps_instructions: false, }, }); + expect(threadStart?.params).not.toHaveProperty("baseInstructions"); expect(JSON.stringify(threadStart?.params.input ?? null)).not.toContain(baseInstructions); }); diff --git a/packages/paperclip-runner/src/drivers/codex/codex-driver-types.ts b/packages/paperclip-runner/src/drivers/codex/codex-driver-types.ts index 77303d77be..33b56c880a 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-driver-types.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-driver-types.ts @@ -20,6 +20,8 @@ export interface CodexAppServerDriverOptions { /** Per-run reasoning effort sent with each Codex turn, including resumed turns. */ reasoningEffort?: string; approvalPolicy?: "never" | "on-request" | "untrusted"; + /** Paperclip runtime instructions. Codex receives these as additive developer + * instructions; the historical option name remains compatible with callers. */ baseInstructions?: string; includeSkillInstructions?: boolean; /** Private instruction directory registered by the control plane for this run. */ diff --git a/packages/paperclip-runner/src/live/live-session.test.ts b/packages/paperclip-runner/src/live/live-session.test.ts index 8eef8f295f..938479a0da 100644 --- a/packages/paperclip-runner/src/live/live-session.test.ts +++ b/packages/paperclip-runner/src/live/live-session.test.ts @@ -918,8 +918,9 @@ describe("Capability live runnerd and Codex session", () => { expect(names.filter((name) => name === "paperclip_finish")).toHaveLength(1); expect(names.filter((name) => name === "paperclip_block")).toHaveLength(1); expect(names).not.toContain("create_task"); - expect(opened.params.baseInstructions).toContain('"revision":"paperclip-capability-live-v1"'); - expect(opened.params.baseInstructions).toContain('"criterionIds":["objective"]'); + expect(opened.params).not.toHaveProperty("baseInstructions"); + expect(opened.params.developerInstructions).toContain('"revision":"paperclip-capability-live-v1"'); + expect(opened.params.developerInstructions).toContain('"criterionIds":["objective"]'); await first.suspend(); const restoredService = new CapabilityLiveSessionService({ store, transportFactory: factory }); const restored = await restoredService.restore(first.id); @@ -928,7 +929,8 @@ describe("Capability live runnerd and Codex session", () => { names.filter((name) => name !== "paperclip_finish" && name !== "paperclip_block"), ); const resumed = state.transports[1]!.requests.find((request) => request.method === "thread/resume")!; - expect(resumed.params.baseInstructions).toBe(opened.params.baseInstructions); + expect(resumed.params).not.toHaveProperty("baseInstructions"); + expect(resumed.params.developerInstructions).toBe(opened.params.developerInstructions); await restoredService.shutdown(restored.id); await firstService.shutdown(first.id); }); @@ -989,11 +991,11 @@ describe("Capability live runnerd and Codex session", () => { expect( state.transports[0]?.requests.find( (request) => request.method === "thread/start", - )?.params.baseInstructions, + )?.params.developerInstructions, ).toContain( "Native instructions\n\nRead-only instruction sibling root: /runtime/instructions", ); - expect(state.transports[0]?.requests.find((request) => request.method === "thread/start")?.params.baseInstructions) + expect(state.transports[0]?.requests.find((request) => request.method === "thread/start")?.params.developerInstructions) .toContain('Native completion report contract: {"revision":"paperclip-capability-live-v1","criterionIds":["objective"]}'); await service.shutdown(session.id); }); diff --git a/packages/paperclip-runner/src/live/live-session.ts b/packages/paperclip-runner/src/live/live-session.ts index d24995b5ec..28ec49d172 100644 --- a/packages/paperclip-runner/src/live/live-session.ts +++ b/packages/paperclip-runner/src/live/live-session.ts @@ -2479,7 +2479,7 @@ export class CapabilityLiveSession { config: createSkilllessCodexThreadConfig(this.#config.workingDirectory), permissions: CODEX_PERMISSION_PROFILE, runtimeWorkspaceRoots: [this.#config.workingDirectory], - baseInstructions, + ...(provider === "codex" ? { developerInstructions: baseInstructions } : { baseInstructions }), persistExtendedHistory: true, }); const resumedThread = record(resumed.thread); @@ -2504,7 +2504,7 @@ export class CapabilityLiveSession { permissions: CODEX_PERMISSION_PROFILE, runtimeWorkspaceRoots: [this.#config.workingDirectory], approvalPolicy: "never", - baseInstructions, + ...(provider === "codex" ? { developerInstructions: baseInstructions } : { baseInstructions }), completionContract: LIVE_COMPLETION_CONTRACT, dynamicTools: [...semanticTools, ...codexSemanticToolSpecs()], experimentalRawEvents: true, diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts index 94c744c7e5..8ab547d221 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts @@ -4247,11 +4247,14 @@ it("captures exact provider frames and correlates Rust and TypeScript interpreta decodedFrames.find((frame) => frame.method === "thread/start"), ).toMatchObject({ params: { - baseInstructions: withCodexCollaborationRuntimeInstructions( + developerInstructions: withCodexCollaborationRuntimeInstructions( CODEX_SKILLLESS_BASE_INSTRUCTIONS, ), }, }); + expect( + (decodedFrames.find((frame) => frame.method === "thread/start")?.params as Record), + ).not.toHaveProperty("baseInstructions"); const stages = new Set( [...nativeEntries, ...rehydratedEntries] .filter((entry) => entry.kind === "interpretation") diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts index 1a49999f21..c067f2a541 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts @@ -4544,7 +4544,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { provider === "codex" && record(params.config).include_collaboration_mode_instructions !== false; const unboundBaseInstructions = String( - params.baseInstructions ?? "You are a Paperclip agent.", + params.developerInstructions ?? params.baseInstructions ?? "You are a Paperclip agent.", ); const baseInstructions = sourceRuntimeContext && runtimeContext