mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
fix: keep the usage-limit error when it arrives after compaction (#1377)
Fixes #1370 After a compaction in the same turn, the synthetic usage-limit frame is dropped by the stale-frame suppression before its failure details are read. The prompt then ends with a generic internal error instead of the quota error, so clients can't show the limit UI. The suppression branch now records the usage-limit title and error before it skips the frame. Only recognized usage-limit frames are read there, so a stale replayed frame still can't overwrite the failure of the current turn. Added a test for a usage-limit frame after a completed compaction, for both the legacy `errorKind` and the typed session failure, plus a stale frame arriving after it. It fails without the change. `npm run check`, `npm run build` and `npm run test:run` pass.
This commit is contained in:
@@ -8047,6 +8047,13 @@ export class ClaudeAcpAgent {
|
||||
message.message.model === "<synthetic>" &&
|
||||
compaction.hasDeliveredOutput
|
||||
) {
|
||||
if (isSyntheticUsageLimitMessage(message.message)) {
|
||||
lastAssistantWasUsageLimit = true;
|
||||
lastAssistantFailureTitle = assistantMessageText(message.message);
|
||||
if (message.error) {
|
||||
lastAssistantError = message.error;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -12631,6 +12631,56 @@ describe("stop reason propagation", () => {
|
||||
expect((err as { data: unknown }).data).toEqual({ errorKind: "rate_limit" });
|
||||
});
|
||||
|
||||
it.each(
|
||||
[false, true].flatMap((typedFailures) =>
|
||||
[undefined, "authentication_failed" as const].map((staleError) => ({
|
||||
typedFailures,
|
||||
staleError,
|
||||
})),
|
||||
),
|
||||
)(
|
||||
"preserves a usage-limit failure after compaction (typed: $typedFailures, stale error: $staleError)",
|
||||
async ({ typedFailures, staleError }) => {
|
||||
const agent = createMockAgent();
|
||||
if (typedFailures) await initializeClient(agent, airSessionFailureCapabilities);
|
||||
const staleFrame = createAssistantError(staleError, "stale compaction output");
|
||||
staleFrame.message.model = "<synthetic>";
|
||||
injectSession(agent, [
|
||||
{
|
||||
type: "system",
|
||||
subtype: "status",
|
||||
status: null,
|
||||
compact_result: "success",
|
||||
uuid: "compact-completed",
|
||||
session_id: "test-session",
|
||||
},
|
||||
createUsageLimitAssistantError(),
|
||||
staleFrame,
|
||||
createResultMessage({
|
||||
subtype: "success",
|
||||
stop_reason: "end_turn",
|
||||
is_error: true,
|
||||
result: "private provider detail",
|
||||
}),
|
||||
]);
|
||||
|
||||
const prompt = agent.prompt({
|
||||
sessionId: "test-session",
|
||||
prompt: [{ type: "text", text: "test" }],
|
||||
});
|
||||
if (typedFailures) {
|
||||
expect(sessionFailureFromResponse(await prompt)).toMatchObject({
|
||||
category: "limit",
|
||||
actions: [],
|
||||
title:
|
||||
"You've hit your individual spend limit · run /usage-credits to ask your admin for a higher limit",
|
||||
});
|
||||
} else {
|
||||
await expect(prompt).rejects.toMatchObject({ data: { errorKind: "rate_limit" } });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("omits errorKind data when no SDKAssistantMessage.error was observed", async () => {
|
||||
const agent = createMockAgent();
|
||||
injectSession(agent, [
|
||||
|
||||
Reference in New Issue
Block a user