diff --git a/CHANGELOG.md b/CHANGELOG.md index ce7eb5574..dfd13a1ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Models/Providers: treat credential validation failures as auth errors to trigger fallback; normalize `${ENV_VAR}` apiKey values and auto-fill missing provider keys; preserve explicit GitHub Copilot provider config + agent-dir auth profiles. - Auth: drop invalid auth profiles from ordering so environment keys can still be used for providers like MiniMax. - Gemini: normalize Gemini 3 ids to preview variants; strip Gemini CLI tool call/response ids; downgrade missing `thought_signature`; strip Claude `msg_*` thought_signature fields to avoid base64 decode errors. +- Agents: auto-recover from compaction context overflow by resetting the session and retrying; propagate overflow details from embedded runs so callers can recover. - MiniMax: strip malformed tool invocation XML; include `MiniMax-VL-01` in implicit provider for image pairing. - Onboarding/Auth: honor `CLAWDBOT_AGENT_DIR` / `PI_CODING_AGENT_DIR` when writing auth profiles (MiniMax). (#829) — thanks @roshanasingh4. - Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid incorrect role errors. diff --git a/src/agents/pi-embedded-helpers.ts b/src/agents/pi-embedded-helpers.ts index 02a05d4a5..4a1c4fcc5 100644 --- a/src/agents/pi-embedded-helpers.ts +++ b/src/agents/pi-embedded-helpers.ts @@ -370,8 +370,8 @@ export function formatAssistantErrorText( // Check for context overflow (413) errors if (isContextOverflowError(raw)) { return ( - "Context overflow: the conversation history is too large. " + - "Use /new or /reset to start a fresh session." + "Context overflow: prompt too large for the model. " + + "Try again with less input or a larger-context model." ); } diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index 40eed6ea3..c01579392 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -362,6 +362,10 @@ export type EmbeddedPiRunMeta = { durationMs: number; agentMeta?: EmbeddedPiAgentMeta; aborted?: boolean; + error?: { + kind: "context_overflow" | "compaction_failure"; + message: string; + }; }; function buildModelAliasLines(cfg?: ClawdbotConfig) { @@ -1976,12 +1980,15 @@ export async function runEmbeddedPiAgent(params: { if (promptError && !aborted) { const errorText = describeUnknownError(promptError); if (isContextOverflowError(errorText)) { + const kind = isCompactionFailureError(errorText) + ? "compaction_failure" + : "context_overflow"; return { payloads: [ { text: - "Context overflow: the conversation history is too large for the model. " + - "Use /new or /reset to start a fresh session, or try a model with a larger context window.", + "Context overflow: prompt too large for the model. " + + "Try again with less input or a larger-context model.", isError: true, }, ], @@ -1992,6 +1999,7 @@ export async function runEmbeddedPiAgent(params: { provider, model: model.id, }, + error: { kind, message: errorText }, }, }; } diff --git a/src/auto-reply/reply/agent-runner.heartbeat-typing.test.ts b/src/auto-reply/reply/agent-runner.heartbeat-typing.test.ts index 89544d618..5b3d9b562 100644 --- a/src/auto-reply/reply/agent-runner.heartbeat-typing.test.ts +++ b/src/auto-reply/reply/agent-runner.heartbeat-typing.test.ts @@ -525,6 +525,65 @@ describe("runReplyAgent typing (heartbeat)", () => { } }); + it("retries after context overflow payload by resetting the session", async () => { + const prevStateDir = process.env.CLAWDBOT_STATE_DIR; + const stateDir = await fs.mkdtemp( + path.join(tmpdir(), "clawdbot-session-overflow-reset-"), + ); + process.env.CLAWDBOT_STATE_DIR = stateDir; + try { + const sessionId = "session"; + const storePath = path.join(stateDir, "sessions", "sessions.json"); + const sessionEntry = { sessionId, updatedAt: Date.now() }; + const sessionStore = { main: sessionEntry }; + + await fs.mkdir(path.dirname(storePath), { recursive: true }); + await fs.writeFile(storePath, JSON.stringify(sessionStore), "utf-8"); + + runEmbeddedPiAgentMock + .mockImplementationOnce(async () => ({ + payloads: [ + { text: "Context overflow: prompt too large", isError: true }, + ], + meta: { + durationMs: 1, + error: { + kind: "context_overflow", + message: + 'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}', + }, + }, + })) + .mockImplementationOnce(async () => ({ + payloads: [{ text: "ok" }], + meta: { durationMs: 1 }, + })); + + const callsBefore = runEmbeddedPiAgentMock.mock.calls.length; + const { run } = createMinimalRun({ + sessionEntry, + sessionStore, + sessionKey: "main", + storePath, + }); + const res = await run(); + + expect(runEmbeddedPiAgentMock.mock.calls.length - callsBefore).toBe(2); + const payload = Array.isArray(res) ? res[0] : res; + expect(payload).toMatchObject({ text: "ok" }); + expect(sessionStore.main.sessionId).not.toBe(sessionId); + + const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")); + expect(persisted.main.sessionId).toBe(sessionStore.main.sessionId); + } finally { + if (prevStateDir) { + process.env.CLAWDBOT_STATE_DIR = prevStateDir; + } else { + delete process.env.CLAWDBOT_STATE_DIR; + } + } + }); + it("still replies even if session reset fails to persist", async () => { const prevStateDir = process.env.CLAWDBOT_STATE_DIR; const stateDir = await fs.mkdtemp( diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index a25c55d92..3478aeb62 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -834,6 +834,20 @@ export async function runReplyAgent(params: { runResult = fallbackResult.result; fallbackProvider = fallbackResult.provider; fallbackModel = fallbackResult.model; + + // Some embedded runs surface context overflow as an error payload instead of throwing. + // Treat those as a session-level failure and auto-recover by starting a fresh session. + const embeddedError = runResult.meta?.error; + if ( + embeddedError && + isContextOverflowError(embeddedError.message) && + !didResetAfterCompactionFailure && + (await resetSessionAfterCompactionFailure(embeddedError.message)) + ) { + didResetAfterCompactionFailure = true; + continue; + } + break; } catch (err) { const message = err instanceof Error ? err.message : String(err); @@ -894,7 +908,7 @@ export async function runReplyAgent(params: { defaultRuntime.error(`Embedded agent failed before reply: ${message}`); return finalizeWithFollowup({ text: isContextOverflow - ? "⚠️ Context overflow - conversation too long. Starting fresh might help!" + ? "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model." : `⚠️ Agent failed before reply: ${message}. Check gateway logs for details.`, }); }