Merge pull request #674 from rowboatlabs/code-mode-fixes

Code mode fixes
This commit is contained in:
Ramnique Singh 2026-07-06 14:06:41 +05:30 committed by GitHub
commit 1050614295
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 686 additions and 259 deletions

View file

@ -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<never>): void {
function mockCodeServices(
runPrompt: (opts: { onEvent: (event: CodeRunEvent) => void }) => Promise<unknown>,
): { 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([]);
});
});

View file

@ -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 }
> {
@ -876,8 +898,19 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
// 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 <command> 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>('codeModeManager');
const registry = container.resolve<CodePermissionRegistry>('codePermissionRegistry');
@ -905,6 +938,10 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
let finalText = '';
const changedFiles = new Set<string>();
// 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>('codeRunFeed');
try {
const result = await manager.runPrompt({
runId: ctx.runId,
@ -916,6 +953,11 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
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',
@ -958,6 +1000,23 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
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)}`);
});
}
}
},
},

View file

@ -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<void> {
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.

View file

@ -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=="
}
}
}

View file

@ -89,13 +89,16 @@ function locateExecutable(agent: CodingAgent, root: string): string | null {
}
return null;
}
// codex: vendor/<target-triple>/codex/codex[.exe]
// codex ships its native binary under vendor/<target-triple>/. 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);
}
}
}
}

View file

@ -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);
};
}
}

View file

@ -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.

View file

@ -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 () => {

View file

@ -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),
});
}
},
},