diff --git a/CHANGELOG.md b/CHANGELOG.md index a38e52207..77453de78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ tagged release also ships native binaries for Linux, macOS, and Windows. - Fix duration day/minute boundaries, file directory and extension labels, and payload-size rounding. Unknown payload sizes now display clearly. Background queues process undefined items, reject invalid concurrency, and release timeout timers when an operation fails. +- Loading or forking a session no longer fails outright when one `todowrite` + entry in its history is not JSON; the rest of the history replays. + - Expand account, privacy, and usage documentation; clarify prepaid Ace access, fixed optional reloads, and current account requirements. ### Changed diff --git a/backend/cli/src/acp/agent.ts b/backend/cli/src/acp/agent.ts index a9b1dccd0..4d14f9c04 100644 --- a/backend/cli/src/acp/agent.ts +++ b/backend/cli/src/acp/agent.ts @@ -91,6 +91,22 @@ export namespace ACP { return getNewContent(content, diff) } + function parseTodoOutput(output: string) { + let raw: unknown + try { + raw = JSON.parse(output) + } catch (error) { + log.error("could not parse todowrite output as JSON", { error }) + return undefined + } + const parsed = z.array(Todo.Info).safeParse(raw) + if (!parsed.success) { + log.error("failed to parse todo output", { error: parsed.error }) + return undefined + } + return parsed.data + } + export async function init({ sdk: _sdk }: { sdk: OpenScienceClient }) { return { create: (connection: AgentSideConnection, fullConfig: ACPConfig) => { @@ -800,14 +816,14 @@ export namespace ACP { } if (part.tool === "todowrite") { - const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output)) - if (parsedTodos.success) { + const todos = parseTodoOutput(part.state.output) + if (todos) { await this.connection .sessionUpdate({ sessionId, update: { sessionUpdate: "plan", - entries: parsedTodos.data.map((todo) => { + entries: todos.map((todo) => { const status: PlanEntry["status"] = todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"]) return { @@ -821,8 +837,6 @@ export namespace ACP { .catch((err) => { log.error("failed to send session update for todo", { error: err }) }) - } else { - log.error("failed to parse todo output", { error: parsedTodos.error }) } } diff --git a/backend/cli/test/acp/event-subscription.test.ts b/backend/cli/test/acp/event-subscription.test.ts index 6e7a90788..13657e2ca 100644 --- a/backend/cli/test/acp/event-subscription.test.ts +++ b/backend/cli/test/acp/event-subscription.test.ts @@ -368,6 +368,46 @@ describe("acp.agent event subscription", () => { }) }) + test("replays a session whose todowrite history is not valid JSON", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const { agent, updates, stop, sdk } = createFakeAgent() + const cwd = "/tmp/openscience-acp-test" + + const sessionID = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId) + + sdk.session.messages = async () => ({ + data: [ + { + info: { role: "assistant", sessionID }, + parts: [ + { + type: "tool", + callID: "call_replay", + tool: "todowrite", + state: { + status: "completed", + input: {}, + output: "not json", + title: "todowrite", + metadata: {}, + }, + }, + ], + }, + ], + }) + + await agent.loadSession({ sessionId: sessionID, cwd, mcpServers: [] } as any) + + expect(updates.get(sessionID)).toContain("tool_call_update") + stop() + }, + }) + }) + test("permission.asked events are handled and replied", async () => { await using tmp = await tmpdir() await Instance.provide({