Skip to content

Commit 867de85

Browse files
authored
Revert "Don't get out of sync when background task creates new init/result (#386)" (#398)
1 parent af11cdb commit 867de85

2 files changed

Lines changed: 17 additions & 123 deletions

File tree

src/acp-agent.ts

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -481,23 +481,17 @@ export class ClaudeAcpAgent implements Agent {
481481

482482
const userMessage = promptToClaude(params);
483483

484-
const promptUuid = randomUUID();
485-
userMessage.uuid = promptUuid;
486-
487-
let promptReplayed = false;
488-
489484
if (session.promptRunning) {
485+
const uuid = randomUUID();
486+
userMessage.uuid = uuid;
490487
session.input.push(userMessage);
491488
const order = session.nextPendingOrder++;
492489
const cancelled = await new Promise<boolean>((resolve) => {
493-
session.pendingMessages.set(promptUuid, { resolve, order });
490+
session.pendingMessages.set(uuid, { resolve, order });
494491
});
495492
if (cancelled) {
496493
return { stopReason: "cancelled" };
497494
}
498-
// The replay resolved the promise, mark in this loop too,
499-
// so we don't treat the next result as a background task's result.
500-
promptReplayed = true;
501495
} else {
502496
session.input.push(userMessage);
503497
}
@@ -572,6 +566,10 @@ export class ClaudeAcpAgent implements Agent {
572566
}
573567
break;
574568
case "result": {
569+
if (session.cancelled) {
570+
return { stopReason: "cancelled" };
571+
}
572+
575573
// Accumulate usage from this result
576574
session.accumulatedUsage.inputTokens += message.usage.input_tokens;
577575
session.accumulatedUsage.outputTokens += message.usage.output_tokens;
@@ -599,18 +597,6 @@ export class ClaudeAcpAgent implements Agent {
599597
});
600598
}
601599

602-
if (!promptReplayed) {
603-
// This result is from a background task that finished after
604-
// the previous prompt loop ended. Consume it and continue
605-
// waiting for our own prompt's result.
606-
this.logger.log(`Session ${params.sessionId}: consuming background task result`);
607-
break;
608-
}
609-
610-
if (session.cancelled) {
611-
return { stopReason: "cancelled" };
612-
}
613-
614600
// Build the usage response
615601
const usage: PromptResponse["usage"] = {
616602
inputTokens: session.accumulatedUsage.inputTokens,
@@ -686,14 +672,8 @@ export class ClaudeAcpAgent implements Agent {
686672
break;
687673
}
688674

689-
// Check for prompt replay
675+
// Check for queued prompt replay
690676
if (message.type === "user" && "uuid" in message && message.uuid) {
691-
if (message.uuid === promptUuid) {
692-
// Our own prompt was replayed back — we're now processing
693-
// our prompt's response (not a background task's).
694-
promptReplayed = true;
695-
break;
696-
}
697677
const pending = session.pendingMessages.get(message.uuid as string);
698678
if (pending) {
699679
pending.resolve(false);

src/tests/acp-agent.test.ts

Lines changed: 9 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration"
228228
sessionId: newSessionResponse.sessionId,
229229
});
230230

231-
expect(client.takeReceivedText()).toBe("Error: No messages to compact");
231+
expect(client.takeReceivedText()).toBe("Compacting...\n\nCompacting completed.");
232232

233233
// Send something
234234
await connection.prompt({
@@ -249,7 +249,7 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration"
249249
sessionId: newSessionResponse.sessionId,
250250
});
251251

252-
expect(client.takeReceivedText()).toContain("");
252+
expect(client.takeReceivedText()).toContain("Compacting...\n\nCompacting completed.");
253253
}, 30000);
254254
});
255255

@@ -1305,27 +1305,15 @@ describe("stop reason propagation", () => {
13051305
};
13061306
}
13071307

1308+
function* messageGenerator(messages: any[]) {
1309+
yield* messages;
1310+
}
1311+
13081312
function injectSession(agent: ClaudeAcpAgent, messages: any[]) {
1309-
const input = new Pushable<any>();
1310-
async function* messageGenerator() {
1311-
// Wait for the prompt to push its user message so we can replay it
1312-
const iter = input[Symbol.asyncIterator]();
1313-
const { value: userMessage, done } = await iter.next();
1314-
if (!done && userMessage) {
1315-
yield {
1316-
type: "user",
1317-
message: userMessage.message,
1318-
parent_tool_use_id: null,
1319-
uuid: userMessage.uuid,
1320-
session_id: "test-session",
1321-
isReplay: true,
1322-
};
1323-
}
1324-
yield* messages;
1325-
}
1313+
const gen = messageGenerator(messages);
13261314
agent.sessions["test-session"] = {
1327-
query: messageGenerator() as any,
1328-
input,
1315+
query: gen as any,
1316+
input: new Pushable(),
13291317
cancelled: false,
13301318
cwd: "/test",
13311319
permissionMode: "default",
@@ -1409,80 +1397,6 @@ describe("stop reason propagation", () => {
14091397
expect(response.stopReason).toBe("end_turn");
14101398
});
14111399

1412-
it("should consume background task results and return the prompt's own result", async () => {
1413-
const agent = createMockAgent();
1414-
const input = new Pushable<any>();
1415-
1416-
const backgroundTaskResult = createResultMessage({
1417-
subtype: "success",
1418-
stop_reason: null,
1419-
is_error: false,
1420-
});
1421-
// Background task used some tokens
1422-
backgroundTaskResult.usage.input_tokens = 100;
1423-
backgroundTaskResult.usage.output_tokens = 50;
1424-
1425-
const promptResult = createResultMessage({
1426-
subtype: "success",
1427-
stop_reason: null,
1428-
is_error: false,
1429-
});
1430-
1431-
async function* messageGenerator() {
1432-
// Background task init + result arrive before our prompt's replay
1433-
yield { type: "system", subtype: "init", session_id: "test-session" };
1434-
yield backgroundTaskResult;
1435-
1436-
// Now the prompt's user message replay arrives
1437-
const iter = input[Symbol.asyncIterator]();
1438-
const { value: userMessage } = await iter.next();
1439-
yield {
1440-
type: "user",
1441-
message: userMessage.message,
1442-
parent_tool_use_id: null,
1443-
uuid: userMessage.uuid,
1444-
session_id: "test-session",
1445-
isReplay: true,
1446-
};
1447-
1448-
// Then the prompt's own result
1449-
yield promptResult;
1450-
}
1451-
1452-
agent.sessions["test-session"] = {
1453-
query: messageGenerator() as any,
1454-
input,
1455-
cwd: "/tmp/test",
1456-
cancelled: false,
1457-
permissionMode: "default",
1458-
settingsManager: {} as any,
1459-
accumulatedUsage: {
1460-
inputTokens: 0,
1461-
outputTokens: 0,
1462-
cachedReadTokens: 0,
1463-
cachedWriteTokens: 0,
1464-
},
1465-
configOptions: [],
1466-
promptRunning: false,
1467-
pendingMessages: new Map(),
1468-
nextPendingOrder: 0,
1469-
};
1470-
1471-
const response = await agent.prompt({
1472-
sessionId: "test-session",
1473-
prompt: [{ type: "text", text: "test" }],
1474-
});
1475-
1476-
expect(response.stopReason).toBe("end_turn");
1477-
// Usage should include both background task and prompt result tokens
1478-
expect(response.usage?.inputTokens).toBe(
1479-
backgroundTaskResult.usage.input_tokens + promptResult.usage.input_tokens,
1480-
);
1481-
expect(response.usage?.outputTokens).toBe(
1482-
backgroundTaskResult.usage.output_tokens + promptResult.usage.output_tokens,
1483-
);
1484-
});
1485-
14861400
it("should throw internal error for success with is_error true and no max_tokens", async () => {
14871401
const agent = createMockAgent();
14881402
injectSession(agent, [

0 commit comments

Comments
 (0)