diff --git a/CHANGELOG.md b/CHANGELOG.md
index adb990f01..765889304 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,7 @@
- ClawdbotKit: fix SwiftPM resource bundling path for `tool-display.json`. Thanks @fcatuhe for PR #398.
- Tools: add Telegram/WhatsApp reaction tools (with per-provider gating). Thanks @zats for PR #353.
- Tools: flatten literal-union schemas for Claude on Vertex AI. Thanks @carlulsoe for PR #409.
+- Tools: keep tool failure logs concise (no stack traces); full stack only in debug logs.
- Tools: unify reaction removal semantics across Discord/Slack/Telegram/WhatsApp and allow WhatsApp reaction routing across accounts.
- Android: fix APK output filename renaming after AGP updates. Thanks @Syhids for PR #410.
- Android: rotate camera photos by EXIF orientation. Thanks @fcatuhe for PR #403.
@@ -40,6 +41,7 @@
- Agent: protect bootstrap prefix from context pruning. Thanks @maxsumrall for PR #381.
- Agent: deliver final replies for non-streaming models when block chunking is enabled. Thank you @mneves75 for PR #369!
- Agent: trim bootstrap context injections and keep group guidance concise (emoji reactions allowed). Thanks @tobiasbischoff for PR #370.
+- Agent: return a friendly context overflow response (413/request_too_large). Thanks @alejandroOPI for PR #395.
- Sub-agents: allow `sessions_spawn` model overrides and error on invalid models. Thanks @azade-c for PR #298.
- Sub-agents: skip invalid model overrides with a warning and keep the run alive; tool exceptions now return tool errors instead of crashing the agent.
- Sessions: forward explicit sessionKey through gateway/chat/node bridge to avoid sub-agent sessionId mixups.
diff --git a/README.md b/README.md
index a6623a632..eb491440b 100644
--- a/README.md
+++ b/README.md
@@ -454,5 +454,5 @@ Thanks to all clawtributors:
-
+
diff --git a/src/agents/pi-embedded-helpers.test.ts b/src/agents/pi-embedded-helpers.test.ts
index c36664dba..69a93430a 100644
--- a/src/agents/pi-embedded-helpers.test.ts
+++ b/src/agents/pi-embedded-helpers.test.ts
@@ -1,6 +1,11 @@
+import type { AssistantMessage } from "@mariozechner/pi-ai";
import { describe, expect, it } from "vitest";
-import { buildBootstrapContextFiles } from "./pi-embedded-helpers.js";
+import {
+ buildBootstrapContextFiles,
+ formatAssistantErrorText,
+ isContextOverflowError,
+} from "./pi-embedded-helpers.js";
import {
DEFAULT_AGENTS_FILENAME,
type WorkspaceBootstrapFile,
@@ -46,3 +51,35 @@ describe("buildBootstrapContextFiles", () => {
expect(result?.content.endsWith(long.slice(-120))).toBe(true);
});
});
+
+describe("isContextOverflowError", () => {
+ it("matches known overflow hints", () => {
+ const samples = [
+ "request_too_large",
+ "Request exceeds the maximum size",
+ "context length exceeded",
+ "Maximum context length",
+ "413 Request Entity Too Large",
+ ];
+ for (const sample of samples) {
+ expect(isContextOverflowError(sample)).toBe(true);
+ }
+ });
+
+ it("ignores unrelated errors", () => {
+ expect(isContextOverflowError("rate limit exceeded")).toBe(false);
+ });
+});
+
+describe("formatAssistantErrorText", () => {
+ const makeAssistantError = (errorMessage: string): AssistantMessage =>
+ ({
+ stopReason: "error",
+ errorMessage,
+ }) as AssistantMessage;
+
+ it("returns a friendly message for context overflow", () => {
+ const msg = makeAssistantError("request_too_large");
+ expect(formatAssistantErrorText(msg)).toContain("Context overflow");
+ });
+});
diff --git a/src/agents/pi-embedded-helpers.ts b/src/agents/pi-embedded-helpers.ts
index 750c9504b..0b0eaec19 100644
--- a/src/agents/pi-embedded-helpers.ts
+++ b/src/agents/pi-embedded-helpers.ts
@@ -126,6 +126,18 @@ export function buildBootstrapContextFiles(
return result;
}
+export function isContextOverflowError(errorMessage?: string): boolean {
+ if (!errorMessage) return false;
+ const lower = errorMessage.toLowerCase();
+ return (
+ lower.includes("request_too_large") ||
+ lower.includes("request exceeds the maximum size") ||
+ lower.includes("context length exceeded") ||
+ lower.includes("maximum context length") ||
+ (lower.includes("413") && lower.includes("too large"))
+ );
+}
+
export function formatAssistantErrorText(
msg: AssistantMessage,
): string | undefined {
@@ -133,6 +145,14 @@ export function formatAssistantErrorText(
const raw = (msg.errorMessage ?? "").trim();
if (!raw) return "LLM request failed with an unknown error.";
+ // 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."
+ );
+ }
+
const invalidRequest = raw.match(
/"type":"invalid_request_error".*?"message":"([^"]+)"/,
);
diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts
index 86b790a44..e5a75a473 100644
--- a/src/agents/pi-embedded-runner.ts
+++ b/src/agents/pi-embedded-runner.ts
@@ -59,6 +59,7 @@ import {
formatAssistantErrorText,
isAuthAssistantError,
isAuthErrorMessage,
+ isContextOverflowError,
isRateLimitAssistantError,
isRateLimitErrorMessage,
pickFallbackThinkingLevel,
@@ -1153,6 +1154,26 @@ export async function runEmbeddedPiAgent(params: {
}
if (promptError && !aborted) {
const errorText = describeUnknownError(promptError);
+ if (isContextOverflowError(errorText)) {
+ 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.",
+ isError: true,
+ },
+ ],
+ meta: {
+ durationMs: Date.now() - started,
+ agentMeta: {
+ sessionId: sessionIdUsed,
+ provider,
+ model: model.id,
+ },
+ },
+ };
+ }
if (
(isAuthErrorMessage(errorText) ||
isRateLimitErrorMessage(errorText)) &&
diff --git a/src/agents/pi-tool-definition-adapter.test.ts b/src/agents/pi-tool-definition-adapter.test.ts
index 27a101002..48700ad28 100644
--- a/src/agents/pi-tool-definition-adapter.test.ts
+++ b/src/agents/pi-tool-definition-adapter.test.ts
@@ -22,6 +22,7 @@ describe("pi tool definition adapter", () => {
status: "error",
tool: "boom",
});
- expect(JSON.stringify(result.details)).toContain("nope");
+ expect(result.details).toMatchObject({ error: "nope" });
+ expect(JSON.stringify(result.details)).not.toContain("\n at ");
});
});
diff --git a/src/agents/pi-tool-definition-adapter.ts b/src/agents/pi-tool-definition-adapter.ts
index df8b64d8d..9f4451625 100644
--- a/src/agents/pi-tool-definition-adapter.ts
+++ b/src/agents/pi-tool-definition-adapter.ts
@@ -4,12 +4,23 @@ import type {
AgentToolUpdateCallback,
} from "@mariozechner/pi-agent-core";
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
-import { logError } from "../logger.js";
+import { logDebug, logError } from "../logger.js";
import { jsonResult } from "./tools/common.js";
// biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance.
type AnyAgentTool = AgentTool;
+function describeToolExecutionError(err: unknown): {
+ message: string;
+ stack?: string;
+} {
+ if (err instanceof Error) {
+ const message = err.message?.trim() ? err.message : String(err);
+ return { message, stack: err.stack };
+ }
+ return { message: String(err) };
+}
+
export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] {
return tools.map((tool) => {
const name = tool.name || "tool";
@@ -37,13 +48,15 @@ export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] {
? String((err as { name?: unknown }).name)
: "";
if (name === "AbortError") throw err;
- const message =
- err instanceof Error ? (err.stack ?? err.message) : String(err);
- logError(`[tools] ${tool.name} failed: ${message}`);
+ const described = describeToolExecutionError(err);
+ if (described.stack && described.stack !== described.message) {
+ logDebug(`tools: ${tool.name} failed stack:\n${described.stack}`);
+ }
+ logError(`[tools] ${tool.name} failed: ${described.message}`);
return jsonResult({
status: "error",
tool: tool.name,
- error: message,
+ error: described.message,
});
}
},