mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-18 21:21:11 +02:00
Merge pull request #674 from rowboatlabs/code-mode-fixes
Code mode fixes
This commit is contained in:
commit
1050614295
24 changed files with 686 additions and 259 deletions
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
apps/x/packages/core/src/code-mode/feed.ts
Normal file
32
apps/x/packages/core/src/code-mode/feed.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue