diff --git a/packages/core/package.json b/packages/core/package.json index bee1ea97..746edfc1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/src/agent/auth-profiles/error-classification.test.ts b/packages/core/src/agent/auth-profiles/error-classification.test.ts index c771c6b4..a7ddce60 100644 --- a/packages/core/src/agent/auth-profiles/error-classification.test.ts +++ b/packages/core/src/agent/auth-profiles/error-classification.test.ts @@ -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"); diff --git a/packages/core/src/agent/runner.ts b/packages/core/src/agent/runner.ts index 5afbdecb..0d912a94 100644 --- a/packages/core/src/agent/runner.ts +++ b/packages/core/src/agent/runner.ts @@ -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); }