Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 19 additions & 5 deletions backend/cli/src/acp/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 })
}
}

Expand Down
40 changes: 40 additions & 0 deletions backend/cli/test/acp/event-subscription.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading