Merge pull request #225 from multica-ai/fix/format-error-recovery

fix(agent): auto-recover from persistent 400 format errors
This commit is contained in:
Bohan Jiang 2026-02-24 13:33:28 +08:00 committed by GitHub
commit 61bbf8fa6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 50 additions and 8 deletions

View file

@ -5,35 +5,43 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./agent": {
"types": "./dist/agent/index.d.ts",
"import": "./dist/agent/index.js"
"import": "./dist/agent/index.js",
"default": "./dist/agent/index.js"
},
"./hub": {
"types": "./dist/hub/index.d.ts",
"import": "./dist/hub/index.js"
"import": "./dist/hub/index.js",
"default": "./dist/hub/index.js"
},
"./channels": {
"types": "./dist/channels/index.d.ts",
"import": "./dist/channels/index.js"
"import": "./dist/channels/index.js",
"default": "./dist/channels/index.js"
},
"./cron": {
"types": "./dist/cron/index.d.ts",
"import": "./dist/cron/index.js"
"import": "./dist/cron/index.js",
"default": "./dist/cron/index.js"
},
"./heartbeat": {
"types": "./dist/heartbeat/index.d.ts",
"import": "./dist/heartbeat/index.js"
"import": "./dist/heartbeat/index.js",
"default": "./dist/heartbeat/index.js"
},
"./media": {
"types": "./dist/media/index.d.ts",
"import": "./dist/media/index.js"
"import": "./dist/media/index.js",
"default": "./dist/media/index.js"
},
"./client": {
"types": "./dist/client/index.d.ts",
"import": "./dist/client/index.js"
"import": "./dist/client/index.js",
"default": "./dist/client/index.js"
}
},
"main": "./dist/index.js",

View file

@ -20,6 +20,13 @@ describe("classifyError", () => {
expect(classifyError(new Error("Schema validation failed"))).toBe("format");
});
it("classifies tool_call_id 400 errors as format (recoverable via transcript repair)", () => {
expect(
classifyError(new Error("400 tool_call_id is not found in the list of tool calls")),
).toBe("format");
expect(classifyError(new Error("400 Bad Request: tool_call_id not found"))).toBe("format");
});
it("classifies 429/rate limit as rate_limit", () => {
expect(classifyError(new Error("429 Too Many Requests"))).toBe("rate_limit");
expect(classifyError(new Error("Rate limit exceeded"))).toBe("rate_limit");

View file

@ -794,6 +794,8 @@ export class Agent {
const MAX_OVERFLOW_COMPACTION_ATTEMPTS = 2;
let overflowAttempts = 0;
const MAX_FORMAT_REPAIR_ATTEMPTS = 1;
let formatRepairAttempts = 0;
// Loop to exhaust all candidate profiles on rotatable errors
while (true) {
@ -860,6 +862,31 @@ export class Agent {
reason,
rotatable: isRotatableError(reason),
});
// Format error recovery: reload sanitized messages from disk and retry.
// This handles corrupted in-memory state (e.g. orphaned tool_call_id references)
// that causes persistent 400 errors until process restart.
if (reason === "format" && formatRepairAttempts < MAX_FORMAT_REPAIR_ATTEMPTS) {
formatRepairAttempts++;
this.stderr.write(
`[format-repair] Format error detected (attempt ${formatRepairAttempts}/${MAX_FORMAT_REPAIR_ATTEMPTS}), reloading messages from disk...\n`,
);
this.runLog.log("format_repair", {
attempt: formatRepairAttempts,
error: errorMsg.slice(0, 200),
messages_before: this.agent.state.messages.length,
});
const repairedMessages = this.session.loadMessages();
if (repairedMessages.length > 0) {
this.runLog.log("format_repair_reloaded", {
messages_after: repairedMessages.length,
});
this.agent.replaceMessages(repairedMessages);
this.output.state.lastAssistantText = "";
continue; // retry with sanitized messages
}
}
if (this.currentProfileId && isRotatableError(reason)) {
markAuthProfileFailure(this.currentProfileId, reason);
}