From 7c06af04a7ab9672c6e38b9ed8004e7cc485b5e4 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:57:02 +0530 Subject: [PATCH 1/2] fix(x): unblock codex code-mode (bad cwd + revoked engine cert) code_agent_run failed with a misleading "spawn ENOENT" that read as "Codex isn't installed". Two stacked causes: 1. cwd wasn't expanded/validated: a `~` or non-existent path was passed straight to child_process.spawn, which reports ENOENT against the command (Electron) rather than the missing directory. Now expandHome + resolve + existence-check with a clear error. 2. Apple revoked the signing cert on the pinned @openai/codex@0.128.0, so macOS Gatekeeper trashed the binary on launch and the ACP adapter died mid-handshake. Bump the ACP stack off the revoked build: codex-acp 0.0.44 -> 1.1.0, claude-agent-acp 0.39 -> 0.55, sdk 0.22 -> 1.1, regen engine-manifest (codex 0.142.5 / claude 0.3.198). Removed the two now-obsolete patches (contextCompaction upstreamed; the codex windowsHide patch targeted bin/codex.js, which we bypass via CODEX_PATH). Bump fallout handled: - client.ts setModel: sdk 1.x dropped unstable_setSessionModel; model is now a config option -> setSessionConfigOption({configId:'model'}). - engine-provisioner: codex 0.142 moved its binary (codex/ -> bin/) and rg (path/ -> codex-path/); probe both layouts. - pnpm override vscode-jsonrpc to 8.2.0: codex-acp 1.1 pulls jsonrpc 9.x whose restrictive exports break langium's deep import in the renderer (blank screen). codex-acp is the only 9.x consumer and works on 8.x (handshake verified), so pin the workspace to 8.x. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/x/apps/main/package.json | 4 +- apps/x/packages/core/package.json | 6 +- .../core/src/application/lib/builtin-tools.ts | 15 +- .../packages/core/src/code-mode/acp/client.ts | 5 +- .../core/src/code-mode/acp/engine-manifest.ts | 88 +++---- .../src/code-mode/acp/engine-provisioner.ts | 18 +- ...gentclientprotocol__codex-acp@0.0.44.patch | 57 ----- apps/x/patches/@openai__codex@0.128.0.patch | 15 -- apps/x/pnpm-lock.yaml | 214 ++++++++---------- apps/x/pnpm-workspace.yaml | 7 +- 10 files changed, 180 insertions(+), 249 deletions(-) delete mode 100644 apps/x/patches/@agentclientprotocol__codex-acp@0.0.44.patch delete mode 100644 apps/x/patches/@openai__codex@0.128.0.patch diff --git a/apps/x/apps/main/package.json b/apps/x/apps/main/package.json index a7eca38e..2577b0c1 100644 --- a/apps/x/apps/main/package.json +++ b/apps/x/apps/main/package.json @@ -13,8 +13,8 @@ "make": "electron-forge make" }, "dependencies": { - "@agentclientprotocol/claude-agent-acp": "^0.39.0", - "@agentclientprotocol/codex-acp": "^0.0.44", + "@agentclientprotocol/claude-agent-acp": "^0.55.0", + "@agentclientprotocol/codex-acp": "^1.1.0", "@x/core": "workspace:*", "@x/shared": "workspace:*", "agent-slack": "0.9.3", diff --git a/apps/x/packages/core/package.json b/apps/x/packages/core/package.json index f17f0bc4..14d35e81 100644 --- a/apps/x/packages/core/package.json +++ b/apps/x/packages/core/package.json @@ -13,9 +13,9 @@ "inspect": "node dist/turns/inspect-cli.js" }, "dependencies": { - "@agentclientprotocol/claude-agent-acp": "^0.39.0", - "@agentclientprotocol/codex-acp": "^0.0.44", - "@agentclientprotocol/sdk": "^0.22.1", + "@agentclientprotocol/claude-agent-acp": "^0.55.0", + "@agentclientprotocol/codex-acp": "^1.1.0", + "@agentclientprotocol/sdk": "^1.1.0", "@ai-sdk/anthropic": "^2.0.63", "@ai-sdk/google": "^2.0.53", "@ai-sdk/openai": "^2.0.91", diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts index 2f74a9c5..bf40ab1b 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -876,8 +876,19 @@ export const BuiltinTools: z.infer = { // chip change. Honor the chip so switching it deterministically switches agents. const effectiveAgent = ctx.codeMode ?? agent; // Code-section sessions pin the working directory — never trust the model's - // cwd argument over the session's. - const effectiveCwd = ctx.codeCwd ?? cwd; + // cwd argument over the session's. Expand `~` and resolve to an absolute path: + // the engine is spawned with this as the child's cwd, and `child_process.spawn` + // does NO shell tilde expansion. + const effectiveCwd = path.resolve(expandHome(ctx.codeCwd ?? cwd)); + // Fail loudly if the directory is missing. Otherwise the spawn below fails with + // Node's misleading "spawn ENOENT" (it blames the executable, not the + // bad cwd), which reads as "the coding engine isn't installed" — see the enriched + // message the model surfaces. A clear error lets the model/user fix the path. + try { + if (!(await fs.stat(effectiveCwd)).isDirectory()) throw new Error('not a directory'); + } catch { + throw new Error(`code_agent_run: working directory does not exist: ${effectiveCwd}`); + } const manager = container.resolve('codeModeManager'); const registry = container.resolve('codePermissionRegistry'); diff --git a/apps/x/packages/core/src/code-mode/acp/client.ts b/apps/x/packages/core/src/code-mode/acp/client.ts index f7351604..ba55bb34 100644 --- a/apps/x/packages/core/src/code-mode/acp/client.ts +++ b/apps/x/packages/core/src/code-mode/acp/client.ts @@ -275,8 +275,11 @@ export class AcpClient { // Point the open session at a specific model. The adapter resolves aliases // ("opus"/"sonnet"/…) to concrete ids. Throws if the model is unknown; the // caller applies this best-effort so a bad value never blocks a turn. + // ACP 1.x folded model selection into the generic config-option system (the + // 'model' category), so this goes through setSessionConfigOption just like + // effort does — matching the id extractModelOptions reads. async setModel(sessionId: string, modelId: string): Promise { - await this.conn().unstable_setSessionModel({ sessionId, modelId }); + await this.conn().setSessionConfigOption({ sessionId, configId: 'model', value: modelId }); } // Set the reasoning-effort level via the agent's "effort" config option. diff --git a/apps/x/packages/core/src/code-mode/acp/engine-manifest.ts b/apps/x/packages/core/src/code-mode/acp/engine-manifest.ts index 8d2f042d..274d0cc8 100644 --- a/apps/x/packages/core/src/code-mode/acp/engine-manifest.ts +++ b/apps/x/packages/core/src/code-mode/acp/engine-manifest.ts @@ -4,96 +4,96 @@ export const ENGINE_MANIFEST = { "claude": { - "version": "0.3.156", + "version": "0.3.198", "platforms": { "darwin-arm64": { "pkg": "@anthropic-ai/claude-agent-sdk-darwin-arm64", - "pkgVersion": "0.3.156", - "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.156.tgz", - "integrity": "sha512-IkjcS9dqAUlD4Nb62L9AZtmAXCa+FV4ul8lIlyXXUprh3nlecbKsWOXVd/GORrzAhMmynJaX4+iV1JiutFKXUA==" + "pkgVersion": "0.3.198", + "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.198.tgz", + "integrity": "sha512-ZmiAybQKIKcP1qEAE/vfXvfxtKxG9CnJn98QTXC5Zxiwuy7Mllx2ALXh9dfmsf0V87CGEodlZQmMgUJotNIsUw==" }, "darwin-x64": { "pkg": "@anthropic-ai/claude-agent-sdk-darwin-x64", - "pkgVersion": "0.3.156", - "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.156.tgz", - "integrity": "sha512-6PKi5fPmGRuzXu+Em/iwLmPG3mqg0hl92wcTU8fmChqyNtxhxsjCw7LTbdFqp/05o5NeZVVV4k3p7YUv5IFD6g==" + "pkgVersion": "0.3.198", + "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.198.tgz", + "integrity": "sha512-XwH5vgN46WSwg8aC1OagNofnJpV/G1ciEu118GEKer8ZhVkq/dvK/DqShxMkb6r1jV7u5IJ7zPXu9uKliyNJAw==" }, "linux-x64": { "pkg": "@anthropic-ai/claude-agent-sdk-linux-x64", - "pkgVersion": "0.3.156", - "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.156.tgz", - "integrity": "sha512-ymhrdlbWoYvTACUdaGdhrEv+ZMfwXLsf0BRLkr/IvY5aqybP7URzWmmZGOtDQpqkT/8xu/UCGqUYH3woJwUxfg==" + "pkgVersion": "0.3.198", + "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.198.tgz", + "integrity": "sha512-Zqxyz2AT1UM5WlOOoLJhLssZDgZo8rBK5ku6daveK12zp+UTJGZhGsjFghz1/ASxH08KqOTbUePNTORnPhHAEQ==" }, "linux-arm64": { "pkg": "@anthropic-ai/claude-agent-sdk-linux-arm64", - "pkgVersion": "0.3.156", - "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.156.tgz", - "integrity": "sha512-H0Nfd41iw5isto9uQI1FlVSZ0eaDttr8rBpJMR25oK/mj3egMO5EmZ6aAxeeUYSLn2mSU50HA5VNxlGUE118TQ==" + "pkgVersion": "0.3.198", + "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.198.tgz", + "integrity": "sha512-qmz8dxEtDIlKntU5qYe0R4aWTxTue5S7zIQknatLX7aJ6HN/nq1aCNXWn5smTH2FViBkUPPR+sCIsNwSk6AT6Q==" }, "linux-x64-musl": { "pkg": "@anthropic-ai/claude-agent-sdk-linux-x64-musl", - "pkgVersion": "0.3.156", - "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.156.tgz", - "integrity": "sha512-/Q6WUizI6a+hqZZ6ElwRU0PEuFhOoN4v6CuU35HHbiZ/7uaocGht4A8ZIgK1Fw6wOGtZzGLbc00CA1OU1Zg8EA==" + "pkgVersion": "0.3.198", + "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.198.tgz", + "integrity": "sha512-h1SrWVIMjLInYNPlf+TxXuKTOdoiOfJLBSoQG97315Z2Nh0IpBfqWExlqYTtPCgKE7q2iga31U283QfHpIDlSQ==" }, "linux-arm64-musl": { "pkg": "@anthropic-ai/claude-agent-sdk-linux-arm64-musl", - "pkgVersion": "0.3.156", - "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.156.tgz", - "integrity": "sha512-R7KEVjxkR4rYgIQoHGBzwPdUJYxRTO8I4vHjRbMLH1eW4FS7BJvVs7ogfKR/NnHFBvMVqtC+l6jHLQv8bobUiw==" + "pkgVersion": "0.3.198", + "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.198.tgz", + "integrity": "sha512-Q7lKVNjIrUQ2B/AR77OvRf0zeOdEjonFVaR9FYrrwtzGeEqum69WSht5nM7Y7el3wjbNi0/eV0QTUM0DlsTEfw==" }, "win32-x64": { "pkg": "@anthropic-ai/claude-agent-sdk-win32-x64", - "pkgVersion": "0.3.156", - "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.156.tgz", - "integrity": "sha512-/PofeTWoiKgnWNSNk0wG4SsRn22GGLmnLhg2R94WcNhCRFOyOTmiZcYH2DBlWZBIRVTZDsSfa/Pl1DyPvYCGKw==" + "pkgVersion": "0.3.198", + "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.198.tgz", + "integrity": "sha512-y3HLuCCz1kDwUrhd6OnqO+d5BUpTFSzNUsPT9kf3r1vk9HYKF+eMC9eIlcOhiW2kX491kxEvuEOfqgIkGx15cg==" }, "win32-arm64": { "pkg": "@anthropic-ai/claude-agent-sdk-win32-arm64", - "pkgVersion": "0.3.156", - "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.156.tgz", - "integrity": "sha512-5sAeNObQQrMy4NF9HwxewrMnU7mVxZDHh+/MfJVQSz0GSTvXQ6gOuRH8helMlfspoU6VOdekPxVLRooX/3foEw==" + "pkgVersion": "0.3.198", + "tarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.198.tgz", + "integrity": "sha512-mjIHf1HFiRuXefewWTaNZFlTZlCaEt/xsRjc1nSTCEEpFolZayVhrDKz+O2QFVcDtPl8x8GeYSL0kiikg1DZjQ==" } } }, "codex": { - "version": "0.128.0", + "version": "0.142.5", "platforms": { "darwin-arm64": { "pkg": "@openai/codex", - "pkgVersion": "0.128.0-darwin-arm64", - "tarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.128.0-darwin-arm64.tgz", - "integrity": "sha512-w+6zohfHx/kHBdles/CyFKaY57u9I3nK8QI9+NrdwMliKA0b7xn13yblRNkMpe09j6vL1oAWoxYsMOQ/vjBGug==" + "pkgVersion": "0.142.5-darwin-arm64", + "tarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-darwin-arm64.tgz", + "integrity": "sha512-l43p8xv+Z/2/b6fCUc7/FmcQZsaPB7RFizLponGwHAnFOWe3i9Vky69p+up3BUam9AetoQQUv7Mo+2KdaFEqhA==" }, "darwin-x64": { "pkg": "@openai/codex", - "pkgVersion": "0.128.0-darwin-x64", - "tarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.128.0-darwin-x64.tgz", - "integrity": "sha512-SDbn6fO22Puy8xmMIbZi4f2znMrUEPwABApke4mo+4ihaauwuVjeqzXvW5SPJz5ty/bG11/mSupQgReT7T8BBw==" + "pkgVersion": "0.142.5-darwin-x64", + "tarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-darwin-x64.tgz", + "integrity": "sha512-yk6A06/VmW7NFsa48OVPaj//g/zeSpd79wjuqfXZwW8ZKRYQm3+wCd3hWjPl79F3QnXvDvM2j3JMIBL3m3GXXg==" }, "linux-x64": { "pkg": "@openai/codex", - "pkgVersion": "0.128.0-linux-x64", - "tarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.128.0-linux-x64.tgz", - "integrity": "sha512-2lnSPA05CRRuKAzFW8BCmmNCSieDcToLwfC2ALLbBYilGLgzhRibjlDglK9F1BkEzfohSSWJu4PBbRu/aG60lQ==" + "pkgVersion": "0.142.5-linux-x64", + "tarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-linux-x64.tgz", + "integrity": "sha512-pxY+d3NgNE57Y/MApD3/TZUAygxJN6I9h3ZeDUwe67mxWjUxsuapxMRFTKSznCalYbRAeZp752+AAXmUbmguEg==" }, "linux-arm64": { "pkg": "@openai/codex", - "pkgVersion": "0.128.0-linux-arm64", - "tarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.128.0-linux-arm64.tgz", - "integrity": "sha512-+SvH73H60qvCXFuQGP/EsmR//s1hHMBR22PvJkXvM/hdnTIGucx+JqRUjAWdmmQ1IU6j3kgwVvdLW/6ICB+M6w==" + "pkgVersion": "0.142.5-linux-arm64", + "tarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-linux-arm64.tgz", + "integrity": "sha512-77ka5PSnm5HdxdBT99IwntCasmbqevlS0eiC0AtEb6ZXCLkim2gm0AWm+jNYy0EhbssvNK+KghayWo34HMgXeA==" }, "win32-x64": { "pkg": "@openai/codex", - "pkgVersion": "0.128.0-win32-x64", - "tarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.128.0-win32-x64.tgz", - "integrity": "sha512-k3jmUAFrzkUtvjGTXvSKjQqJLLlzjxp/VoHJDYedgmXUn6j70HxK38IwapzmnYfiBiTuzETvGwjXHzZgzKjhoQ==" + "pkgVersion": "0.142.5-win32-x64", + "tarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-win32-x64.tgz", + "integrity": "sha512-a+wI4PEx9a2fg6V5ueTTDkOkr1XpEvA5RFXIbo/L2hOfzMmGtyRnbG24bCGu5Q2RSgVxSQV0aLkdb3vdYMNH9A==" }, "win32-arm64": { "pkg": "@openai/codex", - "pkgVersion": "0.128.0-win32-arm64", - "tarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.128.0-win32-arm64.tgz", - "integrity": "sha512-ECJvsqmYFdA9pn42xxK3Odp/G16AjmBW0BglX8L0PwPjqbstbmlew9bfHf7xvL+SNfNl4NmyotW0+RNo1phgaA==" + "pkgVersion": "0.142.5-win32-arm64", + "tarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-win32-arm64.tgz", + "integrity": "sha512-65BEqGbUZ7r0ayunIHdBjo5crwgbwKX/6puOcO+VCswUw/dXvDsN2IGcbXB52+bS9U5+FxP783cUHfTT6m40DQ==" } } } diff --git a/apps/x/packages/core/src/code-mode/acp/engine-provisioner.ts b/apps/x/packages/core/src/code-mode/acp/engine-provisioner.ts index fd69d105..807ad13d 100644 --- a/apps/x/packages/core/src/code-mode/acp/engine-provisioner.ts +++ b/apps/x/packages/core/src/code-mode/acp/engine-provisioner.ts @@ -89,13 +89,16 @@ function locateExecutable(agent: CodingAgent, root: string): string | null { } return null; } - // codex: vendor//codex/codex[.exe] + // codex ships its native binary under vendor//. The containing + // subdir moved from `codex/` (≤0.128) to `bin/` (≥0.142), so probe both. const vendor = path.join(root, 'vendor'); if (!fs.existsSync(vendor)) return null; for (const triple of fs.readdirSync(vendor)) { - for (const name of ['codex', 'codex.exe']) { - const p = path.join(vendor, triple, 'codex', name); - if (fs.existsSync(p)) return p; + for (const sub of ['bin', 'codex']) { + for (const name of ['codex', 'codex.exe']) { + const p = path.join(vendor, triple, sub, name); + if (fs.existsSync(p)) return p; + } } } return null; @@ -224,8 +227,11 @@ function makeExecutable(agent: CodingAgent, root: string, exe: string): void { if (agent === 'codex') { const vendor = path.join(root, 'vendor'); for (const triple of fs.existsSync(vendor) ? fs.readdirSync(vendor) : []) { - const rg = path.join(vendor, triple, 'path', 'rg'); - if (fs.existsSync(rg)) fs.chmodSync(rg, 0o755); + // Bundled ripgrep moved from `path/` (≤0.128) to `codex-path/` (≥0.142). + for (const sub of ['codex-path', 'path']) { + const rg = path.join(vendor, triple, sub, 'rg'); + if (fs.existsSync(rg)) fs.chmodSync(rg, 0o755); + } } } } diff --git a/apps/x/patches/@agentclientprotocol__codex-acp@0.0.44.patch b/apps/x/patches/@agentclientprotocol__codex-acp@0.0.44.patch deleted file mode 100644 index 79ab1623..00000000 --- a/apps/x/patches/@agentclientprotocol__codex-acp@0.0.44.patch +++ /dev/null @@ -1,57 +0,0 @@ -diff --git a/dist/index.js b/dist/index.js ---- a/dist/index.js -+++ b/dist/index.js -@@ -17678,15 +17678,22 @@ - case "dynamicToolCall": - return await createDynamicToolCallUpdate(event.item); -+ case "contextCompaction": -+ return { -+ sessionUpdate: "tool_call", -+ toolCallId: event.item.id, -+ kind: "other", -+ title: "Compacting context", -+ status: "in_progress" -+ }; - case "collabAgentToolCall": - case "userMessage": - case "hookPrompt": - case "agentMessage": - case "reasoning": - case "webSearch": - case "imageView": - case "imageGeneration": - case "enteredReviewMode": - case "exitedReviewMode": -- case "contextCompaction": - case "plan": - return null; -@@ -17713,23 +17720,28 @@ - return this.completeCommandExecutionEvent(event.item); - case "reasoning": - const summary = event.item.summary[0]; - if (!summary) return null; - return { - sessionUpdate: "agent_thought_chunk", - content: { - type: "text", - text: summary - } - }; -+ case "contextCompaction": -+ return { -+ sessionUpdate: "tool_call_update", -+ toolCallId: event.item.id, -+ status: "completed" -+ }; - case "collabAgentToolCall": - case "userMessage": - case "hookPrompt": - case "agentMessage": - case "webSearch": - case "imageView": - case "imageGeneration": - case "enteredReviewMode": - case "exitedReviewMode": -- case "contextCompaction": - case "plan": - return null; diff --git a/apps/x/patches/@openai__codex@0.128.0.patch b/apps/x/patches/@openai__codex@0.128.0.patch deleted file mode 100644 index 73b2e0c3..00000000 --- a/apps/x/patches/@openai__codex@0.128.0.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/bin/codex.js b/bin/codex.js -index 67ab3e2d95dfac1c91882578b5403916c3121484..f8030b6e1459e05161af99e152b2e7f65ea6c41d 100644 ---- a/bin/codex.js -+++ b/bin/codex.js -@@ -175,6 +175,10 @@ env[packageManagerEnvVar] = "1"; - const child = spawn(binaryPath, process.argv.slice(2), { - stdio: "inherit", - env, -+ // Native console-subsystem binary: without this Windows pops a visible console -+ // window when launched from a console-less (Electron GUI) parent. Closing that -+ // window wedges the agent. CREATE_NO_WINDOW keeps the console hidden. -+ windowsHide: true, - }); - - child.on("error", (err) => { diff --git a/apps/x/pnpm-lock.yaml b/apps/x/pnpm-lock.yaml index d3c69af4..ed73f72a 100644 --- a/apps/x/pnpm-lock.yaml +++ b/apps/x/pnpm-lock.yaml @@ -10,13 +10,8 @@ catalogs: specifier: 4.1.7 version: 4.1.7 -patchedDependencies: - '@agentclientprotocol/codex-acp@0.0.44': - hash: 0079b1ab3870451b47fe5ea5ecaca697e17139cff50fd2ee9c296b172aba525e - path: patches/@agentclientprotocol__codex-acp@0.0.44.patch - '@openai/codex@0.128.0': - hash: 9edd926108a95aaa788aa93870fd6b16d70eeccdf5740b503af5d34cc9f25e86 - path: patches/@openai__codex@0.128.0.patch +overrides: + vscode-jsonrpc: 8.2.0 importers: @@ -56,11 +51,11 @@ importers: apps/main: dependencies: '@agentclientprotocol/claude-agent-acp': - specifier: ^0.39.0 - version: 0.39.0(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1)) + specifier: ^0.55.0 + version: 0.55.0(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1)) '@agentclientprotocol/codex-acp': - specifier: ^0.0.44 - version: 0.0.44(patch_hash=0079b1ab3870451b47fe5ea5ecaca697e17139cff50fd2ee9c296b172aba525e)(zod@4.2.1) + specifier: ^1.1.0 + version: 1.1.0 '@x/core': specifier: workspace:* version: link:../../packages/core @@ -428,14 +423,14 @@ importers: packages/core: dependencies: '@agentclientprotocol/claude-agent-acp': - specifier: ^0.39.0 - version: 0.39.0(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1)) + specifier: ^0.55.0 + version: 0.55.0(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1)) '@agentclientprotocol/codex-acp': - specifier: ^0.0.44 - version: 0.0.44(patch_hash=0079b1ab3870451b47fe5ea5ecaca697e17139cff50fd2ee9c296b172aba525e)(zod@4.2.1) + specifier: ^1.1.0 + version: 1.1.0 '@agentclientprotocol/sdk': - specifier: ^0.22.1 - version: 0.22.1(zod@4.2.1) + specifier: ^1.1.0 + version: 1.1.0(zod@4.2.1) '@ai-sdk/anthropic': specifier: ^2.0.63 version: 2.0.70(zod@4.2.1) @@ -579,21 +574,17 @@ packages: '@adobe/css-tools@4.5.0': resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} - '@agentclientprotocol/claude-agent-acp@0.39.0': - resolution: {integrity: sha512-+tCm5v32L0R3zE4qjZQowfO1L/zqvQ5FapmsMSIf4gawXfTf26CG5hgz99wARdo0zn20/1eP80gzx7PbZlSX9A==} + '@agentclientprotocol/claude-agent-acp@0.55.0': + resolution: {integrity: sha512-zZ4EpT6OppURh3lrTTvQR42+ggPi5Q42TC7Nig+Vt0RGBAmw73xAnM6W5BpdNRL7VcT/2aRndV0Z7fuGrruMGA==} + engines: {node: '>=22'} hasBin: true - '@agentclientprotocol/codex-acp@0.0.44': - resolution: {integrity: sha512-iHzFWKzJ0Z8I6yJCkuLZ+nb9mF2WYmfTcHFFvc7sU/awBsQmVBmpSOXOpZ+IK2Dy9cR3iRoML/B2/Wq2/zKBCA==} + '@agentclientprotocol/codex-acp@1.1.0': + resolution: {integrity: sha512-EdUwW6n12yy4khxS6YpOgT8dd4JbVeOP8qPLdCEj795kp85qVI6369EWUXHq1CrnvOmGVwieUMvnrMlsqJaRWg==} hasBin: true - '@agentclientprotocol/sdk@0.21.1': - resolution: {integrity: sha512-ZTLH+o9QxcZDLX/9ww+W7C2iExnXFM+vD/uGFVSlR61Kzj9FaxUqBC6Rv/kwgA7qVWYUEI9c5ZNqCuO9PM4rKg==} - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - - '@agentclientprotocol/sdk@0.22.1': - resolution: {integrity: sha512-DfqXtl/8gO9NImq094MTaCXEU2vkhh6v7q/kT+9UjZxUqj8hYaya2OjLVIqn16MzNHcXEpShTR2RIauLSYeDQQ==} + '@agentclientprotocol/sdk@1.1.0': + resolution: {integrity: sha512-NT2KqphUJ3w6EksUL51ZhJgIYgq/ZLGcBPkyMKgRSO5PMVwe9DnKKX+Htnvk6KHh6dUuh34UHK4gKp+4te1Mdg==} peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -652,48 +643,48 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.156': - resolution: {integrity: sha512-IkjcS9dqAUlD4Nb62L9AZtmAXCa+FV4ul8lIlyXXUprh3nlecbKsWOXVd/GORrzAhMmynJaX4+iV1JiutFKXUA==} + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.198': + resolution: {integrity: sha512-ZmiAybQKIKcP1qEAE/vfXvfxtKxG9CnJn98QTXC5Zxiwuy7Mllx2ALXh9dfmsf0V87CGEodlZQmMgUJotNIsUw==} cpu: [arm64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.156': - resolution: {integrity: sha512-6PKi5fPmGRuzXu+Em/iwLmPG3mqg0hl92wcTU8fmChqyNtxhxsjCw7LTbdFqp/05o5NeZVVV4k3p7YUv5IFD6g==} + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.198': + resolution: {integrity: sha512-XwH5vgN46WSwg8aC1OagNofnJpV/G1ciEu118GEKer8ZhVkq/dvK/DqShxMkb6r1jV7u5IJ7zPXu9uKliyNJAw==} cpu: [x64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.156': - resolution: {integrity: sha512-R7KEVjxkR4rYgIQoHGBzwPdUJYxRTO8I4vHjRbMLH1eW4FS7BJvVs7ogfKR/NnHFBvMVqtC+l6jHLQv8bobUiw==} + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.198': + resolution: {integrity: sha512-Q7lKVNjIrUQ2B/AR77OvRf0zeOdEjonFVaR9FYrrwtzGeEqum69WSht5nM7Y7el3wjbNi0/eV0QTUM0DlsTEfw==} cpu: [arm64] os: [linux] - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.156': - resolution: {integrity: sha512-H0Nfd41iw5isto9uQI1FlVSZ0eaDttr8rBpJMR25oK/mj3egMO5EmZ6aAxeeUYSLn2mSU50HA5VNxlGUE118TQ==} + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.198': + resolution: {integrity: sha512-qmz8dxEtDIlKntU5qYe0R4aWTxTue5S7zIQknatLX7aJ6HN/nq1aCNXWn5smTH2FViBkUPPR+sCIsNwSk6AT6Q==} cpu: [arm64] os: [linux] - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.156': - resolution: {integrity: sha512-/Q6WUizI6a+hqZZ6ElwRU0PEuFhOoN4v6CuU35HHbiZ/7uaocGht4A8ZIgK1Fw6wOGtZzGLbc00CA1OU1Zg8EA==} + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.198': + resolution: {integrity: sha512-h1SrWVIMjLInYNPlf+TxXuKTOdoiOfJLBSoQG97315Z2Nh0IpBfqWExlqYTtPCgKE7q2iga31U283QfHpIDlSQ==} cpu: [x64] os: [linux] - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.156': - resolution: {integrity: sha512-ymhrdlbWoYvTACUdaGdhrEv+ZMfwXLsf0BRLkr/IvY5aqybP7URzWmmZGOtDQpqkT/8xu/UCGqUYH3woJwUxfg==} + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.198': + resolution: {integrity: sha512-Zqxyz2AT1UM5WlOOoLJhLssZDgZo8rBK5ku6daveK12zp+UTJGZhGsjFghz1/ASxH08KqOTbUePNTORnPhHAEQ==} cpu: [x64] os: [linux] - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.156': - resolution: {integrity: sha512-5sAeNObQQrMy4NF9HwxewrMnU7mVxZDHh+/MfJVQSz0GSTvXQ6gOuRH8helMlfspoU6VOdekPxVLRooX/3foEw==} + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.198': + resolution: {integrity: sha512-mjIHf1HFiRuXefewWTaNZFlTZlCaEt/xsRjc1nSTCEEpFolZayVhrDKz+O2QFVcDtPl8x8GeYSL0kiikg1DZjQ==} cpu: [arm64] os: [win32] - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.156': - resolution: {integrity: sha512-/PofeTWoiKgnWNSNk0wG4SsRn22GGLmnLhg2R94WcNhCRFOyOTmiZcYH2DBlWZBIRVTZDsSfa/Pl1DyPvYCGKw==} + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.198': + resolution: {integrity: sha512-y3HLuCCz1kDwUrhd6OnqO+d5BUpTFSzNUsPT9kf3r1vk9HYKF+eMC9eIlcOhiW2kX491kxEvuEOfqgIkGx15cg==} cpu: [x64] os: [win32] - '@anthropic-ai/claude-agent-sdk@0.3.156': - resolution: {integrity: sha512-6nM/Dj+VMds52UXJ2YaV4IKhYamlUqN0HtdDrFzYz5lvPMpDS935qD8YZDAUpy+ltdoD6PJMd1V/CKFY3/oWCQ==} + '@anthropic-ai/claude-agent-sdk@0.3.198': + resolution: {integrity: sha512-xt469sSCyclTPtzpLAg0Aschy665GiRMgZKabSmESbGUA5/H56HcILVOiFxclXswkeMUk2fQxfHJUY9UZfiTnA==} engines: {node: '>=18.0.0'} peerDependencies: '@anthropic-ai/sdk': '>=0.93.0' @@ -2295,43 +2286,43 @@ packages: resolution: {integrity: sha512-6gH/bLQJSJEg7OEpkH4wGQdA8KXHRbzL1YkGyUO12YNAgV3jxKy4K9kvfXj4+9T0OLug5k58cnPCKSSIKzp7pg==} engines: {node: '>=8.0'} - '@openai/codex@0.128.0': - resolution: {integrity: sha512-+xp6ODmFfBNnexIWRHApEaPXot2j6gyM8A5we/5IS/uY4eYHj4arETct4hQ5M4eO+MK7JY3ZU4xhuobhlysr0A==} + '@openai/codex@0.142.5': + resolution: {integrity: sha512-WQEpD7l3k68eIAP0aq28EdR18ENBAf8DyprzFhzNwCOQJSv4nHzpwT8Fl30IJacprko2ZCmUBZjM2u941l2yLw==} engines: {node: '>=16'} hasBin: true - '@openai/codex@0.128.0-darwin-arm64': - resolution: {integrity: sha512-w+6zohfHx/kHBdles/CyFKaY57u9I3nK8QI9+NrdwMliKA0b7xn13yblRNkMpe09j6vL1oAWoxYsMOQ/vjBGug==} + '@openai/codex@0.142.5-darwin-arm64': + resolution: {integrity: sha512-l43p8xv+Z/2/b6fCUc7/FmcQZsaPB7RFizLponGwHAnFOWe3i9Vky69p+up3BUam9AetoQQUv7Mo+2KdaFEqhA==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@openai/codex@0.128.0-darwin-x64': - resolution: {integrity: sha512-SDbn6fO22Puy8xmMIbZi4f2znMrUEPwABApke4mo+4ihaauwuVjeqzXvW5SPJz5ty/bG11/mSupQgReT7T8BBw==} + '@openai/codex@0.142.5-darwin-x64': + resolution: {integrity: sha512-yk6A06/VmW7NFsa48OVPaj//g/zeSpd79wjuqfXZwW8ZKRYQm3+wCd3hWjPl79F3QnXvDvM2j3JMIBL3m3GXXg==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@openai/codex@0.128.0-linux-arm64': - resolution: {integrity: sha512-+SvH73H60qvCXFuQGP/EsmR//s1hHMBR22PvJkXvM/hdnTIGucx+JqRUjAWdmmQ1IU6j3kgwVvdLW/6ICB+M6w==} + '@openai/codex@0.142.5-linux-arm64': + resolution: {integrity: sha512-77ka5PSnm5HdxdBT99IwntCasmbqevlS0eiC0AtEb6ZXCLkim2gm0AWm+jNYy0EhbssvNK+KghayWo34HMgXeA==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@openai/codex@0.128.0-linux-x64': - resolution: {integrity: sha512-2lnSPA05CRRuKAzFW8BCmmNCSieDcToLwfC2ALLbBYilGLgzhRibjlDglK9F1BkEzfohSSWJu4PBbRu/aG60lQ==} + '@openai/codex@0.142.5-linux-x64': + resolution: {integrity: sha512-pxY+d3NgNE57Y/MApD3/TZUAygxJN6I9h3ZeDUwe67mxWjUxsuapxMRFTKSznCalYbRAeZp752+AAXmUbmguEg==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@openai/codex@0.128.0-win32-arm64': - resolution: {integrity: sha512-ECJvsqmYFdA9pn42xxK3Odp/G16AjmBW0BglX8L0PwPjqbstbmlew9bfHf7xvL+SNfNl4NmyotW0+RNo1phgaA==} + '@openai/codex@0.142.5-win32-arm64': + resolution: {integrity: sha512-65BEqGbUZ7r0ayunIHdBjo5crwgbwKX/6puOcO+VCswUw/dXvDsN2IGcbXB52+bS9U5+FxP783cUHfTT6m40DQ==} engines: {node: '>=16'} cpu: [arm64] os: [win32] - '@openai/codex@0.128.0-win32-x64': - resolution: {integrity: sha512-k3jmUAFrzkUtvjGTXvSKjQqJLLlzjxp/VoHJDYedgmXUn6j70HxK38IwapzmnYfiBiTuzETvGwjXHzZgzKjhoQ==} + '@openai/codex@0.142.5-win32-x64': + resolution: {integrity: sha512-a+wI4PEx9a2fg6V5ueTTDkOkr1XpEvA5RFXIbo/L2hOfzMmGtyRnbG24bCGu5Q2RSgVxSQV0aLkdb3vdYMNH9A==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -5340,8 +5331,8 @@ packages: diff3@0.0.3: resolution: {integrity: sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==} - diff@8.0.4: - resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} dijkstrajs@1.0.3: @@ -8921,10 +8912,6 @@ packages: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} - vscode-jsonrpc@8.2.1: - resolution: {integrity: sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==} - engines: {node: '>=14.0.0'} - vscode-languageserver-protocol@3.17.5: resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} @@ -9190,32 +9177,31 @@ snapshots: '@adobe/css-tools@4.5.0': {} - '@agentclientprotocol/claude-agent-acp@0.39.0(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1))': + '@agentclientprotocol/claude-agent-acp@0.55.0(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1))': dependencies: - '@agentclientprotocol/sdk': 0.22.1(zod@4.2.1) - '@anthropic-ai/claude-agent-sdk': 0.3.156(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1))(zod@4.2.1) - zod: 4.2.1 + '@agentclientprotocol/sdk': 1.1.0(zod@4.4.3) + '@anthropic-ai/claude-agent-sdk': 0.3.198(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1))(zod@4.4.3) + zod: 4.4.3 transitivePeerDependencies: - '@anthropic-ai/sdk' - '@modelcontextprotocol/sdk' - '@agentclientprotocol/codex-acp@0.0.44(patch_hash=0079b1ab3870451b47fe5ea5ecaca697e17139cff50fd2ee9c296b172aba525e)(zod@4.2.1)': + '@agentclientprotocol/codex-acp@1.1.0': dependencies: - '@agentclientprotocol/sdk': 0.21.1(zod@4.2.1) - '@openai/codex': 0.128.0(patch_hash=9edd926108a95aaa788aa93870fd6b16d70eeccdf5740b503af5d34cc9f25e86) - diff: 8.0.4 + '@agentclientprotocol/sdk': 1.1.0(zod@4.4.3) + '@openai/codex': 0.142.5 + diff: 9.0.0 open: 11.0.0 - vscode-jsonrpc: 8.2.1 - transitivePeerDependencies: - - zod + vscode-jsonrpc: 8.2.0 + zod: 4.4.3 - '@agentclientprotocol/sdk@0.21.1(zod@4.2.1)': + '@agentclientprotocol/sdk@1.1.0(zod@4.2.1)': dependencies: zod: 4.2.1 - '@agentclientprotocol/sdk@0.22.1(zod@4.2.1)': + '@agentclientprotocol/sdk@1.1.0(zod@4.4.3)': dependencies: - zod: 4.2.1 + zod: 4.4.3 '@ai-sdk/anthropic@2.0.70(zod@4.2.1)': dependencies: @@ -9278,44 +9264,44 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.2 - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.156': + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.156': + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.156': + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.156': + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.156': + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.156': + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.156': + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.156': + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk@0.3.156(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1))(zod@4.2.1)': + '@anthropic-ai/claude-agent-sdk@0.3.198(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1))(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.100.1(zod@4.2.1) '@modelcontextprotocol/sdk': 1.25.1(hono@4.11.3)(zod@4.2.1) - zod: 4.2.1 + zod: 4.4.3 optionalDependencies: - '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.156 - '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.156 - '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.156 - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.156 - '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.156 - '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.156 - '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.156 - '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.156 + '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.198 + '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.198 + '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.198 + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.198 + '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.198 + '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.198 + '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.198 + '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.198 '@anthropic-ai/sdk@0.100.1(zod@4.2.1)': dependencies: @@ -11605,31 +11591,31 @@ snapshots: '@oozcitak/util@8.3.4': {} - '@openai/codex@0.128.0(patch_hash=9edd926108a95aaa788aa93870fd6b16d70eeccdf5740b503af5d34cc9f25e86)': + '@openai/codex@0.142.5': optionalDependencies: - '@openai/codex-darwin-arm64': '@openai/codex@0.128.0-darwin-arm64' - '@openai/codex-darwin-x64': '@openai/codex@0.128.0-darwin-x64' - '@openai/codex-linux-arm64': '@openai/codex@0.128.0-linux-arm64' - '@openai/codex-linux-x64': '@openai/codex@0.128.0-linux-x64' - '@openai/codex-win32-arm64': '@openai/codex@0.128.0-win32-arm64' - '@openai/codex-win32-x64': '@openai/codex@0.128.0-win32-x64' + '@openai/codex-darwin-arm64': '@openai/codex@0.142.5-darwin-arm64' + '@openai/codex-darwin-x64': '@openai/codex@0.142.5-darwin-x64' + '@openai/codex-linux-arm64': '@openai/codex@0.142.5-linux-arm64' + '@openai/codex-linux-x64': '@openai/codex@0.142.5-linux-x64' + '@openai/codex-win32-arm64': '@openai/codex@0.142.5-win32-arm64' + '@openai/codex-win32-x64': '@openai/codex@0.142.5-win32-x64' - '@openai/codex@0.128.0-darwin-arm64': + '@openai/codex@0.142.5-darwin-arm64': optional: true - '@openai/codex@0.128.0-darwin-x64': + '@openai/codex@0.142.5-darwin-x64': optional: true - '@openai/codex@0.128.0-linux-arm64': + '@openai/codex@0.142.5-linux-arm64': optional: true - '@openai/codex@0.128.0-linux-x64': + '@openai/codex@0.142.5-linux-x64': optional: true - '@openai/codex@0.128.0-win32-arm64': + '@openai/codex@0.142.5-win32-arm64': optional: true - '@openai/codex@0.128.0-win32-x64': + '@openai/codex@0.142.5-win32-x64': optional: true '@openrouter/ai-sdk-provider@1.5.4(ai@5.0.151(zod@4.2.1))(zod@4.2.1)': @@ -11640,7 +11626,7 @@ snapshots: '@openrouter/sdk@0.1.27': dependencies: - zod: 4.2.1 + zod: 4.4.3 '@opentelemetry/api-logs@0.208.0': dependencies: @@ -14994,7 +14980,7 @@ snapshots: diff3@0.0.3: {} - diff@8.0.4: {} + diff@9.0.0: {} dijkstrajs@1.0.3: {} @@ -19396,8 +19382,6 @@ snapshots: vscode-jsonrpc@8.2.0: {} - vscode-jsonrpc@8.2.1: {} - vscode-languageserver-protocol@3.17.5: dependencies: vscode-jsonrpc: 8.2.0 diff --git a/apps/x/pnpm-workspace.yaml b/apps/x/pnpm-workspace.yaml index d46f98a2..0e7566a4 100644 --- a/apps/x/pnpm-workspace.yaml +++ b/apps/x/pnpm-workspace.yaml @@ -12,6 +12,9 @@ allowBuilds: node-pty: true protobufjs: true +overrides: + vscode-jsonrpc: 8.2.0 + catalog: vitest: 4.1.7 @@ -24,7 +27,3 @@ onlyBuiltDependencies: - fs-xattr - macos-alias - protobufjs - -patchedDependencies: - '@agentclientprotocol/codex-acp@0.0.44': patches/@agentclientprotocol__codex-acp@0.0.44.patch - '@openai/codex@0.128.0': patches/@openai__codex@0.128.0.patch From 3bbed557351219b31d4b1af72909b3c1cf0b40ac Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:04:33 +0530 Subject: [PATCH 2/2] feat(x): ephemeral CodeRunFeed for codex streaming + settle-time durable batch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runs->turns migration broke codex live streaming in copilot chat: the turns bridge's publish shim forwarded only tool-output-stream and silently dropped code-run-event / code-run-permission-request (the latter would deadlock a turn under policy 'ask'). An interim fix persisted every stream event as durable tool_progress, but that wrote each text chunk to the turn file — unsustainable. Final architecture — live and durable paths split: - Live (ephemeral, bypasses the turn runtime): code_agent_run broadcasts each ACP event on a new CodeRunFeed (core DI singleton), forwarded by main over a dedicated codeRun:events channel, buffered per toolCallId in a module-level renderer store and rendered by CodingRunBlock. The buffer survives session switches; nothing is persisted. - Durable (one line per run): when the run settles (success, error, or cancel), code_agent_run publishes a single code-run-events-batch with the whole ordered timeline, consecutive same-role message chunks coalesced (display-lossless — the timeline concatenates them anyway). The turns bridge maps it to tool_progress {kind:'code-run-events'}; turn-view derives the replay timeline from it, so reloads keep history. - Permissions stay durable per-ask (request + resolved marker): the renderer overlay resets on session switch, so an ephemeral-only ask would strand a blocked turn with no card to answer. Pending = requests minus resolutions (handles concurrent asks), cleared on tool result. The legacy code-section path (runs bus per-event) is untouched; its per-event ctx.publish remains and is a no-op under the turns shim. Also: repaired the two code_agent_run tests broken by the earlier cwd existence check (they used a nonexistent /repo), and added coverage for feed broadcast, batch coalescing, partial-batch-on-failure, bridge durability routing, and pending-permission derivation. Co-Authored-By: Claude Fable 5 --- apps/x/apps/main/src/ipc.ts | 20 ++++ apps/x/apps/main/src/main.ts | 2 + .../renderer/src/components/coding-run.tsx | 15 ++- apps/x/apps/renderer/src/lib/code-run-feed.ts | 52 +++++++++ .../src/lib/session-chat/turn-view.test.ts | 82 +++++++++++++ .../src/lib/session-chat/turn-view.ts | 37 ++++++ .../src/application/lib/builtin-tools.test.ts | 110 +++++++++++++++++- .../core/src/application/lib/builtin-tools.ts | 50 +++++++- apps/x/packages/core/src/code-mode/feed.ts | 32 +++++ apps/x/packages/core/src/di/container.ts | 4 + .../turns/bridges/real-tool-registry.test.ts | 53 +++++++++ .../src/turns/bridges/real-tool-registry.ts | 29 +++++ apps/x/packages/shared/src/code-mode.ts | 8 ++ apps/x/packages/shared/src/ipc.ts | 10 +- apps/x/packages/shared/src/runs.ts | 12 ++ 15 files changed, 506 insertions(+), 10 deletions(-) create mode 100644 apps/x/apps/renderer/src/lib/code-run-feed.ts create mode 100644 apps/x/packages/core/src/code-mode/feed.ts diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index a558f4ae..270b1e8f 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -38,6 +38,7 @@ import type { IOAuthRepo } from '@x/core/dist/auth/repo.js'; import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js'; import { ICodeModeConfigRepo } from '@x/core/dist/code-mode/repo.js'; import { CodePermissionRegistry } from '@x/core/dist/code-mode/acp/permission-registry.js'; +import type { CodeRunFeed } from '@x/core/dist/code-mode/feed.js'; import { checkCodeModeAgentStatus } from '@x/core/dist/code-mode/status.js'; import { ensureEngine } from '@x/core/dist/code-mode/acp/engine-provisioner.js'; import type { ICodeProjectsRepo } from '@x/core/dist/code-mode/projects/repo.js'; @@ -703,6 +704,25 @@ export function startSessionsWatcher(): void { sessionsWatcher = sessionBus.subscribe((event) => emitSessionEvent(event)); } +// Ephemeral code-run stream: CodeRunFeed → all renderer windows. A direct +// tool→renderer side-channel that bypasses the turn runtime; the durable +// record is the settle-time code-run-events-batch tool progress. +let codeRunFeedWatcher: (() => void) | null = null; +export function startCodeRunFeedWatcher(): void { + if (codeRunFeedWatcher) { + return; + } + const feed = container.resolve('codeRunFeed'); + codeRunFeedWatcher = feed.subscribe((event) => { + const windows = BrowserWindow.getAllWindows(); + for (const win of windows) { + if (!win.isDestroyed() && win.webContents) { + win.webContents.send('codeRun:events', event); + } + } + }); +} + // The renderer window is created before the session-index startup scan // finishes, so an early sessions:list could observe a partially built index // (the scan runs oldest-first — exactly the newest chats would be missing). diff --git a/apps/x/apps/main/src/main.ts b/apps/x/apps/main/src/main.ts index d8184cf1..4e1d42e0 100644 --- a/apps/x/apps/main/src/main.ts +++ b/apps/x/apps/main/src/main.ts @@ -3,6 +3,7 @@ import path from "node:path"; import { setupIpcHandlers, startRunsWatcher, startSessionsWatcher, markSessionsIndexReady, + startCodeRunFeedWatcher, startChannelsWatcher, startCodeSessionStatusWatcher, startServicesWatcher, @@ -422,6 +423,7 @@ app.whenReady().then(async () => { markSessionsIndexReady(); } startSessionsWatcher(); + startCodeRunFeedWatcher(); // Mobile channels (WhatsApp/Telegram bridge): needs the session index, so // start after initialize(). Failures must never block boot. diff --git a/apps/x/apps/renderer/src/components/coding-run.tsx b/apps/x/apps/renderer/src/components/coding-run.tsx index 2625b65a..62b1043d 100644 --- a/apps/x/apps/renderer/src/components/coding-run.tsx +++ b/apps/x/apps/renderer/src/components/coding-run.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { AlertCircle, CheckCircle2, @@ -19,6 +19,7 @@ import type { CodeRunEvent, PermissionAsk, PermissionDecision } from '@x/shared/ import { cn } from '@/lib/utils' import { Tool, ToolContent, ToolHeader } from '@/components/ai-elements/tool' import { getToolErrorText, toToolState, type ToolCall } from '@/lib/chat-conversation' +import { clearCodeRunBuffer, useCodeRunFeed } from '@/lib/code-run-feed' // ── Timeline reduction ────────────────────────────────────────────── // The raw ACP stream is a flat list of events; collapse it into ordered rows, @@ -283,12 +284,22 @@ export function CodingRunBlock({ (item.input as { agent?: string } | undefined)?.agent const title = AGENT_LABEL[agent ?? ''] ?? 'Coding agent' const error = getToolErrorText(item) + // Timeline source: the durable record (item.codeRunEvents — the settle-time + // batch, or the legacy path's inline accumulation) wins; while it's absent + // the live CodeRunFeed buffer streams the run in real time. + const liveEvents = useCodeRunFeed(item.id) + const durableEvents = item.codeRunEvents + const events = durableEvents?.length ? durableEvents : liveEvents + // Once the durable batch has landed the buffer is redundant — drop it. + useEffect(() => { + if (durableEvents?.length) clearCodeRunBuffer(item.id) + }, [durableEvents?.length, item.id]) return ( <> - + {item.pendingCodePermission && ( diff --git a/apps/x/apps/renderer/src/lib/code-run-feed.ts b/apps/x/apps/renderer/src/lib/code-run-feed.ts new file mode 100644 index 00000000..4ba08894 --- /dev/null +++ b/apps/x/apps/renderer/src/lib/code-run-feed.ts @@ -0,0 +1,52 @@ +import { useSyncExternalStore } from 'react' +import type { CodeRunEvent, CodeRunFeedEvent } from '@x/shared/src/code-mode.js' + +// Renderer half of the ephemeral CodeRunFeed side-channel: buffers the live +// `codeRun:events` broadcast per toolCallId so tool cards can render a +// code_agent_run's activity while it streams. Module-level (not tied to any +// session store) so the buffer survives session switches mid-run. Nothing here +// is persisted — on settle the durable code-run-events-batch in the turn +// record supersedes the buffer, which is then dropped. +const buffers = new Map() +const listeners = new Set<() => void>() +const EMPTY: CodeRunEvent[] = [] +// Backstop so abandoned runs can't grow the map forever (a run's buffer is +// normally dropped explicitly once its durable batch lands). +const MAX_TRACKED_RUNS = 32 + +let attached = false +function ensureAttached(): void { + if (attached) return + attached = true + window.ipc.on('codeRun:events', ((raw: unknown) => { + const { toolCallId, event } = raw as CodeRunFeedEvent + if (!toolCallId || !event) return + if (!buffers.has(toolCallId) && buffers.size >= MAX_TRACKED_RUNS) { + const oldest = buffers.keys().next().value + if (oldest !== undefined) buffers.delete(oldest) + } + // Immutable append: useSyncExternalStore consumers compare by reference. + buffers.set(toolCallId, [...(buffers.get(toolCallId) ?? EMPTY), event]) + for (const listener of [...listeners]) listener() + }) as never) +} + +function subscribe(onChange: () => void): () => void { + ensureAttached() + listeners.add(onChange) + return () => { + listeners.delete(onChange) + } +} + +export function clearCodeRunBuffer(toolCallId: string): void { + if (buffers.delete(toolCallId)) { + for (const listener of [...listeners]) listener() + } +} + +// Live events for one code_agent_run tool call, empty once the durable batch +// takes over (or if the buffer never existed — e.g. after an app reload). +export function useCodeRunFeed(toolCallId: string): CodeRunEvent[] { + return useSyncExternalStore(subscribe, () => buffers.get(toolCallId) ?? EMPTY) +} diff --git a/apps/x/apps/renderer/src/lib/session-chat/turn-view.test.ts b/apps/x/apps/renderer/src/lib/session-chat/turn-view.test.ts index 34df50ba..10671d9e 100644 --- a/apps/x/apps/renderer/src/lib/session-chat/turn-view.test.ts +++ b/apps/x/apps/renderer/src/lib/session-chat/turn-view.test.ts @@ -219,6 +219,88 @@ describe('buildTurnConversation', () => { expect(items.map((i) => i.status)).toEqual(['error', 'completed']) }) + it('derives the code-run timeline from the settle-time batch and asks from request events', () => { + const codeProgress = (progress: unknown): TEvent => ({ + type: 'tool_progress', + turnId: T1, + ts: TS, + toolCallId: 'cr1', + source: 'sync', + progress: progress as never, + }) + const state = reduceTurn([ + created(T1, S1), + requested(T1, 0), + completed(T1, 0, assistantCalls(toolCallPart('cr1', 'code_agent_run', { agent: 'codex' }))), + invocation(T1, 'cr1', 'code_agent_run'), + codeProgress({ + kind: 'code-run-permission-request', + requestId: 'cpr-1', + ask: { toolCallId: 'x', title: 'write file', options: [] }, + }), + codeProgress({ + kind: 'code-run-events', + events: [ + { type: 'message', role: 'agent', text: 'hi' }, + { type: 'tool_call', id: 'x', title: 'write file' }, + ], + }), + ]) + const tool = buildTurnConversation(state).filter(isToolCall)[0] + expect(tool.status).toBe('running') + expect(tool.codeRunEvents?.map((e) => e.type)).toEqual(['message', 'tool_call']) + expect(tool.pendingCodePermission?.requestId).toBe('cpr-1') + }) + + it('clears the pending code permission on the resolved marker and on tool result', () => { + const codeProgress = (toolCallId: string, progress: unknown): TEvent => ({ + type: 'tool_progress', + turnId: T1, + ts: TS, + toolCallId, + source: 'sync', + progress: progress as never, + }) + const ask = { toolCallId: 'x', title: 'write file', options: [] } + // resolved: the durable marker pairs off the request mid-run + const resolvedState = reduceTurn([ + created(T1, S1), + requested(T1, 0), + completed(T1, 0, assistantCalls(toolCallPart('cr1', 'code_agent_run'))), + invocation(T1, 'cr1', 'code_agent_run'), + codeProgress('cr1', { kind: 'code-run-permission-request', requestId: 'cpr-1', ask }), + codeProgress('cr1', { kind: 'code-run-permission-resolved' }), + ]) + const resolved = buildTurnConversation(resolvedState).filter(isToolCall)[0] + expect(resolved.pendingCodePermission).toBeUndefined() + + // a second ask after the first resolution is pending again + const secondAskState = reduceTurn([ + created(T1, S1), + requested(T1, 0), + completed(T1, 0, assistantCalls(toolCallPart('cr1', 'code_agent_run'))), + invocation(T1, 'cr1', 'code_agent_run'), + codeProgress('cr1', { kind: 'code-run-permission-request', requestId: 'cpr-1', ask }), + codeProgress('cr1', { kind: 'code-run-permission-resolved' }), + codeProgress('cr1', { kind: 'code-run-permission-request', requestId: 'cpr-2', ask }), + ]) + const secondAsk = buildTurnConversation(secondAskState).filter(isToolCall)[0] + expect(secondAsk.pendingCodePermission?.requestId).toBe('cpr-2') + + // settled: an unanswered ask must not survive the tool's terminal result + const settledState = reduceTurn([ + created(T1, S1), + requested(T1, 0), + completed(T1, 0, assistantCalls(toolCallPart('cr2', 'code_agent_run'))), + invocation(T1, 'cr2', 'code_agent_run'), + codeProgress('cr2', { kind: 'code-run-permission-request', requestId: 'cpr-2', ask }), + toolResult(T1, 'cr2', 'code_agent_run', { success: false, stopReason: 'cancelled' }), + ]) + const settled = buildTurnConversation(settledState).filter(isToolCall)[0] + expect(settled.pendingCodePermission).toBeUndefined() + expect(settled.status).toBe('completed') + }) + it('renders user attachments and a failed turn as an error item', () => { const input = { role: 'user' as const, diff --git a/apps/x/apps/renderer/src/lib/session-chat/turn-view.ts b/apps/x/apps/renderer/src/lib/session-chat/turn-view.ts index 2e8df218..cc1fc6da 100644 --- a/apps/x/apps/renderer/src/lib/session-chat/turn-view.ts +++ b/apps/x/apps/renderer/src/lib/session-chat/turn-view.ts @@ -13,6 +13,7 @@ import { type TurnState, type TurnStreamEvent, } from '@x/shared/src/turns.js' +import type { CodeRunEvent, PermissionAsk } from '@x/shared/src/code-mode.js' import type { ChatMessage, ConversationItem, @@ -205,6 +206,41 @@ function toolStatus(tc: ToolCallState): ToolCall['status'] { return 'running' } +// code_agent_run's durable trail in tool_progress (see the publish bridge in +// real-tool-registry.ts): ONE settle-time 'code-run-events' batch carrying the +// whole timeline (the live per-event stream travels over the ephemeral +// CodeRunFeed and never reaches turn state), plus per-ask +// 'code-run-permission-request' / 'code-run-permission-resolved' pairs. An ask +// is pending while requests outnumber resolutions and the tool hasn't settled. +function codeRunViewOf( + tc: ToolCallState, +): Pick { + let events: CodeRunEvent[] | undefined + let pending: { requestId: string; ask: PermissionAsk } | null = null + let unresolved = 0 + for (const p of tc.progress) { + const entry = p.progress + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue + const kind = (entry as { kind?: unknown }).kind + if (kind === 'code-run-events') { + const batch = (entry as { events?: unknown }).events + if (Array.isArray(batch)) events = batch as CodeRunEvent[] + } else if (kind === 'code-run-permission-request') { + const { requestId, ask } = entry as { requestId?: unknown; ask?: unknown } + if (typeof requestId === 'string' && ask) { + pending = { requestId, ask: ask as PermissionAsk } + unresolved += 1 + } + } else if (kind === 'code-run-permission-resolved') { + unresolved -= 1 + } + } + return { + ...(events && events.length > 0 ? { codeRunEvents: events } : {}), + ...(pending && unresolved > 0 && !tc.result ? { pendingCodePermission: pending } : {}), + } +} + // One turn's contribution to the conversation: the user input, then per // completed model call its text and tool calls (with live status/results). export function buildTurnConversation(state: TurnState): ConversationItem[] { @@ -258,6 +294,7 @@ export function buildTurnConversation(state: TurnState): ConversationItem[] { ...(tc?.result ? { result: tc.result.result.output as ToolCall['result'] } : {}), status: tc ? toolStatus(tc) : 'running', timestamp: ts(), + ...(tc ? codeRunViewOf(tc) : {}), } satisfies ToolCall) } } diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.test.ts b/apps/x/packages/core/src/application/lib/builtin-tools.test.ts index 1842024c..12ff2fe1 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.test.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.test.ts @@ -1,28 +1,42 @@ +import * as os from "os"; import { afterEach, describe, expect, it, vi } from "vitest"; +import type { CodeRunEvent } from "@x/shared/dist/code-mode.js"; import container from "../../di/container.js"; import { InMemoryAbortRegistry } from "../../runs/abort-registry.js"; -import { BuiltinTools } from "./builtin-tools.js"; +import { BuiltinTools, coalesceCodeRunEvents } from "./builtin-tools.js"; import type { ToolContext } from "./exec-tool.js"; -function context(signal: AbortSignal): ToolContext { +// A real directory: code_agent_run validates the cwd exists before spawning. +const CWD = os.tmpdir(); + +function context(signal: AbortSignal, published: unknown[] = []): ToolContext { return { runId: "turn-1", toolCallId: "tool-1", signal, abortRegistry: new InMemoryAbortRegistry(), - publish: async () => {}, + publish: async (event) => { + published.push(event); + }, codePolicy: "ask", }; } -function mockCodeServices(runPrompt: () => Promise): void { +function mockCodeServices( + runPrompt: (opts: { onEvent: (event: CodeRunEvent) => void }) => Promise, +): { feedEvents: unknown[] } { + const feedEvents: unknown[] = []; vi.spyOn(container, "resolve").mockImplementation(((name: string) => { if (name === "codeModeManager") return { runPrompt }; if (name === "codePermissionRegistry") { return { cancelRun: vi.fn(), request: vi.fn() }; } + if (name === "codeRunFeed") { + return { broadcast: (event: unknown) => feedEvents.push(event) }; + } throw new Error(`Unexpected dependency: ${name}`); }) as typeof container.resolve); + return { feedEvents }; } describe("code_agent_run", () => { @@ -34,11 +48,22 @@ describe("code_agent_run", () => { }); await expect(BuiltinTools.code_agent_run.execute( - { agent: "codex", cwd: "/repo", prompt: "Fix it" }, + { agent: "codex", cwd: CWD, prompt: "Fix it" }, context(new AbortController().signal), )).rejects.toThrow("Coding agent failed: spawn Electron ENOENT"); }); + it("rejects a working directory that does not exist with a clear error", async () => { + mockCodeServices(async () => { + throw new Error("unreachable"); + }); + + await expect(BuiltinTools.code_agent_run.execute( + { agent: "codex", cwd: "/nonexistent-dir-for-test", prompt: "Fix it" }, + context(new AbortController().signal), + )).rejects.toThrow("working directory does not exist"); + }); + it("returns an ordinary cancellation result when the turn was aborted", async () => { const controller = new AbortController(); controller.abort(); @@ -47,8 +72,81 @@ describe("code_agent_run", () => { }); await expect(BuiltinTools.code_agent_run.execute( - { agent: "codex", cwd: "/repo", prompt: "Fix it" }, + { agent: "codex", cwd: CWD, prompt: "Fix it" }, context(controller.signal), )).resolves.toMatchObject({ success: false, stopReason: "cancelled" }); }); + + it("broadcasts events on the feed live and publishes ONE coalesced durable batch", async () => { + const { feedEvents } = mockCodeServices(async ({ onEvent }) => { + onEvent({ type: "message", role: "agent", text: "hel" }); + onEvent({ type: "message", role: "agent", text: "lo" }); + onEvent({ type: "tool_call", id: "x", title: "write file" }); + return { stopReason: "end_turn", sessionId: "s1" }; + }); + const published: unknown[] = []; + + const result = await BuiltinTools.code_agent_run.execute( + { agent: "codex", cwd: CWD, prompt: "Fix it" }, + context(new AbortController().signal, published), + ); + + expect(result).toMatchObject({ success: true, summary: "hello" }); + // Live side-channel: every event, verbatim, keyed by the tool call. + expect(feedEvents).toHaveLength(3); + expect(feedEvents[0]).toMatchObject({ + toolCallId: "tool-1", + event: { type: "message", text: "hel" }, + }); + // Durable: per-event publishes for the legacy bus + exactly one batch, + // with consecutive same-role message chunks coalesced. + const batches = published.filter( + (e) => (e as { type?: string }).type === "code-run-events-batch", + ); + expect(batches).toHaveLength(1); + expect((batches[0] as { events: CodeRunEvent[] }).events).toEqual([ + { type: "message", role: "agent", text: "hello" }, + { type: "tool_call", id: "x", title: "write file" }, + ]); + }); + + it("publishes the partial batch even when the run fails", async () => { + mockCodeServices(async ({ onEvent }) => { + onEvent({ type: "message", role: "agent", text: "started..." }); + throw new Error("engine crashed"); + }); + const published: unknown[] = []; + + await expect(BuiltinTools.code_agent_run.execute( + { agent: "codex", cwd: CWD, prompt: "Fix it" }, + context(new AbortController().signal, published), + )).rejects.toThrow("Coding agent failed"); + const batches = published.filter( + (e) => (e as { type?: string }).type === "code-run-events-batch", + ); + expect(batches).toHaveLength(1); + }); +}); + +describe("coalesceCodeRunEvents", () => { + it("merges consecutive same-role message chunks and keeps everything else in order", () => { + const events: CodeRunEvent[] = [ + { type: "message", role: "agent", text: "a" }, + { type: "message", role: "agent", text: "b" }, + { type: "tool_call", id: "t1", title: "run" }, + { type: "message", role: "agent", text: "c" }, + { type: "message", role: "user", text: "d" }, + { type: "message", role: "user", text: "e" }, + ]; + expect(coalesceCodeRunEvents(events)).toEqual([ + { type: "message", role: "agent", text: "ab" }, + { type: "tool_call", id: "t1", title: "run" }, + { type: "message", role: "agent", text: "c" }, + { type: "message", role: "user", text: "de" }, + ]); + }); + + it("returns an empty list unchanged", () => { + expect(coalesceCodeRunEvents([])).toEqual([]); + }); }); diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts index bf40ab1b..a7921852 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -20,7 +20,8 @@ import { BackgroundTaskSchema, TriggersSchema } from "@x/shared/dist/background- import type { CodeModeManager } from "../../code-mode/acp/manager.js"; import type { CodePermissionRegistry } from "../../code-mode/acp/permission-registry.js"; import { ICodeModeConfigRepo } from "../../code-mode/repo.js"; -import type { ApprovalPolicy } from "@x/shared/dist/code-mode.js"; +import type { ApprovalPolicy, CodeRunEvent as CodeRunEventType } from "@x/shared/dist/code-mode.js"; +import type { CodeRunFeed } from "../../code-mode/feed.js"; import type { ICodeProjectsRepo } from "../../code-mode/projects/repo.js"; import * as gitService from "../../code-mode/git/service.js"; import { listImportantThreads, searchThreads } from "../../knowledge/sync_gmail.js"; @@ -68,6 +69,27 @@ function expandHome(p: string): string { return t; } +// Shrink a code-run timeline for durable storage: consecutive same-role message +// chunks merge into one event. Display-lossless — the timeline renderer +// concatenates consecutive messages anyway (CodingRunTimeline) — and typically +// collapses the ~90% of a run's events that are per-token text deltas. +// Everything else (tool calls/updates, plans, permissions) is kept verbatim in +// order: updates are id-keyed transitions and must not be merged. +export function coalesceCodeRunEvents(events: CodeRunEventType[]): CodeRunEventType[] { + const out: CodeRunEventType[] = []; + for (const event of events) { + const last = out[out.length - 1]; + if ( + event.type === 'message' && last?.type === 'message' && last.role === event.role + ) { + out[out.length - 1] = { ...last, text: last.text + event.text }; + } else { + out.push(event); + } + } + return out; +} + async function resolveCodeProject(dirPath: string): Promise< { ok: true; projectId: string; path: string; warning?: string } | { ok: false; error: string } > { @@ -916,6 +938,10 @@ export const BuiltinTools: z.infer = { let finalText = ''; const changedFiles = new Set(); + // The full ordered timeline, published ONCE as a durable batch when the + // run settles (see finally). The per-event copies below are ephemeral. + const collected: CodeRunEventType[] = []; + const feed = container.resolve('codeRunFeed'); try { const result = await manager.runPrompt({ runId: ctx.runId, @@ -927,6 +953,11 @@ export const BuiltinTools: z.infer = { onEvent: (event) => { if (event.type === 'message' && event.role === 'agent') finalText += event.text; if (event.type === 'tool_call_update') for (const f of event.diffs) changedFiles.add(f); + collected.push(event); + // Live rendering, two transports: the CodeRunFeed side-channel + // (turns-runtime chats — the runtime never sees this traffic) + // and the legacy runs bus (code-section tabs). Both ephemeral. + feed.broadcast({ toolCallId: ctx.toolCallId, event }); void ctx.publish({ runId: ctx.runId, type: 'code-run-event', @@ -969,6 +1000,23 @@ export const BuiltinTools: z.infer = { throw new Error(`Coding agent failed: ${error instanceof Error ? error.message : String(error)}`); } finally { ctx.signal.removeEventListener('abort', onAbort); + // Durable record for replay-on-reload — one event with the whole + // (coalesced) timeline, on every settle path including errors and + // cancellation, so partial runs keep their history too. + if (collected.length > 0) { + await ctx.publish({ + runId: ctx.runId, + type: 'code-run-events-batch', + toolCallId: ctx.toolCallId, + events: coalesceCodeRunEvents(collected), + subflow: [], + }).catch((e: unknown) => { + // History is best-effort (rethrowing here would mask the run's + // real outcome) — but a lost timeline must leave a trail, since + // this batch is the only durable record of the run's activity. + console.warn(`[code_agent_run] failed to persist code-run timeline: ${e instanceof Error ? e.message : String(e)}`); + }); + } } }, }, diff --git a/apps/x/packages/core/src/code-mode/feed.ts b/apps/x/packages/core/src/code-mode/feed.ts new file mode 100644 index 00000000..f63b05b8 --- /dev/null +++ b/apps/x/packages/core/src/code-mode/feed.ts @@ -0,0 +1,32 @@ +import type { CodeRunFeedEvent } from '@x/shared/dist/code-mode.js'; + +// Ephemeral side-channel for code_agent_run's live ACP stream — a direct +// tool-implementation → renderer contract that deliberately bypasses the turn +// runtime. The stream is chatty (per-chunk agent messages, tool status +// updates) and only ever renders inside one tool card, so persisting each +// event as durable turn progress bloats the turn log for no benefit. Instead: +// - live: the tool broadcasts here; main forwards over `codeRun:events` +// (see apps/main ipc.ts) and the renderer buffers per toolCallId. +// - durable: ONE code-run-events-batch is published when the run settles, +// so reloads replay the full timeline from the turn record. +// Fire-and-forget: no subscribers ⇒ events vanish, which is the point. +export class CodeRunFeed { + private readonly listeners = new Set<(event: CodeRunFeedEvent) => void>(); + + broadcast(event: CodeRunFeedEvent): void { + for (const listener of [...this.listeners]) { + try { + listener(event); + } catch { + // A broken subscriber must not stall the coding turn. + } + } + } + + subscribe(listener: (event: CodeRunFeedEvent) => void): () => void { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } +} diff --git a/apps/x/packages/core/src/di/container.ts b/apps/x/packages/core/src/di/container.ts index eefdaa5f..77bda6bb 100644 --- a/apps/x/packages/core/src/di/container.ts +++ b/apps/x/packages/core/src/di/container.ts @@ -21,6 +21,7 @@ import { FSSlackConfigRepo, ISlackConfigRepo } from "../slack/repo.js"; import { FSChannelsConfigRepo, IChannelsConfigRepo } from "../channels/repo.js"; import { CodeModeManager } from "../code-mode/acp/manager.js"; import { CodePermissionRegistry } from "../code-mode/acp/permission-registry.js"; +import { CodeRunFeed } from "../code-mode/feed.js"; import { FSCodeProjectsRepo, ICodeProjectsRepo } from "../code-mode/projects/repo.js"; import { FSCodeSessionsRepo, ICodeSessionsRepo } from "../code-mode/sessions/repo.js"; import { CodeSessionService } from "../code-mode/sessions/service.js"; @@ -98,6 +99,9 @@ container.register({ // session/load); the registry brokers mid-run approvals. codeModeManager: asClass(CodeModeManager).singleton(), codePermissionRegistry: asClass(CodePermissionRegistry).singleton(), + // Ephemeral live stream for code_agent_run (renderer side-channel; the + // durable record is the settle-time code-run-events-batch). + codeRunFeed: asClass(CodeRunFeed).singleton(), // Code section: project registry, session metadata, the direct-drive // session service, and the live status tracker. diff --git a/apps/x/packages/core/src/turns/bridges/real-tool-registry.test.ts b/apps/x/packages/core/src/turns/bridges/real-tool-registry.test.ts index 4fb6113e..4ccb15a6 100644 --- a/apps/x/packages/core/src/turns/bridges/real-tool-registry.test.ts +++ b/apps/x/packages/core/src/turns/bridges/real-tool-registry.test.ts @@ -125,6 +125,59 @@ describe("RealToolRegistry", () => { expect(ctx.progress).toEqual([{ kind: "tool-output", chunk: "chunk-1" }]); }); + it("keeps code-run durability to permission asks/resolutions and the settle batch", async () => { + const chunk = { type: "message", role: "agent", text: "hi" } as const; + const resolution = { + type: "permission", + ask: { toolCallId: "x", title: "write file", options: [] }, + decision: "allow_once", + auto: false, + } as const; + const ask = { toolCallId: "x", title: "write file", options: [] }; + const { registry } = makeRegistry(async ({ ctx }) => { + // Chatty stream events are ephemeral (CodeRunFeed) — NOT progress. + await ctx.publish({ + runId: "turn-1", + type: "code-run-event", + toolCallId: "tc-1", + event: chunk, + subflow: [], + }); + await ctx.publish({ + runId: "turn-1", + type: "code-run-permission-request", + toolCallId: "tc-1", + requestId: "cpr-1", + ask, + subflow: [], + }); + // A permission resolution in the stream leaves a durable marker. + await ctx.publish({ + runId: "turn-1", + type: "code-run-event", + toolCallId: "tc-1", + event: resolution, + subflow: [], + }); + await ctx.publish({ + runId: "turn-1", + type: "code-run-events-batch", + toolCallId: "tc-1", + events: [chunk, resolution], + subflow: [], + }); + return "done"; + }); + const tool = (await registry.resolve(descriptor())) as SyncRuntimeTool; + const ctx = makeCtx(); + await tool.execute({}, ctx); + expect(ctx.progress).toEqual([ + { kind: "code-run-permission-request", requestId: "cpr-1", ask }, + { kind: "code-run-permission-resolved" }, + { kind: "code-run-events", events: [chunk, resolution] }, + ]); + }); + it("wires the abort signal to the registry's force-kill path", async () => { const controller = new AbortController(); const { registry, abortRegistry } = makeRegistry(async () => { diff --git a/apps/x/packages/core/src/turns/bridges/real-tool-registry.ts b/apps/x/packages/core/src/turns/bridges/real-tool-registry.ts index 697978ab..e891ccb9 100644 --- a/apps/x/packages/core/src/turns/bridges/real-tool-registry.ts +++ b/apps/x/packages/core/src/turns/bridges/real-tool-registry.ts @@ -108,6 +108,35 @@ export class RealToolRegistry implements IToolRegistry { kind: "tool-output", chunk: event.output, }); + } else if (event.type === "code-run-event") { + // The live per-event stream travels over the + // ephemeral CodeRunFeed (never persisted) — but a + // permission RESOLUTION is durably marked so an + // answered ask never resurrects as a pending card + // after a reload or session switch. + if (event.event.type === "permission") { + await ctx.reportProgress({ + kind: "code-run-permission-resolved", + }); + } + } else if (event.type === "code-run-permission-request") { + // Durable (not feed-ephemeral): the coding turn is + // BLOCKED until the user answers via + // codeRun:resolvePermission, so the ask must survive + // session switches — dropping it would hang the turn + // under policy 'ask' with no card to answer. + await ctx.reportProgress({ + kind: "code-run-permission-request", + requestId: event.requestId, + ask: toJsonValue(event.ask), + }); + } else if (event.type === "code-run-events-batch") { + // Settle-time durable record of the whole timeline — + // what reloads replay instead of the live feed. + await ctx.reportProgress({ + kind: "code-run-events", + events: toJsonValue(event.events), + }); } }, }, diff --git a/apps/x/packages/shared/src/code-mode.ts b/apps/x/packages/shared/src/code-mode.ts index 8a85f62f..4a8429e8 100644 --- a/apps/x/packages/shared/src/code-mode.ts +++ b/apps/x/packages/shared/src/code-mode.ts @@ -73,3 +73,11 @@ export const RunPromptResult = z.object({ sessionId: z.string(), }); export type RunPromptResult = z.infer; + +// One item on the ephemeral CodeRunFeed (`codeRun:events` broadcast): a live +// code-run event tagged with the tool call it belongs to. Fire-and-forget — +// the durable record is the code-run-events-batch written when the run settles. +export type CodeRunFeedEvent = { + toolCallId: string; + event: CodeRunEvent; +}; diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 04651dc8..4958a03a 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -21,7 +21,7 @@ import { ZListToolkitsResponse } from './composio.js'; import { BrowserStateSchema, HttpAuthRequestSchema } from './browser-control.js'; import { BillingInfoSchema } from './billing.js'; import { EmailBlockSchema, GmailThreadSchema } from './blocks.js'; -import { PermissionDecision, ApprovalPolicy, CodingAgent } from './code-mode.js'; +import { PermissionDecision, ApprovalPolicy, CodingAgent, type CodeRunFeedEvent } from './code-mode.js'; import { NotificationSettingsSchema } from './notification-settings.js'; import { CodeProject, CodeSession, CodeSessionMode, CodeSessionStatus, GitRepoInfo, GitStatusFile, CodeAgentModelOptions } from './code-sessions.js'; import { ChannelsConfig, ChannelsStatus } from './channels.js'; @@ -450,6 +450,14 @@ const ipcSchemas = { req: z.null(), res: z.null(), }, + // Ephemeral code-run stream (CodeRunFeed): per-event broadcast of a + // code_agent_run's live ACP activity, keyed by toolCallId. Never persisted — + // the durable record is the code-run-events-batch tool progress written when + // the run settles. Typed via z.custom like the other broadcast feeds. + 'codeRun:events': { + req: z.custom(), + res: z.null(), + }, // ── New runtime: sessions + turns (session-design.md) ──────────────────── // Turn-mutating calls return quickly; the renderer follows progress through // the sessions:events feed and the shared reduceTurn reducer. diff --git a/apps/x/packages/shared/src/runs.ts b/apps/x/packages/shared/src/runs.ts index 571bf95a..e5771c2e 100644 --- a/apps/x/packages/shared/src/runs.ts +++ b/apps/x/packages/shared/src/runs.ts @@ -130,6 +130,17 @@ export const CodeRunPermissionRequestEvent = BaseRunEvent.extend({ ask: PermissionAsk, }); +// The complete, ordered code-run timeline, published ONCE when the coding turn +// settles (consecutive agent message chunks coalesced — display-lossless, the +// timeline concatenates them anyway). This is the durable record; the live +// per-event stream travels over the ephemeral CodeRunFeed (`codeRun:events`) +// and is never persisted. +export const CodeRunEventsBatchEvent = BaseRunEvent.extend({ + type: z.literal("code-run-events-batch"), + toolCallId: z.string(), + events: z.array(CodeRunEventSchema), +}); + export const ToolPermissionAutoDecisionEvent = BaseRunEvent.extend({ type: z.literal("tool-permission-auto-decision"), toolCallId: z.string(), @@ -165,6 +176,7 @@ export const RunEvent = z.union([ ToolPermissionResponseEvent, CodeRunStreamEvent, CodeRunPermissionRequestEvent, + CodeRunEventsBatchEvent, ToolPermissionAutoDecisionEvent, RunErrorEvent, RunStoppedEvent,