mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
fix: replay a session with a Write on the experimental ACP v2 surface (#1238)
Fixes a bug in #1237 on the opt-in ACP v2 surface. Nothing changes on ACP v1. ## Problem A v2 `session/resume` with `replayFrom` failed with an internal error on any session that contains a Write: - A replay has no approval preview and no PostToolUse hook, so the Write reporter builds a diff from the input for every client on replay. - That's a v1 diff. It even claims a creation, since the input doesn't say whether the file existed. - The v2 surface rejects v1 diffs (#1232), so the replay threw and the resume failed. `session-load-replay` has an Edit but no Write, so the scenario suite didn't catch it. ## Change A v2 client gets no diff from a Write's input, live or replayed: a v2 diff names the operation (`add` or `modify`), which the input doesn't tell. Replayed Writes and Edits show no diff on v2 yet; building them needs the tool's structured result, which the SDK's transcript reader leaves out (see the follow-up discussion). ## Verification A new test replays a transcript with a Write through a v2 `session/resume`: it failed with "Internal error" before the change and passes after. The v1 golden files are unchanged. Build, lint and all 2386 tests pass.
This commit is contained in:
@@ -740,6 +740,49 @@ describe("ACP v2 prompts", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("replays a Write without the v1 diff that v2 cannot take", async () => {
|
||||
const client = v2Client();
|
||||
await client.app.connectWith(connectRouter(), async (agent) => {
|
||||
await initializeV2(agent);
|
||||
const { sessionId } = await agent.request(v2.methods.agent.session.new, { cwd });
|
||||
const file = path.join(cwd, "a.ts");
|
||||
vi.mocked(getSessionMessages).mockResolvedValueOnce([
|
||||
transcriptEntry("assistant", randomUUID(), {
|
||||
id: "msg_write",
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_w",
|
||||
name: "Write",
|
||||
input: { file_path: file, content: "x\n" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
transcriptEntry("user", randomUUID(), {
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_w",
|
||||
content: `File created successfully at: ${file}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
] as unknown as Awaited<ReturnType<typeof getSessionMessages>>);
|
||||
await agent.request(v2.methods.agent.session.resume, {
|
||||
sessionId,
|
||||
cwd,
|
||||
replayFrom: { type: "start" },
|
||||
});
|
||||
await client.authUpdate(1);
|
||||
});
|
||||
|
||||
expect(client.updates("tool_call_update")).toContainEqual(
|
||||
expect.objectContaining({ toolCallId: "toolu_w", title: "Write a.ts", content: [] }),
|
||||
);
|
||||
});
|
||||
|
||||
it("replays the history before it answers session/resume, clearing each message first", async () => {
|
||||
const echoed = scriptTurns(
|
||||
async function* (options) {
|
||||
|
||||
@@ -37,8 +37,10 @@ export class WriteReporter implements ToolReporter {
|
||||
// whether the file exists, so the live tool call shows no diff: a diff
|
||||
// without the old text would claim a creation. The adapter reads the
|
||||
// file only for the preview. A replay has no preview and no hook, so it
|
||||
// keeps the diff.
|
||||
if (!(capabilities.diffPatch || capabilities.v2) || replay) {
|
||||
// keeps the diff, except for a v2 client: a v2 diff names the operation,
|
||||
// which the input does not tell.
|
||||
const inputDiff = capabilities.v2 ? false : !capabilities.diffPatch || replay;
|
||||
if (inputDiff) {
|
||||
facts.change = [
|
||||
{
|
||||
type: "diff",
|
||||
|
||||
Reference in New Issue
Block a user