From da934b6d3da4110ad296e4f007aa7f689873a13d Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 21 Aug 2026 10:02:53 -0500 Subject: [PATCH] fix(runner): buffer pre-accept Codex items --- .../drivers/codex/codex-app-server-driver.test.ts | 7 ++++++- .../src/drivers/codex/codex-app-server-driver.ts | 12 ++++++++++++ server/src/vendor/paperclip-runner/index.ts | 5 +++-- 3 files changed, 21 insertions(+), 3 deletions(-) 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 9db4ec23a99..248f91aa02e 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 @@ -1218,11 +1218,16 @@ describe("Codex app-server Codex driver", () => { normalizedSessionId: "normalized-implicit-item-turn", workingDirectory: "/workspace", }); - const turn = await session.startTurn({ message: { role: "user", text: "Complete." } }); + let resolveTurnStart!: (value: Record) => void; + transport.turnStartResponse = new Promise((resolve) => { resolveTurnStart = resolve; }); + const starting = session.startTurn({ message: { role: "user", text: "Complete." } }); + while (!transport.calls.some((call) => call.method === "turn/start")) await Promise.resolve(); transport.push("item/completed", { threadId: "thread-1", item: { id: "answer-implicit", type: "agentMessage", text: JSON.stringify(result) }, }); + resolveTurnStart({ turn: { id: "turn-1", status: "inProgress", items: [] } }); + const turn = await starting; transport.push("turn/completed", { threadId: "thread-1", turn: { id: turn.turnId, status: "completed", items: [] }, diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.ts index f7cf49ac815..51b74903f4d 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.ts @@ -663,6 +663,7 @@ class CodexHarnessSession implements HarnessSession { #resultCallId: string | null = null; #resultTurnId: string | null = null; #turnStartPending = false; + readonly #pendingTurnNotifications: CodexRpcNotification[] = []; #protocolFailed = false; #protocolFailureCode: string | null = null; #protocolFailureMessage: string | null = null; @@ -822,6 +823,8 @@ class CodexHarnessSession implements HarnessSession { } this.#activeTurnId ??= turnId; this.#emit("turn.accepted", { turnId }, { turnId }); + const pendingTurnNotifications = this.#pendingTurnNotifications.splice(0); + for (const notification of pendingTurnNotifications) this.#mapNotification(notification); if (this.#interruptQueued) { this.#interruptQueued = false; await this.#sendInterrupt(turnId, "queued_before_start"); @@ -1125,6 +1128,15 @@ class CodexHarnessSession implements HarnessSession { const item = itemFromParams(params); const threadId = text(params.threadId); const explicitTurnId = text(params.turnId, text(turn.id)); + if ( + explicitTurnId.length === 0 + && this.#turnStartPending + && this.#activeTurnId === null + && (notification.method.startsWith("item/") || notification.method === "thread/tokenUsage/updated") + ) { + this.#pendingTurnNotifications.push(structuredClone(notification)); + return; + } // Current Codex app-server builds omit turnId from some item notifications. This driver // permits only one active turn, so a same-thread item can be bound to that turn without // ambiguity. Pre-turn, cross-thread, stale, and post-terminal notifications still fail closed. diff --git a/server/src/vendor/paperclip-runner/index.ts b/server/src/vendor/paperclip-runner/index.ts index 6e943c26b8b..5922c21c97c 100644 --- a/server/src/vendor/paperclip-runner/index.ts +++ b/server/src/vendor/paperclip-runner/index.ts @@ -1,4 +1,5 @@ -// Development and test builds resolve the workspace package here. The server -// build replaces this compiled shim with the package's built distribution so +// Development and test builds resolve the workspace package here. Rebuild the package and restart +// the server after changing runner internals; the workspace export resolves through dist. The +// server build replaces this compiled shim with the package's built distribution so // published server installs do not need a separate paperclip-runner npm bootstrap. export * from "@paperclipai/paperclip-runner";