1000 lines
40 KiB
TypeScript
Raw Normal View History

2026-01-14 01:08:15 +00:00
import fs from "node:fs/promises";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
import type { RunEmbeddedPiAgentParams } from "./run/params.js";
import type { EmbeddedPiAgentMeta, EmbeddedPiRunResult } from "./types.js";
2026-01-14 01:08:15 +00:00
import { enqueueCommandInLane } from "../../process/command-queue.js";
import { isMarkdownCapableMessageChannel } from "../../utils/message-channel.js";
2026-01-30 03:15:10 +01:00
import { resolveOpenClawAgentDir } from "../agent-paths.js";
2026-01-14 01:08:15 +00:00
import {
isProfileInCooldown,
2026-01-14 01:08:15 +00:00
markAuthProfileFailure,
markAuthProfileGood,
markAuthProfileUsed,
} from "../auth-profiles.js";
import {
CONTEXT_WINDOW_HARD_MIN_TOKENS,
CONTEXT_WINDOW_WARN_BELOW_TOKENS,
evaluateContextWindowGuard,
resolveContextWindowInfo,
} from "../context-window-guard.js";
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
2026-01-14 01:08:15 +00:00
import { FailoverError, resolveFailoverStatus } from "../failover-error.js";
import {
ensureAuthProfileStore,
getApiKeyForModel,
resolveAuthProfileOrder,
type ResolvedProviderAuth,
2026-01-14 01:08:15 +00:00
} from "../model-auth.js";
import { normalizeProviderId } from "../model-selection.js";
2026-01-30 03:15:10 +01:00
import { ensureOpenClawModelsJson } from "../models-config.js";
2026-01-14 01:08:15 +00:00
import {
formatBillingErrorMessage,
2026-01-14 01:08:15 +00:00
classifyFailoverReason,
formatAssistantErrorText,
isAuthAssistantError,
isBillingAssistantError,
2026-01-14 01:08:15 +00:00
isCompactionFailureError,
isLikelyContextOverflowError,
2026-01-14 01:08:15 +00:00
isFailoverAssistantError,
isFailoverErrorMessage,
parseImageSizeError,
2026-01-18 15:19:25 +00:00
parseImageDimensionError,
2026-01-14 01:08:15 +00:00
isRateLimitAssistantError,
isTimeoutErrorMessage,
pickFallbackThinkingLevel,
type FailoverReason,
2026-01-14 01:08:15 +00:00
} from "../pi-embedded-helpers.js";
import { derivePromptTokens, normalizeUsage, type UsageLike } from "../usage.js";
import { redactRunIdentifier, resolveRunWorkspaceDir } from "../workspace-run.js";
fix: auto-compact on context overflow promptError before returning error (#1627) * fix: detect Anthropic 'Request size exceeds model context window' as context overflow Anthropic now returns 'Request size exceeds model context window' instead of the previously detected 'prompt is too long' format. This new error message was not recognized by isContextOverflowError(), causing auto-compaction to NOT trigger. Users would see the raw error twice without any recovery attempt. Changes: - Add 'exceeds model context window' and 'request size exceeds' to isContextOverflowError() detection patterns - Add tests that fail without the fix, verifying both the raw error string and the JSON-wrapped format from Anthropic's API - Add test for formatAssistantErrorText to ensure the friendly 'Context overflow' message is shown instead of the raw error Note: The upstream pi-ai package (@mariozechner/pi-ai) also needs a fix in its OVERFLOW_PATTERNS regex: /exceeds the context window/i should be changed to /exceeds.*context window/i to match both 'the' and 'model' variants for triggering auto-compaction retry. * fix(tests): remove unused imports and helper from test files Remove WorkspaceBootstrapFile references and _makeFile helper that were incorrectly copied from another test file. These caused type errors and were unrelated to the context overflow detection tests. * fix: trigger auto-compaction on context overflow promptError When the LLM rejects a request with a context overflow error that surfaces as a promptError (thrown exception rather than streamed error), the existing auto-compaction in pi-coding-agent never triggers. This happens because the error bypasses the agent's message_end → agent_end → _checkCompaction path. This fix adds a fallback compaction attempt directly in the run loop: - Detects context overflow in promptError (excluding compaction_failure) - Calls compactEmbeddedPiSessionDirect (bypassing lane queues since already in-lane) - Retries the prompt after successful compaction - Limits to one compaction attempt per run to prevent infinite loops Fixes: context overflow errors shown to user without auto-compaction attempt * style: format compact.ts and run.ts with oxfmt * fix: tighten context overflow match (#1627) (thanks @rodrigouroz) --------- Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-24 19:09:24 -03:00
import { compactEmbeddedPiSessionDirect } from "./compact.js";
2026-01-14 01:08:15 +00:00
import { resolveGlobalLane, resolveSessionLane } from "./lanes.js";
import { log } from "./logger.js";
import { resolveModel } from "./model.js";
import { runEmbeddedAttempt } from "./run/attempt.js";
import { buildEmbeddedRunPayloads } from "./run/payloads.js";
fix: recover from context overflow caused by oversized tool results (#11579) * fix: gracefully handle oversized tool results causing context overflow When a subagent reads a very large file or gets a huge tool result (e.g., gh pr diff on a massive PR), it can exceed the model's context window in a single prompt. Auto-compaction can't help because there's no older history to compact — just one giant tool result. This adds two layers of defense: 1. Pre-emptive: Hard cap on tool result size (400K chars ≈ 100K tokens) applied in the session tool result guard before persistence. This prevents extremely large tool results from being stored in full, regardless of model context window size. 2. Recovery: When context overflow is detected and compaction fails, scan session messages for oversized tool results relative to the model's actual context window (30% max share). If found, truncate them in the session via branching (creating a new branch with truncated content) and retry the prompt. The truncation preserves the beginning of the content (most useful for understanding what was read) and appends a notice explaining the truncation and suggesting offset/limit parameters for targeted reads. Includes comprehensive tests for: - Text truncation with newline-boundary awareness - Context-window-proportional size calculation - In-memory message truncation - Oversized detection heuristics - Guard-level size capping during persistence * fix: prep fixes for tool result truncation PR (#11579) (thanks @tyler6204)
2026-02-07 17:40:51 -08:00
import {
truncateOversizedToolResultsInSession,
sessionLikelyHasOversizedToolResults,
} from "./tool-result-truncation.js";
2026-01-14 01:08:15 +00:00
import { describeUnknownError } from "./utils.js";
type ApiKeyInfo = ResolvedProviderAuth;
2026-01-14 01:08:15 +00:00
2026-01-21 07:28:11 +00:00
// Avoid Anthropic's refusal test token poisoning session transcripts.
const ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL = "ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL";
const ANTHROPIC_MAGIC_STRING_REPLACEMENT = "ANTHROPIC MAGIC STRING TRIGGER REFUSAL (redacted)";
function scrubAnthropicRefusalMagic(prompt: string): string {
if (!prompt.includes(ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL)) {
return prompt;
}
2026-01-21 07:28:11 +00:00
return prompt.replaceAll(
ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL,
ANTHROPIC_MAGIC_STRING_REPLACEMENT,
);
}
type UsageAccumulator = {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
total: number;
/** Cache fields from the most recent API call (not accumulated). */
lastCacheRead: number;
lastCacheWrite: number;
lastInput: number;
};
const createUsageAccumulator = (): UsageAccumulator => ({
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
lastCacheRead: 0,
lastCacheWrite: 0,
lastInput: 0,
});
function createCompactionDiagId(): string {
return `ovf-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
const hasUsageValues = (
usage: ReturnType<typeof normalizeUsage>,
): usage is NonNullable<ReturnType<typeof normalizeUsage>> =>
!!usage &&
[usage.input, usage.output, usage.cacheRead, usage.cacheWrite, usage.total].some(
(value) => typeof value === "number" && Number.isFinite(value) && value > 0,
);
const mergeUsageIntoAccumulator = (
target: UsageAccumulator,
usage: ReturnType<typeof normalizeUsage>,
) => {
if (!hasUsageValues(usage)) {
return;
}
target.input += usage.input ?? 0;
target.output += usage.output ?? 0;
target.cacheRead += usage.cacheRead ?? 0;
target.cacheWrite += usage.cacheWrite ?? 0;
target.total +=
usage.total ??
(usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
// Track the most recent API call's cache fields for accurate context-size reporting.
// Accumulated cache totals inflate context size when there are multiple tool-call round-trips,
// since each call reports cacheRead ≈ current_context_size.
target.lastCacheRead = usage.cacheRead ?? 0;
target.lastCacheWrite = usage.cacheWrite ?? 0;
target.lastInput = usage.input ?? 0;
};
const toNormalizedUsage = (usage: UsageAccumulator) => {
const hasUsage =
usage.input > 0 ||
usage.output > 0 ||
usage.cacheRead > 0 ||
usage.cacheWrite > 0 ||
usage.total > 0;
if (!hasUsage) {
return undefined;
}
// Use the LAST API call's cache fields for context-size calculation.
// The accumulated cacheRead/cacheWrite inflate context size because each tool-call
// round-trip reports cacheRead ≈ current_context_size, and summing N calls gives
// N × context_size which gets clamped to contextWindow (e.g. 200k).
// See: https://github.com/openclaw/openclaw/issues/13698
//
// We use lastInput/lastCacheRead/lastCacheWrite (from the most recent API call) for
// cache-related fields, but keep accumulated output (total generated text this turn).
const lastPromptTokens = usage.lastInput + usage.lastCacheRead + usage.lastCacheWrite;
return {
input: usage.lastInput || undefined,
output: usage.output || undefined,
cacheRead: usage.lastCacheRead || undefined,
cacheWrite: usage.lastCacheWrite || undefined,
total: lastPromptTokens + usage.output || undefined,
};
};
2026-01-14 01:08:15 +00:00
export async function runEmbeddedPiAgent(
params: RunEmbeddedPiAgentParams,
): Promise<EmbeddedPiRunResult> {
const sessionLane = resolveSessionLane(params.sessionKey?.trim() || params.sessionId);
2026-01-14 01:08:15 +00:00
const globalLane = resolveGlobalLane(params.lane);
const enqueueGlobal =
params.enqueue ?? ((task, opts) => enqueueCommandInLane(globalLane, task, opts));
const enqueueSession =
params.enqueue ?? ((task, opts) => enqueueCommandInLane(sessionLane, task, opts));
const channelHint = params.messageChannel ?? params.messageProvider;
const resolvedToolResultFormat =
params.toolResultFormat ??
(channelHint
? isMarkdownCapableMessageChannel(channelHint)
? "markdown"
: "plain"
: "markdown");
2026-01-24 00:04:53 +00:00
const isProbeSession = params.sessionId?.startsWith("probe-") ?? false;
2026-01-14 01:08:15 +00:00
return enqueueSession(() =>
2026-01-14 01:08:15 +00:00
enqueueGlobal(async () => {
const started = Date.now();
const workspaceResolution = resolveRunWorkspaceDir({
workspaceDir: params.workspaceDir,
sessionKey: params.sessionKey,
agentId: params.agentId,
config: params.config,
});
const resolvedWorkspace = workspaceResolution.workspaceDir;
const redactedSessionId = redactRunIdentifier(params.sessionId);
const redactedSessionKey = redactRunIdentifier(params.sessionKey);
const redactedWorkspace = redactRunIdentifier(resolvedWorkspace);
if (workspaceResolution.usedFallback) {
log.warn(
`[workspace-fallback] caller=runEmbeddedPiAgent reason=${workspaceResolution.fallbackReason} run=${params.runId} session=${redactedSessionId} sessionKey=${redactedSessionKey} agent=${workspaceResolution.agentId} workspace=${redactedWorkspace}`,
);
}
2026-01-14 01:08:15 +00:00
const prevCwd = process.cwd();
const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
2026-01-14 01:08:15 +00:00
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
2026-01-30 03:15:10 +01:00
const agentDir = params.agentDir ?? resolveOpenClawAgentDir();
const fallbackConfigured =
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
2026-01-30 03:15:10 +01:00
await ensureOpenClawModelsJson(params.config, agentDir);
2026-01-14 01:08:15 +00:00
const { model, error, authStorage, modelRegistry } = resolveModel(
provider,
modelId,
agentDir,
params.config,
);
if (!model) {
throw new Error(error ?? `Unknown model: ${provider}/${modelId}`);
}
const ctxInfo = resolveContextWindowInfo({
cfg: params.config,
provider,
modelId,
modelContextWindow: model.contextWindow,
defaultTokens: DEFAULT_CONTEXT_TOKENS,
});
const ctxGuard = evaluateContextWindowGuard({
info: ctxInfo,
warnBelowTokens: CONTEXT_WINDOW_WARN_BELOW_TOKENS,
hardMinTokens: CONTEXT_WINDOW_HARD_MIN_TOKENS,
});
if (ctxGuard.shouldWarn) {
log.warn(
`low context window: ${provider}/${modelId} ctx=${ctxGuard.tokens} (warn<${CONTEXT_WINDOW_WARN_BELOW_TOKENS}) source=${ctxGuard.source}`,
);
}
if (ctxGuard.shouldBlock) {
log.error(
`blocked model (context window too small): ${provider}/${modelId} ctx=${ctxGuard.tokens} (min=${CONTEXT_WINDOW_HARD_MIN_TOKENS}) source=${ctxGuard.source}`,
);
throw new FailoverError(
`Model context window too small (${ctxGuard.tokens} tokens). Minimum is ${CONTEXT_WINDOW_HARD_MIN_TOKENS}.`,
{ reason: "unknown", provider, model: modelId },
);
}
const authStore = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
const preferredProfileId = params.authProfileId?.trim();
let lockedProfileId = params.authProfileIdSource === "user" ? preferredProfileId : undefined;
if (lockedProfileId) {
const lockedProfile = authStore.profiles[lockedProfileId];
if (
!lockedProfile ||
normalizeProviderId(lockedProfile.provider) !== normalizeProviderId(provider)
) {
lockedProfileId = undefined;
}
}
2026-01-14 01:08:15 +00:00
const profileOrder = resolveAuthProfileOrder({
cfg: params.config,
store: authStore,
provider,
preferredProfile: preferredProfileId,
2026-01-14 01:08:15 +00:00
});
if (lockedProfileId && !profileOrder.includes(lockedProfileId)) {
throw new Error(`Auth profile "${lockedProfileId}" is not configured for ${provider}.`);
2026-01-14 01:08:15 +00:00
}
const profileCandidates = lockedProfileId
? [lockedProfileId]
: profileOrder.length > 0
? profileOrder
: [undefined];
2026-01-14 01:08:15 +00:00
let profileIndex = 0;
const initialThinkLevel = params.thinkLevel ?? "off";
let thinkLevel = initialThinkLevel;
const attemptedThinking = new Set<ThinkLevel>();
let apiKeyInfo: ApiKeyInfo | null = null;
let lastProfileId: string | undefined;
const resolveAuthProfileFailoverReason = (params: {
allInCooldown: boolean;
message: string;
}): FailoverReason => {
if (params.allInCooldown) {
return "rate_limit";
}
const classified = classifyFailoverReason(params.message);
return classified ?? "auth";
};
const throwAuthProfileFailover = (params: {
allInCooldown: boolean;
message?: string;
error?: unknown;
}): never => {
const fallbackMessage = `No available auth profile for ${provider} (all in cooldown or unavailable).`;
const message =
params.message?.trim() ||
(params.error ? describeUnknownError(params.error).trim() : "") ||
fallbackMessage;
const reason = resolveAuthProfileFailoverReason({
allInCooldown: params.allInCooldown,
message,
});
if (fallbackConfigured) {
throw new FailoverError(message, {
reason,
provider,
model: modelId,
status: resolveFailoverStatus(reason),
cause: params.error,
});
}
if (params.error instanceof Error) {
throw params.error;
}
throw new Error(message);
};
2026-01-14 01:08:15 +00:00
const resolveApiKeyForCandidate = async (candidate?: string) => {
return getApiKeyForModel({
model,
cfg: params.config,
profileId: candidate,
store: authStore,
2026-01-15 04:41:50 +00:00
agentDir,
2026-01-14 01:08:15 +00:00
});
};
const applyApiKeyInfo = async (candidate?: string): Promise<void> => {
apiKeyInfo = await resolveApiKeyForCandidate(candidate);
const resolvedProfileId = apiKeyInfo.profileId ?? candidate;
if (!apiKeyInfo.apiKey) {
if (apiKeyInfo.mode !== "aws-sdk") {
throw new Error(
`No API key resolved for provider "${model.provider}" (auth mode: ${apiKeyInfo.mode}).`,
);
}
lastProfileId = resolvedProfileId;
return;
}
if (model.provider === "github-copilot") {
const { resolveCopilotApiToken } =
await import("../../providers/github-copilot-token.js");
const copilotToken = await resolveCopilotApiToken({
githubToken: apiKeyInfo.apiKey,
});
authStorage.setRuntimeApiKey(model.provider, copilotToken.token);
} else {
authStorage.setRuntimeApiKey(model.provider, apiKeyInfo.apiKey);
}
lastProfileId = apiKeyInfo.profileId;
2026-01-14 01:08:15 +00:00
};
const advanceAuthProfile = async (): Promise<boolean> => {
if (lockedProfileId) {
return false;
}
2026-01-14 01:08:15 +00:00
let nextIndex = profileIndex + 1;
while (nextIndex < profileCandidates.length) {
const candidate = profileCandidates[nextIndex];
if (candidate && isProfileInCooldown(authStore, candidate)) {
nextIndex += 1;
continue;
}
2026-01-14 01:08:15 +00:00
try {
await applyApiKeyInfo(candidate);
profileIndex = nextIndex;
thinkLevel = initialThinkLevel;
attemptedThinking.clear();
return true;
} catch (err) {
if (candidate && candidate === lockedProfileId) {
throw err;
}
2026-01-14 01:08:15 +00:00
nextIndex += 1;
}
}
return false;
};
try {
while (profileIndex < profileCandidates.length) {
const candidate = profileCandidates[profileIndex];
if (
candidate &&
candidate !== lockedProfileId &&
isProfileInCooldown(authStore, candidate)
) {
profileIndex += 1;
continue;
}
await applyApiKeyInfo(profileCandidates[profileIndex]);
break;
}
if (profileIndex >= profileCandidates.length) {
throwAuthProfileFailover({ allInCooldown: true });
}
2026-01-14 01:08:15 +00:00
} catch (err) {
if (err instanceof FailoverError) {
throw err;
}
if (profileCandidates[profileIndex] === lockedProfileId) {
throwAuthProfileFailover({ allInCooldown: false, error: err });
}
2026-01-14 01:08:15 +00:00
const advanced = await advanceAuthProfile();
if (!advanced) {
throwAuthProfileFailover({ allInCooldown: false, error: err });
}
2026-01-14 01:08:15 +00:00
}
const MAX_OVERFLOW_COMPACTION_ATTEMPTS = 3;
let overflowCompactionAttempts = 0;
fix: recover from context overflow caused by oversized tool results (#11579) * fix: gracefully handle oversized tool results causing context overflow When a subagent reads a very large file or gets a huge tool result (e.g., gh pr diff on a massive PR), it can exceed the model's context window in a single prompt. Auto-compaction can't help because there's no older history to compact — just one giant tool result. This adds two layers of defense: 1. Pre-emptive: Hard cap on tool result size (400K chars ≈ 100K tokens) applied in the session tool result guard before persistence. This prevents extremely large tool results from being stored in full, regardless of model context window size. 2. Recovery: When context overflow is detected and compaction fails, scan session messages for oversized tool results relative to the model's actual context window (30% max share). If found, truncate them in the session via branching (creating a new branch with truncated content) and retry the prompt. The truncation preserves the beginning of the content (most useful for understanding what was read) and appends a notice explaining the truncation and suggesting offset/limit parameters for targeted reads. Includes comprehensive tests for: - Text truncation with newline-boundary awareness - Context-window-proportional size calculation - In-memory message truncation - Oversized detection heuristics - Guard-level size capping during persistence * fix: prep fixes for tool result truncation PR (#11579) (thanks @tyler6204)
2026-02-07 17:40:51 -08:00
let toolResultTruncationAttempted = false;
const usageAccumulator = createUsageAccumulator();
let lastRunPromptUsage: ReturnType<typeof normalizeUsage> | undefined;
let autoCompactionCount = 0;
2026-01-14 01:08:15 +00:00
try {
while (true) {
attemptedThinking.add(thinkLevel);
await fs.mkdir(resolvedWorkspace, { recursive: true });
2026-01-21 07:28:11 +00:00
const prompt =
provider === "anthropic" ? scrubAnthropicRefusalMagic(params.prompt) : params.prompt;
2026-01-14 01:08:15 +00:00
const attempt = await runEmbeddedAttempt({
sessionId: params.sessionId,
sessionKey: params.sessionKey,
messageChannel: params.messageChannel,
messageProvider: params.messageProvider,
agentAccountId: params.agentAccountId,
messageTo: params.messageTo,
messageThreadId: params.messageThreadId,
2026-01-24 15:35:05 +13:00
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
spawnedBy: params.spawnedBy,
senderIsOwner: params.senderIsOwner,
2026-01-14 01:08:15 +00:00
currentChannelId: params.currentChannelId,
currentThreadTs: params.currentThreadTs,
replyToMode: params.replyToMode,
hasRepliedRef: params.hasRepliedRef,
sessionFile: params.sessionFile,
workspaceDir: resolvedWorkspace,
2026-01-14 01:08:15 +00:00
agentDir,
config: params.config,
skillsSnapshot: params.skillsSnapshot,
2026-01-21 07:28:11 +00:00
prompt,
2026-01-14 01:08:15 +00:00
images: params.images,
disableTools: params.disableTools,
2026-01-14 01:08:15 +00:00
provider,
modelId,
model,
authStorage,
modelRegistry,
agentId: workspaceResolution.agentId,
2026-01-14 01:08:15 +00:00
thinkLevel,
verboseLevel: params.verboseLevel,
reasoningLevel: params.reasoningLevel,
toolResultFormat: resolvedToolResultFormat,
2026-01-18 06:11:38 +00:00
execOverrides: params.execOverrides,
2026-01-14 01:08:15 +00:00
bashElevated: params.bashElevated,
timeoutMs: params.timeoutMs,
runId: params.runId,
abortSignal: params.abortSignal,
shouldEmitToolResult: params.shouldEmitToolResult,
2026-01-17 05:33:27 +00:00
shouldEmitToolOutput: params.shouldEmitToolOutput,
2026-01-14 01:08:15 +00:00
onPartialReply: params.onPartialReply,
onAssistantMessageStart: params.onAssistantMessageStart,
onBlockReply: params.onBlockReply,
onBlockReplyFlush: params.onBlockReplyFlush,
blockReplyBreak: params.blockReplyBreak,
blockReplyChunking: params.blockReplyChunking,
onReasoningStream: params.onReasoningStream,
onReasoningEnd: params.onReasoningEnd,
2026-01-14 01:08:15 +00:00
onToolResult: params.onToolResult,
onAgentEvent: params.onAgentEvent,
extraSystemPrompt: params.extraSystemPrompt,
inputProvenance: params.inputProvenance,
streamParams: params.streamParams,
2026-01-14 01:08:15 +00:00
ownerNumbers: params.ownerNumbers,
enforceFinalTag: params.enforceFinalTag,
});
const {
aborted,
promptError,
timedOut,
timedOutDuringCompaction,
sessionIdUsed,
lastAssistant,
} = attempt;
const lastAssistantUsage = normalizeUsage(lastAssistant?.usage as UsageLike);
const attemptUsage = attempt.attemptUsage ?? lastAssistantUsage;
mergeUsageIntoAccumulator(usageAccumulator, attemptUsage);
// Keep prompt size from the latest model call so session totalTokens
// reflects current context usage, not accumulated tool-loop usage.
lastRunPromptUsage = lastAssistantUsage ?? attemptUsage;
Agents: add nested subagent orchestration controls and reduce subagent token waste (#14447) * Agents: add subagent orchestration controls * Agents: add subagent orchestration controls (WIP uncommitted changes) * feat(subagents): add depth-based spawn gating for sub-sub-agents * feat(subagents): tool policy, registry, and announce chain for nested agents * feat(subagents): system prompt, docs, changelog for nested sub-agents * fix(subagents): prevent model fallback override, show model during active runs, and block context overflow fallback Bug 1: When a session has an explicit model override (e.g., gpt/openai-codex), the fallback candidate logic in resolveFallbackCandidates silently appended the global primary model (opus) as a backstop. On reinjection/steer with a transient error, the session could fall back to opus which has a smaller context window and crash. Fix: when storedModelOverride is set, pass fallbacksOverride ?? [] instead of undefined, preventing the implicit primary backstop. Bug 2: Active subagents showed 'model n/a' in /subagents list because resolveModelDisplay only read entry.model/modelProvider (populated after run completes). Fix: fall back to modelOverride/providerOverride fields which are populated at spawn time via sessions.patch. Bug 3: Context overflow errors (prompt too long, context_length_exceeded) could theoretically escape runEmbeddedPiAgent and be treated as failover candidates in runWithModelFallback, causing a switch to a model with a smaller context window. Fix: in runWithModelFallback, detect context overflow errors via isLikelyContextOverflowError and rethrow them immediately instead of trying the next model candidate. * fix(subagents): track spawn depth in session store and fix announce routing for nested agents * Fix compaction status tracking and dedupe overflow compaction triggers * fix(subagents): enforce depth block via session store and implement cascade kill * fix: inject group chat context into system prompt * fix(subagents): always write model to session store at spawn time * Preserve spawnDepth when agent handler rewrites session entry * fix(subagents): suppress announce on steer-restart * fix(subagents): fallback spawned session model to runtime default * fix(subagents): enforce spawn depth when caller key resolves by sessionId * feat(subagents): implement active-first ordering for numeric targets and enhance task display - Added a test to verify that subagents with numeric targets follow an active-first list ordering. - Updated `resolveSubagentTarget` to sort subagent runs based on active status and recent activity. - Enhanced task display in command responses to prevent truncation of long task descriptions. - Introduced new utility functions for compacting task text and managing subagent run states. * fix(subagents): show model for active runs via run record fallback When the spawned model matches the agent's default model, the session store's override fields are intentionally cleared (isDefault: true). The model/modelProvider fields are only populated after the run completes. This left active subagents showing 'model n/a'. Fix: store the resolved model on SubagentRunRecord at registration time, and use it as a fallback in both display paths (subagents tool and /subagents command) when the session store entry has no model info. Changes: - SubagentRunRecord: add optional model field - registerSubagentRun: accept and persist model param - sessions-spawn-tool: pass resolvedModel to registerSubagentRun - subagents-tool: pass run record model as fallback to resolveModelDisplay - commands-subagents: pass run record model as fallback to resolveModelDisplay * feat(chat): implement session key resolution and reset on sidebar navigation - Added functions to resolve the main session key and reset chat state when switching sessions from the sidebar. - Updated the `renderTab` function to handle session key changes when navigating to the chat tab. - Introduced a test to verify that the session resets to "main" when opening chat from the sidebar navigation. * fix: subagent timeout=0 passthrough and fallback prompt duplication Bug 1: runTimeoutSeconds=0 now means 'no timeout' instead of applying 600s default - sessions-spawn-tool: default to undefined (not 0) when neither timeout param is provided; use != null check so explicit 0 passes through to gateway - agent.ts: accept 0 as valid timeout (resolveAgentTimeoutMs already handles 0 → MAX_SAFE_TIMEOUT_MS) Bug 2: model fallback no longer re-injects the original prompt as a duplicate - agent.ts: track fallback attempt index; on retries use a short continuation message instead of the full original prompt since the session file already contains it from the first attempt - Also skip re-sending images on fallback retries (already in session) * feat(subagents): truncate long task descriptions in subagents command output - Introduced a new utility function to format task previews, limiting their length to improve readability. - Updated the command handler to use the new formatting function, ensuring task descriptions are truncated appropriately. - Adjusted related tests to verify that long task descriptions are now truncated in the output. * refactor(subagents): update subagent registry path resolution and improve command output formatting - Replaced direct import of STATE_DIR with a utility function to resolve the state directory dynamically. - Enhanced the formatting of command output for active and recent subagents, adding separators for better readability. - Updated related tests to reflect changes in command output structure. * fix(subagent): default sessions_spawn to no timeout when runTimeoutSeconds omitted The previous fix (75a791106) correctly handled the case where runTimeoutSeconds was explicitly set to 0 ("no timeout"). However, when models omit the parameter entirely (which is common since the schema marks it as optional), runTimeoutSeconds resolved to undefined. undefined flowed through the chain as: sessions_spawn → timeout: undefined (since undefined != null is false) → gateway agent handler → agentCommand opts.timeout: undefined → resolveAgentTimeoutMs({ overrideSeconds: undefined }) → DEFAULT_AGENT_TIMEOUT_SECONDS (600s = 10 minutes) This caused subagents to be killed at exactly 10 minutes even though the user's intent (via TOOLS.md) was for subagents to run without a timeout. Fix: default runTimeoutSeconds to 0 (no timeout) when neither runTimeoutSeconds nor timeoutSeconds is provided by the caller. Subagent spawns are long-running by design and should not inherit the 600s agent-command default timeout. * fix(subagent): accept timeout=0 in agent-via-gateway path (second 600s default) * fix: thread timeout override through getReplyFromConfig dispatch path getReplyFromConfig called resolveAgentTimeoutMs({ cfg }) with no override, always falling back to the config default (600s). Add timeoutOverrideSeconds to GetReplyOptions and pass it through as overrideSeconds so callers of the dispatch chain can specify a custom timeout (0 = no timeout). This complements the existing timeout threading in agentCommand and the cron isolated-agent runner, which already pass overrideSeconds correctly. * feat(model-fallback): normalize OpenAI Codex model references and enhance fallback handling - Added normalization for OpenAI Codex model references, specifically converting "gpt-5.3-codex" to "openai-codex" before execution. - Updated the `resolveFallbackCandidates` function to utilize the new normalization logic. - Enhanced tests to verify the correct behavior of model normalization and fallback mechanisms. - Introduced a new test case to ensure that the normalization process works as expected for various input formats. * feat(tests): add unit tests for steer failure behavior in openclaw-tools - Introduced a new test file to validate the behavior of subagents when steer replacement dispatch fails. - Implemented tests to ensure that the announce behavior is restored correctly and that the suppression reason is cleared as expected. - Enhanced the subagent registry with a new function to clear steer restart suppression. - Updated related components to support the new test scenarios. * fix(subagents): replace stop command with kill in slash commands and documentation - Updated the `/subagents` command to replace `stop` with `kill` for consistency in controlling sub-agent runs. - Modified related documentation to reflect the change in command usage. - Removed legacy timeoutSeconds references from the sessions-spawn-tool schema and tests to streamline timeout handling. - Enhanced tests to ensure correct behavior of the updated commands and their interactions. * feat(tests): add unit tests for readLatestAssistantReply function - Introduced a new test file for the `readLatestAssistantReply` function to validate its behavior with various message scenarios. - Implemented tests to ensure the function correctly retrieves the latest assistant message and handles cases where the latest message has no text. - Mocked the gateway call to simulate different message histories for comprehensive testing. * feat(tests): enhance subagent kill-all cascade tests and announce formatting - Added a new test to verify that the `kill-all` command cascades through ended parents to active descendants in subagents. - Updated the subagent announce formatting tests to reflect changes in message structure, including the replacement of "Findings:" with "Result:" and the addition of new expectations for message content. - Improved the handling of long findings and stats in the announce formatting logic to ensure concise output. - Refactored related functions to enhance clarity and maintainability in the subagent registry and tools. * refactor(subagent): update announce formatting and remove unused constants - Modified the subagent announce formatting to replace "Findings:" with "Result:" and adjusted related expectations in tests. - Removed constants for maximum announce findings characters and summary words, simplifying the announcement logic. - Updated the handling of findings to retain full content instead of truncating, ensuring more informative outputs. - Cleaned up unused imports in the commands-subagents file to enhance code clarity. * feat(tests): enhance billing error handling in user-facing text - Added tests to ensure that normal text mentioning billing plans is not rewritten, preserving user context. - Updated the `isBillingErrorMessage` and `sanitizeUserFacingText` functions to improve handling of billing-related messages. - Introduced new test cases for various scenarios involving billing messages to ensure accurate processing and output. - Enhanced the subagent announce flow to correctly manage active descendant runs, preventing premature announcements. * feat(subagent): enhance workflow guidance and auto-announcement clarity - Added a new guideline in the subagent system prompt to emphasize trust in push-based completion, discouraging busy polling for status updates. - Updated documentation to clarify that sub-agents will automatically announce their results, improving user understanding of the workflow. - Enhanced tests to verify the new guidance on avoiding polling loops and to ensure the accuracy of the updated prompts. * fix(cron): avoid announcing interim subagent spawn acks * chore: clean post-rebase imports * fix(cron): fall back to child replies when parent stays interim * fix(subagents): make active-run guidance advisory * fix(subagents): update announce flow to handle active descendants and enhance test coverage - Modified the announce flow to defer announcements when active descendant runs are present, ensuring accurate status reporting. - Updated tests to verify the new behavior, including scenarios where no fallback requester is available and ensuring proper handling of finished subagents. - Enhanced the announce formatting to include an `expectFinal` flag for better clarity in the announcement process. * fix(subagents): enhance announce flow and formatting for user updates - Updated the announce flow to provide clearer instructions for user updates based on active subagent runs and requester context. - Refactored the announcement logic to improve clarity and ensure internal context remains private. - Enhanced tests to verify the new message expectations and formatting, including updated prompts for user-facing updates. - Introduced a new function to build reply instructions based on session context, improving the overall announcement process. * fix: resolve prep blockers and changelog placement (#14447) (thanks @tyler6204) * fix: restore cron delivery-plan import after rebase (#14447) (thanks @tyler6204) * fix: resolve test failures from rebase conflicts (#14447) (thanks @tyler6204) * fix: apply formatting after rebase (#14447) (thanks @tyler6204)
2026-02-14 22:03:45 -08:00
const attemptCompactionCount = Math.max(0, attempt.compactionCount ?? 0);
autoCompactionCount += attemptCompactionCount;
const formattedAssistantErrorText = lastAssistant
? formatAssistantErrorText(lastAssistant, {
cfg: params.config,
sessionKey: params.sessionKey ?? params.sessionId,
provider,
})
: undefined;
const assistantErrorText =
lastAssistant?.stopReason === "error"
? lastAssistant.errorMessage?.trim() || formattedAssistantErrorText
: undefined;
const contextOverflowError = !aborted
? (() => {
if (promptError) {
const errorText = describeUnknownError(promptError);
if (isLikelyContextOverflowError(errorText)) {
return { text: errorText, source: "promptError" as const };
}
// Prompt submission failed with a non-overflow error. Do not
// inspect prior assistant errors from history for this attempt.
return null;
}
if (assistantErrorText && isLikelyContextOverflowError(assistantErrorText)) {
return { text: assistantErrorText, source: "assistantError" as const };
}
return null;
})()
: null;
2026-01-14 01:08:15 +00:00
if (contextOverflowError) {
const overflowDiagId = createCompactionDiagId();
const errorText = contextOverflowError.text;
const msgCount = attempt.messagesSnapshot?.length ?? 0;
log.warn(
`[context-overflow-diag] sessionKey=${params.sessionKey ?? params.sessionId} ` +
`provider=${provider}/${modelId} source=${contextOverflowError.source} ` +
`messages=${msgCount} sessionFile=${params.sessionFile} ` +
`diagId=${overflowDiagId} compactionAttempts=${overflowCompactionAttempts} ` +
`error=${errorText.slice(0, 200)}`,
);
const isCompactionFailure = isCompactionFailureError(errorText);
Agents: add nested subagent orchestration controls and reduce subagent token waste (#14447) * Agents: add subagent orchestration controls * Agents: add subagent orchestration controls (WIP uncommitted changes) * feat(subagents): add depth-based spawn gating for sub-sub-agents * feat(subagents): tool policy, registry, and announce chain for nested agents * feat(subagents): system prompt, docs, changelog for nested sub-agents * fix(subagents): prevent model fallback override, show model during active runs, and block context overflow fallback Bug 1: When a session has an explicit model override (e.g., gpt/openai-codex), the fallback candidate logic in resolveFallbackCandidates silently appended the global primary model (opus) as a backstop. On reinjection/steer with a transient error, the session could fall back to opus which has a smaller context window and crash. Fix: when storedModelOverride is set, pass fallbacksOverride ?? [] instead of undefined, preventing the implicit primary backstop. Bug 2: Active subagents showed 'model n/a' in /subagents list because resolveModelDisplay only read entry.model/modelProvider (populated after run completes). Fix: fall back to modelOverride/providerOverride fields which are populated at spawn time via sessions.patch. Bug 3: Context overflow errors (prompt too long, context_length_exceeded) could theoretically escape runEmbeddedPiAgent and be treated as failover candidates in runWithModelFallback, causing a switch to a model with a smaller context window. Fix: in runWithModelFallback, detect context overflow errors via isLikelyContextOverflowError and rethrow them immediately instead of trying the next model candidate. * fix(subagents): track spawn depth in session store and fix announce routing for nested agents * Fix compaction status tracking and dedupe overflow compaction triggers * fix(subagents): enforce depth block via session store and implement cascade kill * fix: inject group chat context into system prompt * fix(subagents): always write model to session store at spawn time * Preserve spawnDepth when agent handler rewrites session entry * fix(subagents): suppress announce on steer-restart * fix(subagents): fallback spawned session model to runtime default * fix(subagents): enforce spawn depth when caller key resolves by sessionId * feat(subagents): implement active-first ordering for numeric targets and enhance task display - Added a test to verify that subagents with numeric targets follow an active-first list ordering. - Updated `resolveSubagentTarget` to sort subagent runs based on active status and recent activity. - Enhanced task display in command responses to prevent truncation of long task descriptions. - Introduced new utility functions for compacting task text and managing subagent run states. * fix(subagents): show model for active runs via run record fallback When the spawned model matches the agent's default model, the session store's override fields are intentionally cleared (isDefault: true). The model/modelProvider fields are only populated after the run completes. This left active subagents showing 'model n/a'. Fix: store the resolved model on SubagentRunRecord at registration time, and use it as a fallback in both display paths (subagents tool and /subagents command) when the session store entry has no model info. Changes: - SubagentRunRecord: add optional model field - registerSubagentRun: accept and persist model param - sessions-spawn-tool: pass resolvedModel to registerSubagentRun - subagents-tool: pass run record model as fallback to resolveModelDisplay - commands-subagents: pass run record model as fallback to resolveModelDisplay * feat(chat): implement session key resolution and reset on sidebar navigation - Added functions to resolve the main session key and reset chat state when switching sessions from the sidebar. - Updated the `renderTab` function to handle session key changes when navigating to the chat tab. - Introduced a test to verify that the session resets to "main" when opening chat from the sidebar navigation. * fix: subagent timeout=0 passthrough and fallback prompt duplication Bug 1: runTimeoutSeconds=0 now means 'no timeout' instead of applying 600s default - sessions-spawn-tool: default to undefined (not 0) when neither timeout param is provided; use != null check so explicit 0 passes through to gateway - agent.ts: accept 0 as valid timeout (resolveAgentTimeoutMs already handles 0 → MAX_SAFE_TIMEOUT_MS) Bug 2: model fallback no longer re-injects the original prompt as a duplicate - agent.ts: track fallback attempt index; on retries use a short continuation message instead of the full original prompt since the session file already contains it from the first attempt - Also skip re-sending images on fallback retries (already in session) * feat(subagents): truncate long task descriptions in subagents command output - Introduced a new utility function to format task previews, limiting their length to improve readability. - Updated the command handler to use the new formatting function, ensuring task descriptions are truncated appropriately. - Adjusted related tests to verify that long task descriptions are now truncated in the output. * refactor(subagents): update subagent registry path resolution and improve command output formatting - Replaced direct import of STATE_DIR with a utility function to resolve the state directory dynamically. - Enhanced the formatting of command output for active and recent subagents, adding separators for better readability. - Updated related tests to reflect changes in command output structure. * fix(subagent): default sessions_spawn to no timeout when runTimeoutSeconds omitted The previous fix (75a791106) correctly handled the case where runTimeoutSeconds was explicitly set to 0 ("no timeout"). However, when models omit the parameter entirely (which is common since the schema marks it as optional), runTimeoutSeconds resolved to undefined. undefined flowed through the chain as: sessions_spawn → timeout: undefined (since undefined != null is false) → gateway agent handler → agentCommand opts.timeout: undefined → resolveAgentTimeoutMs({ overrideSeconds: undefined }) → DEFAULT_AGENT_TIMEOUT_SECONDS (600s = 10 minutes) This caused subagents to be killed at exactly 10 minutes even though the user's intent (via TOOLS.md) was for subagents to run without a timeout. Fix: default runTimeoutSeconds to 0 (no timeout) when neither runTimeoutSeconds nor timeoutSeconds is provided by the caller. Subagent spawns are long-running by design and should not inherit the 600s agent-command default timeout. * fix(subagent): accept timeout=0 in agent-via-gateway path (second 600s default) * fix: thread timeout override through getReplyFromConfig dispatch path getReplyFromConfig called resolveAgentTimeoutMs({ cfg }) with no override, always falling back to the config default (600s). Add timeoutOverrideSeconds to GetReplyOptions and pass it through as overrideSeconds so callers of the dispatch chain can specify a custom timeout (0 = no timeout). This complements the existing timeout threading in agentCommand and the cron isolated-agent runner, which already pass overrideSeconds correctly. * feat(model-fallback): normalize OpenAI Codex model references and enhance fallback handling - Added normalization for OpenAI Codex model references, specifically converting "gpt-5.3-codex" to "openai-codex" before execution. - Updated the `resolveFallbackCandidates` function to utilize the new normalization logic. - Enhanced tests to verify the correct behavior of model normalization and fallback mechanisms. - Introduced a new test case to ensure that the normalization process works as expected for various input formats. * feat(tests): add unit tests for steer failure behavior in openclaw-tools - Introduced a new test file to validate the behavior of subagents when steer replacement dispatch fails. - Implemented tests to ensure that the announce behavior is restored correctly and that the suppression reason is cleared as expected. - Enhanced the subagent registry with a new function to clear steer restart suppression. - Updated related components to support the new test scenarios. * fix(subagents): replace stop command with kill in slash commands and documentation - Updated the `/subagents` command to replace `stop` with `kill` for consistency in controlling sub-agent runs. - Modified related documentation to reflect the change in command usage. - Removed legacy timeoutSeconds references from the sessions-spawn-tool schema and tests to streamline timeout handling. - Enhanced tests to ensure correct behavior of the updated commands and their interactions. * feat(tests): add unit tests for readLatestAssistantReply function - Introduced a new test file for the `readLatestAssistantReply` function to validate its behavior with various message scenarios. - Implemented tests to ensure the function correctly retrieves the latest assistant message and handles cases where the latest message has no text. - Mocked the gateway call to simulate different message histories for comprehensive testing. * feat(tests): enhance subagent kill-all cascade tests and announce formatting - Added a new test to verify that the `kill-all` command cascades through ended parents to active descendants in subagents. - Updated the subagent announce formatting tests to reflect changes in message structure, including the replacement of "Findings:" with "Result:" and the addition of new expectations for message content. - Improved the handling of long findings and stats in the announce formatting logic to ensure concise output. - Refactored related functions to enhance clarity and maintainability in the subagent registry and tools. * refactor(subagent): update announce formatting and remove unused constants - Modified the subagent announce formatting to replace "Findings:" with "Result:" and adjusted related expectations in tests. - Removed constants for maximum announce findings characters and summary words, simplifying the announcement logic. - Updated the handling of findings to retain full content instead of truncating, ensuring more informative outputs. - Cleaned up unused imports in the commands-subagents file to enhance code clarity. * feat(tests): enhance billing error handling in user-facing text - Added tests to ensure that normal text mentioning billing plans is not rewritten, preserving user context. - Updated the `isBillingErrorMessage` and `sanitizeUserFacingText` functions to improve handling of billing-related messages. - Introduced new test cases for various scenarios involving billing messages to ensure accurate processing and output. - Enhanced the subagent announce flow to correctly manage active descendant runs, preventing premature announcements. * feat(subagent): enhance workflow guidance and auto-announcement clarity - Added a new guideline in the subagent system prompt to emphasize trust in push-based completion, discouraging busy polling for status updates. - Updated documentation to clarify that sub-agents will automatically announce their results, improving user understanding of the workflow. - Enhanced tests to verify the new guidance on avoiding polling loops and to ensure the accuracy of the updated prompts. * fix(cron): avoid announcing interim subagent spawn acks * chore: clean post-rebase imports * fix(cron): fall back to child replies when parent stays interim * fix(subagents): make active-run guidance advisory * fix(subagents): update announce flow to handle active descendants and enhance test coverage - Modified the announce flow to defer announcements when active descendant runs are present, ensuring accurate status reporting. - Updated tests to verify the new behavior, including scenarios where no fallback requester is available and ensuring proper handling of finished subagents. - Enhanced the announce formatting to include an `expectFinal` flag for better clarity in the announcement process. * fix(subagents): enhance announce flow and formatting for user updates - Updated the announce flow to provide clearer instructions for user updates based on active subagent runs and requester context. - Refactored the announcement logic to improve clarity and ensure internal context remains private. - Enhanced tests to verify the new message expectations and formatting, including updated prompts for user-facing updates. - Introduced a new function to build reply instructions based on session context, improving the overall announcement process. * fix: resolve prep blockers and changelog placement (#14447) (thanks @tyler6204) * fix: restore cron delivery-plan import after rebase (#14447) (thanks @tyler6204) * fix: resolve test failures from rebase conflicts (#14447) (thanks @tyler6204) * fix: apply formatting after rebase (#14447) (thanks @tyler6204)
2026-02-14 22:03:45 -08:00
const hadAttemptLevelCompaction = attemptCompactionCount > 0;
// If this attempt already compacted (SDK auto-compaction), avoid immediately
// running another explicit compaction for the same overflow trigger.
if (
!isCompactionFailure &&
Agents: add nested subagent orchestration controls and reduce subagent token waste (#14447) * Agents: add subagent orchestration controls * Agents: add subagent orchestration controls (WIP uncommitted changes) * feat(subagents): add depth-based spawn gating for sub-sub-agents * feat(subagents): tool policy, registry, and announce chain for nested agents * feat(subagents): system prompt, docs, changelog for nested sub-agents * fix(subagents): prevent model fallback override, show model during active runs, and block context overflow fallback Bug 1: When a session has an explicit model override (e.g., gpt/openai-codex), the fallback candidate logic in resolveFallbackCandidates silently appended the global primary model (opus) as a backstop. On reinjection/steer with a transient error, the session could fall back to opus which has a smaller context window and crash. Fix: when storedModelOverride is set, pass fallbacksOverride ?? [] instead of undefined, preventing the implicit primary backstop. Bug 2: Active subagents showed 'model n/a' in /subagents list because resolveModelDisplay only read entry.model/modelProvider (populated after run completes). Fix: fall back to modelOverride/providerOverride fields which are populated at spawn time via sessions.patch. Bug 3: Context overflow errors (prompt too long, context_length_exceeded) could theoretically escape runEmbeddedPiAgent and be treated as failover candidates in runWithModelFallback, causing a switch to a model with a smaller context window. Fix: in runWithModelFallback, detect context overflow errors via isLikelyContextOverflowError and rethrow them immediately instead of trying the next model candidate. * fix(subagents): track spawn depth in session store and fix announce routing for nested agents * Fix compaction status tracking and dedupe overflow compaction triggers * fix(subagents): enforce depth block via session store and implement cascade kill * fix: inject group chat context into system prompt * fix(subagents): always write model to session store at spawn time * Preserve spawnDepth when agent handler rewrites session entry * fix(subagents): suppress announce on steer-restart * fix(subagents): fallback spawned session model to runtime default * fix(subagents): enforce spawn depth when caller key resolves by sessionId * feat(subagents): implement active-first ordering for numeric targets and enhance task display - Added a test to verify that subagents with numeric targets follow an active-first list ordering. - Updated `resolveSubagentTarget` to sort subagent runs based on active status and recent activity. - Enhanced task display in command responses to prevent truncation of long task descriptions. - Introduced new utility functions for compacting task text and managing subagent run states. * fix(subagents): show model for active runs via run record fallback When the spawned model matches the agent's default model, the session store's override fields are intentionally cleared (isDefault: true). The model/modelProvider fields are only populated after the run completes. This left active subagents showing 'model n/a'. Fix: store the resolved model on SubagentRunRecord at registration time, and use it as a fallback in both display paths (subagents tool and /subagents command) when the session store entry has no model info. Changes: - SubagentRunRecord: add optional model field - registerSubagentRun: accept and persist model param - sessions-spawn-tool: pass resolvedModel to registerSubagentRun - subagents-tool: pass run record model as fallback to resolveModelDisplay - commands-subagents: pass run record model as fallback to resolveModelDisplay * feat(chat): implement session key resolution and reset on sidebar navigation - Added functions to resolve the main session key and reset chat state when switching sessions from the sidebar. - Updated the `renderTab` function to handle session key changes when navigating to the chat tab. - Introduced a test to verify that the session resets to "main" when opening chat from the sidebar navigation. * fix: subagent timeout=0 passthrough and fallback prompt duplication Bug 1: runTimeoutSeconds=0 now means 'no timeout' instead of applying 600s default - sessions-spawn-tool: default to undefined (not 0) when neither timeout param is provided; use != null check so explicit 0 passes through to gateway - agent.ts: accept 0 as valid timeout (resolveAgentTimeoutMs already handles 0 → MAX_SAFE_TIMEOUT_MS) Bug 2: model fallback no longer re-injects the original prompt as a duplicate - agent.ts: track fallback attempt index; on retries use a short continuation message instead of the full original prompt since the session file already contains it from the first attempt - Also skip re-sending images on fallback retries (already in session) * feat(subagents): truncate long task descriptions in subagents command output - Introduced a new utility function to format task previews, limiting their length to improve readability. - Updated the command handler to use the new formatting function, ensuring task descriptions are truncated appropriately. - Adjusted related tests to verify that long task descriptions are now truncated in the output. * refactor(subagents): update subagent registry path resolution and improve command output formatting - Replaced direct import of STATE_DIR with a utility function to resolve the state directory dynamically. - Enhanced the formatting of command output for active and recent subagents, adding separators for better readability. - Updated related tests to reflect changes in command output structure. * fix(subagent): default sessions_spawn to no timeout when runTimeoutSeconds omitted The previous fix (75a791106) correctly handled the case where runTimeoutSeconds was explicitly set to 0 ("no timeout"). However, when models omit the parameter entirely (which is common since the schema marks it as optional), runTimeoutSeconds resolved to undefined. undefined flowed through the chain as: sessions_spawn → timeout: undefined (since undefined != null is false) → gateway agent handler → agentCommand opts.timeout: undefined → resolveAgentTimeoutMs({ overrideSeconds: undefined }) → DEFAULT_AGENT_TIMEOUT_SECONDS (600s = 10 minutes) This caused subagents to be killed at exactly 10 minutes even though the user's intent (via TOOLS.md) was for subagents to run without a timeout. Fix: default runTimeoutSeconds to 0 (no timeout) when neither runTimeoutSeconds nor timeoutSeconds is provided by the caller. Subagent spawns are long-running by design and should not inherit the 600s agent-command default timeout. * fix(subagent): accept timeout=0 in agent-via-gateway path (second 600s default) * fix: thread timeout override through getReplyFromConfig dispatch path getReplyFromConfig called resolveAgentTimeoutMs({ cfg }) with no override, always falling back to the config default (600s). Add timeoutOverrideSeconds to GetReplyOptions and pass it through as overrideSeconds so callers of the dispatch chain can specify a custom timeout (0 = no timeout). This complements the existing timeout threading in agentCommand and the cron isolated-agent runner, which already pass overrideSeconds correctly. * feat(model-fallback): normalize OpenAI Codex model references and enhance fallback handling - Added normalization for OpenAI Codex model references, specifically converting "gpt-5.3-codex" to "openai-codex" before execution. - Updated the `resolveFallbackCandidates` function to utilize the new normalization logic. - Enhanced tests to verify the correct behavior of model normalization and fallback mechanisms. - Introduced a new test case to ensure that the normalization process works as expected for various input formats. * feat(tests): add unit tests for steer failure behavior in openclaw-tools - Introduced a new test file to validate the behavior of subagents when steer replacement dispatch fails. - Implemented tests to ensure that the announce behavior is restored correctly and that the suppression reason is cleared as expected. - Enhanced the subagent registry with a new function to clear steer restart suppression. - Updated related components to support the new test scenarios. * fix(subagents): replace stop command with kill in slash commands and documentation - Updated the `/subagents` command to replace `stop` with `kill` for consistency in controlling sub-agent runs. - Modified related documentation to reflect the change in command usage. - Removed legacy timeoutSeconds references from the sessions-spawn-tool schema and tests to streamline timeout handling. - Enhanced tests to ensure correct behavior of the updated commands and their interactions. * feat(tests): add unit tests for readLatestAssistantReply function - Introduced a new test file for the `readLatestAssistantReply` function to validate its behavior with various message scenarios. - Implemented tests to ensure the function correctly retrieves the latest assistant message and handles cases where the latest message has no text. - Mocked the gateway call to simulate different message histories for comprehensive testing. * feat(tests): enhance subagent kill-all cascade tests and announce formatting - Added a new test to verify that the `kill-all` command cascades through ended parents to active descendants in subagents. - Updated the subagent announce formatting tests to reflect changes in message structure, including the replacement of "Findings:" with "Result:" and the addition of new expectations for message content. - Improved the handling of long findings and stats in the announce formatting logic to ensure concise output. - Refactored related functions to enhance clarity and maintainability in the subagent registry and tools. * refactor(subagent): update announce formatting and remove unused constants - Modified the subagent announce formatting to replace "Findings:" with "Result:" and adjusted related expectations in tests. - Removed constants for maximum announce findings characters and summary words, simplifying the announcement logic. - Updated the handling of findings to retain full content instead of truncating, ensuring more informative outputs. - Cleaned up unused imports in the commands-subagents file to enhance code clarity. * feat(tests): enhance billing error handling in user-facing text - Added tests to ensure that normal text mentioning billing plans is not rewritten, preserving user context. - Updated the `isBillingErrorMessage` and `sanitizeUserFacingText` functions to improve handling of billing-related messages. - Introduced new test cases for various scenarios involving billing messages to ensure accurate processing and output. - Enhanced the subagent announce flow to correctly manage active descendant runs, preventing premature announcements. * feat(subagent): enhance workflow guidance and auto-announcement clarity - Added a new guideline in the subagent system prompt to emphasize trust in push-based completion, discouraging busy polling for status updates. - Updated documentation to clarify that sub-agents will automatically announce their results, improving user understanding of the workflow. - Enhanced tests to verify the new guidance on avoiding polling loops and to ensure the accuracy of the updated prompts. * fix(cron): avoid announcing interim subagent spawn acks * chore: clean post-rebase imports * fix(cron): fall back to child replies when parent stays interim * fix(subagents): make active-run guidance advisory * fix(subagents): update announce flow to handle active descendants and enhance test coverage - Modified the announce flow to defer announcements when active descendant runs are present, ensuring accurate status reporting. - Updated tests to verify the new behavior, including scenarios where no fallback requester is available and ensuring proper handling of finished subagents. - Enhanced the announce formatting to include an `expectFinal` flag for better clarity in the announcement process. * fix(subagents): enhance announce flow and formatting for user updates - Updated the announce flow to provide clearer instructions for user updates based on active subagent runs and requester context. - Refactored the announcement logic to improve clarity and ensure internal context remains private. - Enhanced tests to verify the new message expectations and formatting, including updated prompts for user-facing updates. - Introduced a new function to build reply instructions based on session context, improving the overall announcement process. * fix: resolve prep blockers and changelog placement (#14447) (thanks @tyler6204) * fix: restore cron delivery-plan import after rebase (#14447) (thanks @tyler6204) * fix: resolve test failures from rebase conflicts (#14447) (thanks @tyler6204) * fix: apply formatting after rebase (#14447) (thanks @tyler6204)
2026-02-14 22:03:45 -08:00
hadAttemptLevelCompaction &&
overflowCompactionAttempts < MAX_OVERFLOW_COMPACTION_ATTEMPTS
) {
overflowCompactionAttempts++;
log.warn(
`context overflow persisted after in-attempt compaction (attempt ${overflowCompactionAttempts}/${MAX_OVERFLOW_COMPACTION_ATTEMPTS}); retrying prompt without additional compaction for ${provider}/${modelId}`,
);
continue;
}
// Attempt explicit overflow compaction only when this attempt did not
// already auto-compact.
if (
!isCompactionFailure &&
!hadAttemptLevelCompaction &&
overflowCompactionAttempts < MAX_OVERFLOW_COMPACTION_ATTEMPTS
) {
if (log.isEnabled("debug")) {
log.debug(
`[compaction-diag] decision diagId=${overflowDiagId} branch=compact ` +
`isCompactionFailure=${isCompactionFailure} hasOversizedToolResults=unknown ` +
`attempt=${overflowCompactionAttempts + 1} maxAttempts=${MAX_OVERFLOW_COMPACTION_ATTEMPTS}`,
);
}
overflowCompactionAttempts++;
log.warn(
`context overflow detected (attempt ${overflowCompactionAttempts}/${MAX_OVERFLOW_COMPACTION_ATTEMPTS}); attempting auto-compaction for ${provider}/${modelId}`,
);
const compactResult = await compactEmbeddedPiSessionDirect({
sessionId: params.sessionId,
sessionKey: params.sessionKey,
messageChannel: params.messageChannel,
messageProvider: params.messageProvider,
agentAccountId: params.agentAccountId,
authProfileId: lastProfileId,
sessionFile: params.sessionFile,
workspaceDir: resolvedWorkspace,
agentDir,
config: params.config,
skillsSnapshot: params.skillsSnapshot,
senderIsOwner: params.senderIsOwner,
provider,
model: modelId,
runId: params.runId,
thinkLevel,
reasoningLevel: params.reasoningLevel,
bashElevated: params.bashElevated,
extraSystemPrompt: params.extraSystemPrompt,
ownerNumbers: params.ownerNumbers,
trigger: "overflow",
diagId: overflowDiagId,
attempt: overflowCompactionAttempts,
maxAttempts: MAX_OVERFLOW_COMPACTION_ATTEMPTS,
});
if (compactResult.compacted) {
autoCompactionCount += 1;
log.info(`auto-compaction succeeded for ${provider}/${modelId}; retrying prompt`);
continue;
}
log.warn(
`auto-compaction failed for ${provider}/${modelId}: ${compactResult.reason ?? "nothing to compact"}`,
);
}
// Fallback: try truncating oversized tool results in the session.
// This handles the case where a single tool result exceeds the
// context window and compaction cannot reduce it further.
if (!toolResultTruncationAttempted) {
const contextWindowTokens = ctxInfo.tokens;
const hasOversized = attempt.messagesSnapshot
? sessionLikelyHasOversizedToolResults({
messages: attempt.messagesSnapshot,
contextWindowTokens,
})
: false;
if (hasOversized) {
if (log.isEnabled("debug")) {
log.debug(
`[compaction-diag] decision diagId=${overflowDiagId} branch=truncate_tool_results ` +
`isCompactionFailure=${isCompactionFailure} hasOversizedToolResults=${hasOversized} ` +
`attempt=${overflowCompactionAttempts} maxAttempts=${MAX_OVERFLOW_COMPACTION_ATTEMPTS}`,
);
}
toolResultTruncationAttempted = true;
fix: auto-compact on context overflow promptError before returning error (#1627) * fix: detect Anthropic 'Request size exceeds model context window' as context overflow Anthropic now returns 'Request size exceeds model context window' instead of the previously detected 'prompt is too long' format. This new error message was not recognized by isContextOverflowError(), causing auto-compaction to NOT trigger. Users would see the raw error twice without any recovery attempt. Changes: - Add 'exceeds model context window' and 'request size exceeds' to isContextOverflowError() detection patterns - Add tests that fail without the fix, verifying both the raw error string and the JSON-wrapped format from Anthropic's API - Add test for formatAssistantErrorText to ensure the friendly 'Context overflow' message is shown instead of the raw error Note: The upstream pi-ai package (@mariozechner/pi-ai) also needs a fix in its OVERFLOW_PATTERNS regex: /exceeds the context window/i should be changed to /exceeds.*context window/i to match both 'the' and 'model' variants for triggering auto-compaction retry. * fix(tests): remove unused imports and helper from test files Remove WorkspaceBootstrapFile references and _makeFile helper that were incorrectly copied from another test file. These caused type errors and were unrelated to the context overflow detection tests. * fix: trigger auto-compaction on context overflow promptError When the LLM rejects a request with a context overflow error that surfaces as a promptError (thrown exception rather than streamed error), the existing auto-compaction in pi-coding-agent never triggers. This happens because the error bypasses the agent's message_end → agent_end → _checkCompaction path. This fix adds a fallback compaction attempt directly in the run loop: - Detects context overflow in promptError (excluding compaction_failure) - Calls compactEmbeddedPiSessionDirect (bypassing lane queues since already in-lane) - Retries the prompt after successful compaction - Limits to one compaction attempt per run to prevent infinite loops Fixes: context overflow errors shown to user without auto-compaction attempt * style: format compact.ts and run.ts with oxfmt * fix: tighten context overflow match (#1627) (thanks @rodrigouroz) --------- Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-24 19:09:24 -03:00
log.warn(
`[context-overflow-recovery] Attempting tool result truncation for ${provider}/${modelId} ` +
`(contextWindow=${contextWindowTokens} tokens)`,
fix: auto-compact on context overflow promptError before returning error (#1627) * fix: detect Anthropic 'Request size exceeds model context window' as context overflow Anthropic now returns 'Request size exceeds model context window' instead of the previously detected 'prompt is too long' format. This new error message was not recognized by isContextOverflowError(), causing auto-compaction to NOT trigger. Users would see the raw error twice without any recovery attempt. Changes: - Add 'exceeds model context window' and 'request size exceeds' to isContextOverflowError() detection patterns - Add tests that fail without the fix, verifying both the raw error string and the JSON-wrapped format from Anthropic's API - Add test for formatAssistantErrorText to ensure the friendly 'Context overflow' message is shown instead of the raw error Note: The upstream pi-ai package (@mariozechner/pi-ai) also needs a fix in its OVERFLOW_PATTERNS regex: /exceeds the context window/i should be changed to /exceeds.*context window/i to match both 'the' and 'model' variants for triggering auto-compaction retry. * fix(tests): remove unused imports and helper from test files Remove WorkspaceBootstrapFile references and _makeFile helper that were incorrectly copied from another test file. These caused type errors and were unrelated to the context overflow detection tests. * fix: trigger auto-compaction on context overflow promptError When the LLM rejects a request with a context overflow error that surfaces as a promptError (thrown exception rather than streamed error), the existing auto-compaction in pi-coding-agent never triggers. This happens because the error bypasses the agent's message_end → agent_end → _checkCompaction path. This fix adds a fallback compaction attempt directly in the run loop: - Detects context overflow in promptError (excluding compaction_failure) - Calls compactEmbeddedPiSessionDirect (bypassing lane queues since already in-lane) - Retries the prompt after successful compaction - Limits to one compaction attempt per run to prevent infinite loops Fixes: context overflow errors shown to user without auto-compaction attempt * style: format compact.ts and run.ts with oxfmt * fix: tighten context overflow match (#1627) (thanks @rodrigouroz) --------- Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-24 19:09:24 -03:00
);
const truncResult = await truncateOversizedToolResultsInSession({
sessionFile: params.sessionFile,
contextWindowTokens,
fix: auto-compact on context overflow promptError before returning error (#1627) * fix: detect Anthropic 'Request size exceeds model context window' as context overflow Anthropic now returns 'Request size exceeds model context window' instead of the previously detected 'prompt is too long' format. This new error message was not recognized by isContextOverflowError(), causing auto-compaction to NOT trigger. Users would see the raw error twice without any recovery attempt. Changes: - Add 'exceeds model context window' and 'request size exceeds' to isContextOverflowError() detection patterns - Add tests that fail without the fix, verifying both the raw error string and the JSON-wrapped format from Anthropic's API - Add test for formatAssistantErrorText to ensure the friendly 'Context overflow' message is shown instead of the raw error Note: The upstream pi-ai package (@mariozechner/pi-ai) also needs a fix in its OVERFLOW_PATTERNS regex: /exceeds the context window/i should be changed to /exceeds.*context window/i to match both 'the' and 'model' variants for triggering auto-compaction retry. * fix(tests): remove unused imports and helper from test files Remove WorkspaceBootstrapFile references and _makeFile helper that were incorrectly copied from another test file. These caused type errors and were unrelated to the context overflow detection tests. * fix: trigger auto-compaction on context overflow promptError When the LLM rejects a request with a context overflow error that surfaces as a promptError (thrown exception rather than streamed error), the existing auto-compaction in pi-coding-agent never triggers. This happens because the error bypasses the agent's message_end → agent_end → _checkCompaction path. This fix adds a fallback compaction attempt directly in the run loop: - Detects context overflow in promptError (excluding compaction_failure) - Calls compactEmbeddedPiSessionDirect (bypassing lane queues since already in-lane) - Retries the prompt after successful compaction - Limits to one compaction attempt per run to prevent infinite loops Fixes: context overflow errors shown to user without auto-compaction attempt * style: format compact.ts and run.ts with oxfmt * fix: tighten context overflow match (#1627) (thanks @rodrigouroz) --------- Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-24 19:09:24 -03:00
sessionId: params.sessionId,
sessionKey: params.sessionKey,
});
if (truncResult.truncated) {
log.info(
`[context-overflow-recovery] Truncated ${truncResult.truncatedCount} tool result(s); retrying prompt`,
);
// Session is now smaller; allow compaction retries again.
overflowCompactionAttempts = 0;
fix: auto-compact on context overflow promptError before returning error (#1627) * fix: detect Anthropic 'Request size exceeds model context window' as context overflow Anthropic now returns 'Request size exceeds model context window' instead of the previously detected 'prompt is too long' format. This new error message was not recognized by isContextOverflowError(), causing auto-compaction to NOT trigger. Users would see the raw error twice without any recovery attempt. Changes: - Add 'exceeds model context window' and 'request size exceeds' to isContextOverflowError() detection patterns - Add tests that fail without the fix, verifying both the raw error string and the JSON-wrapped format from Anthropic's API - Add test for formatAssistantErrorText to ensure the friendly 'Context overflow' message is shown instead of the raw error Note: The upstream pi-ai package (@mariozechner/pi-ai) also needs a fix in its OVERFLOW_PATTERNS regex: /exceeds the context window/i should be changed to /exceeds.*context window/i to match both 'the' and 'model' variants for triggering auto-compaction retry. * fix(tests): remove unused imports and helper from test files Remove WorkspaceBootstrapFile references and _makeFile helper that were incorrectly copied from another test file. These caused type errors and were unrelated to the context overflow detection tests. * fix: trigger auto-compaction on context overflow promptError When the LLM rejects a request with a context overflow error that surfaces as a promptError (thrown exception rather than streamed error), the existing auto-compaction in pi-coding-agent never triggers. This happens because the error bypasses the agent's message_end → agent_end → _checkCompaction path. This fix adds a fallback compaction attempt directly in the run loop: - Detects context overflow in promptError (excluding compaction_failure) - Calls compactEmbeddedPiSessionDirect (bypassing lane queues since already in-lane) - Retries the prompt after successful compaction - Limits to one compaction attempt per run to prevent infinite loops Fixes: context overflow errors shown to user without auto-compaction attempt * style: format compact.ts and run.ts with oxfmt * fix: tighten context overflow match (#1627) (thanks @rodrigouroz) --------- Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-24 19:09:24 -03:00
continue;
}
log.warn(
`[context-overflow-recovery] Tool result truncation did not help: ${truncResult.reason ?? "unknown"}`,
fix: auto-compact on context overflow promptError before returning error (#1627) * fix: detect Anthropic 'Request size exceeds model context window' as context overflow Anthropic now returns 'Request size exceeds model context window' instead of the previously detected 'prompt is too long' format. This new error message was not recognized by isContextOverflowError(), causing auto-compaction to NOT trigger. Users would see the raw error twice without any recovery attempt. Changes: - Add 'exceeds model context window' and 'request size exceeds' to isContextOverflowError() detection patterns - Add tests that fail without the fix, verifying both the raw error string and the JSON-wrapped format from Anthropic's API - Add test for formatAssistantErrorText to ensure the friendly 'Context overflow' message is shown instead of the raw error Note: The upstream pi-ai package (@mariozechner/pi-ai) also needs a fix in its OVERFLOW_PATTERNS regex: /exceeds the context window/i should be changed to /exceeds.*context window/i to match both 'the' and 'model' variants for triggering auto-compaction retry. * fix(tests): remove unused imports and helper from test files Remove WorkspaceBootstrapFile references and _makeFile helper that were incorrectly copied from another test file. These caused type errors and were unrelated to the context overflow detection tests. * fix: trigger auto-compaction on context overflow promptError When the LLM rejects a request with a context overflow error that surfaces as a promptError (thrown exception rather than streamed error), the existing auto-compaction in pi-coding-agent never triggers. This happens because the error bypasses the agent's message_end → agent_end → _checkCompaction path. This fix adds a fallback compaction attempt directly in the run loop: - Detects context overflow in promptError (excluding compaction_failure) - Calls compactEmbeddedPiSessionDirect (bypassing lane queues since already in-lane) - Retries the prompt after successful compaction - Limits to one compaction attempt per run to prevent infinite loops Fixes: context overflow errors shown to user without auto-compaction attempt * style: format compact.ts and run.ts with oxfmt * fix: tighten context overflow match (#1627) (thanks @rodrigouroz) --------- Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-24 19:09:24 -03:00
);
} else if (log.isEnabled("debug")) {
log.debug(
`[compaction-diag] decision diagId=${overflowDiagId} branch=give_up ` +
`isCompactionFailure=${isCompactionFailure} hasOversizedToolResults=${hasOversized} ` +
`attempt=${overflowCompactionAttempts} maxAttempts=${MAX_OVERFLOW_COMPACTION_ATTEMPTS}`,
);
fix: auto-compact on context overflow promptError before returning error (#1627) * fix: detect Anthropic 'Request size exceeds model context window' as context overflow Anthropic now returns 'Request size exceeds model context window' instead of the previously detected 'prompt is too long' format. This new error message was not recognized by isContextOverflowError(), causing auto-compaction to NOT trigger. Users would see the raw error twice without any recovery attempt. Changes: - Add 'exceeds model context window' and 'request size exceeds' to isContextOverflowError() detection patterns - Add tests that fail without the fix, verifying both the raw error string and the JSON-wrapped format from Anthropic's API - Add test for formatAssistantErrorText to ensure the friendly 'Context overflow' message is shown instead of the raw error Note: The upstream pi-ai package (@mariozechner/pi-ai) also needs a fix in its OVERFLOW_PATTERNS regex: /exceeds the context window/i should be changed to /exceeds.*context window/i to match both 'the' and 'model' variants for triggering auto-compaction retry. * fix(tests): remove unused imports and helper from test files Remove WorkspaceBootstrapFile references and _makeFile helper that were incorrectly copied from another test file. These caused type errors and were unrelated to the context overflow detection tests. * fix: trigger auto-compaction on context overflow promptError When the LLM rejects a request with a context overflow error that surfaces as a promptError (thrown exception rather than streamed error), the existing auto-compaction in pi-coding-agent never triggers. This happens because the error bypasses the agent's message_end → agent_end → _checkCompaction path. This fix adds a fallback compaction attempt directly in the run loop: - Detects context overflow in promptError (excluding compaction_failure) - Calls compactEmbeddedPiSessionDirect (bypassing lane queues since already in-lane) - Retries the prompt after successful compaction - Limits to one compaction attempt per run to prevent infinite loops Fixes: context overflow errors shown to user without auto-compaction attempt * style: format compact.ts and run.ts with oxfmt * fix: tighten context overflow match (#1627) (thanks @rodrigouroz) --------- Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-24 19:09:24 -03:00
}
2026-01-14 01:08:15 +00:00
}
if (
(isCompactionFailure ||
overflowCompactionAttempts >= MAX_OVERFLOW_COMPACTION_ATTEMPTS ||
toolResultTruncationAttempted) &&
log.isEnabled("debug")
) {
log.debug(
`[compaction-diag] decision diagId=${overflowDiagId} branch=give_up ` +
`isCompactionFailure=${isCompactionFailure} hasOversizedToolResults=unknown ` +
`attempt=${overflowCompactionAttempts} maxAttempts=${MAX_OVERFLOW_COMPACTION_ATTEMPTS}`,
);
}
const kind = isCompactionFailure ? "compaction_failure" : "context_overflow";
return {
payloads: [
{
text:
"Context overflow: prompt too large for the model. " +
"Try /reset (or /new) to start a fresh session, or use a larger-context model.",
isError: true,
},
],
meta: {
durationMs: Date.now() - started,
agentMeta: {
sessionId: sessionIdUsed,
provider,
model: model.id,
},
systemPromptReport: attempt.systemPromptReport,
error: { kind, message: errorText },
},
};
}
if (promptError && !aborted) {
const errorText = describeUnknownError(promptError);
// Handle role ordering errors with a user-friendly message
if (/incorrect role information|roles must alternate/i.test(errorText)) {
return {
payloads: [
{
text:
"Message ordering conflict - please try again. " +
"If this persists, use /new to start a fresh session.",
isError: true,
},
],
meta: {
durationMs: Date.now() - started,
agentMeta: {
sessionId: sessionIdUsed,
provider,
model: model.id,
},
systemPromptReport: attempt.systemPromptReport,
error: { kind: "role_ordering", message: errorText },
},
};
}
// Handle image size errors with a user-friendly message (no retry needed)
const imageSizeError = parseImageSizeError(errorText);
if (imageSizeError) {
const maxMb = imageSizeError.maxMb;
const maxMbLabel =
typeof maxMb === "number" && Number.isFinite(maxMb) ? `${maxMb}` : null;
const maxBytesHint = maxMbLabel ? ` (max ${maxMbLabel}MB)` : "";
return {
payloads: [
{
text:
`Image too large for the model${maxBytesHint}. ` +
"Please compress or resize the image and try again.",
isError: true,
},
],
meta: {
durationMs: Date.now() - started,
agentMeta: {
sessionId: sessionIdUsed,
provider,
model: model.id,
},
systemPromptReport: attempt.systemPromptReport,
error: { kind: "image_size", message: errorText },
},
};
}
2026-01-14 01:08:15 +00:00
const promptFailoverReason = classifyFailoverReason(errorText);
if (promptFailoverReason && promptFailoverReason !== "timeout" && lastProfileId) {
2026-01-14 01:08:15 +00:00
await markAuthProfileFailure({
store: authStore,
profileId: lastProfileId,
reason: promptFailoverReason,
cfg: params.config,
agentDir: params.agentDir,
});
}
if (
isFailoverErrorMessage(errorText) &&
promptFailoverReason !== "timeout" &&
(await advanceAuthProfile())
) {
continue;
}
const fallbackThinking = pickFallbackThinkingLevel({
message: errorText,
attempted: attemptedThinking,
});
if (fallbackThinking) {
log.warn(
`unsupported thinking level for ${provider}/${modelId}; retrying with ${fallbackThinking}`,
);
thinkLevel = fallbackThinking;
continue;
}
// FIX: Throw FailoverError for prompt errors when fallbacks configured
// This enables model fallback for quota/rate limit errors during prompt submission
if (fallbackConfigured && isFailoverErrorMessage(errorText)) {
throw new FailoverError(errorText, {
reason: promptFailoverReason ?? "unknown",
provider,
model: modelId,
profileId: lastProfileId,
status: resolveFailoverStatus(promptFailoverReason ?? "unknown"),
});
}
2026-01-14 01:08:15 +00:00
throw promptError;
}
const fallbackThinking = pickFallbackThinkingLevel({
message: lastAssistant?.errorMessage,
attempted: attemptedThinking,
});
if (fallbackThinking && !aborted) {
log.warn(
`unsupported thinking level for ${provider}/${modelId}; retrying with ${fallbackThinking}`,
);
thinkLevel = fallbackThinking;
continue;
}
const authFailure = isAuthAssistantError(lastAssistant);
const rateLimitFailure = isRateLimitAssistantError(lastAssistant);
const billingFailure = isBillingAssistantError(lastAssistant);
2026-01-14 01:08:15 +00:00
const failoverFailure = isFailoverAssistantError(lastAssistant);
const assistantFailoverReason = classifyFailoverReason(lastAssistant?.errorMessage ?? "");
2026-01-14 01:08:15 +00:00
const cloudCodeAssistFormatError = attempt.cloudCodeAssistFormatError;
2026-01-18 15:19:25 +00:00
const imageDimensionError = parseImageDimensionError(lastAssistant?.errorMessage ?? "");
if (imageDimensionError && lastProfileId) {
const details = [
imageDimensionError.messageIndex !== undefined
? `message=${imageDimensionError.messageIndex}`
: null,
imageDimensionError.contentIndex !== undefined
? `content=${imageDimensionError.contentIndex}`
: null,
imageDimensionError.maxDimensionPx !== undefined
? `limit=${imageDimensionError.maxDimensionPx}px`
: null,
]
.filter(Boolean)
.join(" ");
log.warn(
`Profile ${lastProfileId} rejected image payload${details ? ` (${details})` : ""}.`,
);
}
2026-01-14 01:08:15 +00:00
// Treat timeout as potential rate limit (Antigravity hangs on rate limit)
// But exclude post-prompt compaction timeouts (model succeeded; no profile issue)
const shouldRotate =
(!aborted && failoverFailure) || (timedOut && !timedOutDuringCompaction);
2026-01-14 01:08:15 +00:00
if (shouldRotate) {
if (lastProfileId) {
const reason =
timedOut || assistantFailoverReason === "timeout"
? "timeout"
: (assistantFailoverReason ?? "unknown");
await markAuthProfileFailure({
store: authStore,
profileId: lastProfileId,
reason,
cfg: params.config,
agentDir: params.agentDir,
});
2026-01-24 00:04:53 +00:00
if (timedOut && !isProbeSession) {
2026-01-14 01:08:15 +00:00
log.warn(
`Profile ${lastProfileId} timed out (possible rate limit). Trying next account...`,
);
}
if (cloudCodeAssistFormatError) {
log.warn(
`Profile ${lastProfileId} hit Cloud Code Assist format error. Tool calls will be sanitized on retry.`,
);
}
}
const rotated = await advanceAuthProfile();
if (rotated) {
continue;
}
2026-01-14 01:08:15 +00:00
if (fallbackConfigured) {
// Prefer formatted error message (user-friendly) over raw errorMessage
2026-01-14 01:08:15 +00:00
const message =
(lastAssistant
? formatAssistantErrorText(lastAssistant, {
cfg: params.config,
sessionKey: params.sessionKey ?? params.sessionId,
provider,
2026-01-14 01:08:15 +00:00
})
: undefined) ||
lastAssistant?.errorMessage?.trim() ||
2026-01-14 01:08:15 +00:00
(timedOut
? "LLM request timed out."
: rateLimitFailure
? "LLM request rate limited."
: billingFailure
? formatBillingErrorMessage(provider)
: authFailure
? "LLM request unauthorized."
: "LLM request failed.");
2026-01-14 01:08:15 +00:00
const status =
resolveFailoverStatus(assistantFailoverReason ?? "unknown") ??
(isTimeoutErrorMessage(message) ? 408 : undefined);
throw new FailoverError(message, {
reason: assistantFailoverReason ?? "unknown",
provider,
model: modelId,
profileId: lastProfileId,
status,
});
}
}
const usage = toNormalizedUsage(usageAccumulator);
// Extract the last individual API call's usage for context-window
// utilization display. The accumulated `usage` sums input tokens
// across all calls (tool-use loops, compaction retries), which
// overstates the actual context size. `lastCallUsage` reflects only
// the final call, giving an accurate snapshot of current context.
const lastCallUsage = normalizeUsage(lastAssistant?.usage as UsageLike);
const promptTokens = derivePromptTokens(lastRunPromptUsage);
2026-01-14 01:08:15 +00:00
const agentMeta: EmbeddedPiAgentMeta = {
sessionId: sessionIdUsed,
provider: lastAssistant?.provider ?? provider,
model: lastAssistant?.model ?? model.id,
usage,
lastCallUsage: lastCallUsage ?? undefined,
promptTokens,
compactionCount: autoCompactionCount > 0 ? autoCompactionCount : undefined,
2026-01-14 01:08:15 +00:00
};
const payloads = buildEmbeddedRunPayloads({
assistantTexts: attempt.assistantTexts,
toolMetas: attempt.toolMetas,
lastAssistant: attempt.lastAssistant,
lastToolError: attempt.lastToolError,
2026-01-14 01:08:15 +00:00
config: params.config,
sessionKey: params.sessionKey ?? params.sessionId,
provider,
2026-01-14 01:08:15 +00:00
verboseLevel: params.verboseLevel,
reasoningLevel: params.reasoningLevel,
toolResultFormat: resolvedToolResultFormat,
suppressToolErrorWarnings: params.suppressToolErrorWarnings,
inlineToolResultsAllowed: false,
2026-01-14 01:08:15 +00:00
});
// Timeout aborts can leave the run without any assistant payloads.
// Emit an explicit timeout error instead of silently completing, so
// callers do not lose the turn as an orphaned user message.
if (timedOut && !timedOutDuringCompaction && payloads.length === 0) {
return {
payloads: [
{
text:
"Request timed out before a response was generated. " +
"Please try again, or increase `agents.defaults.timeoutSeconds` in your config.",
isError: true,
},
],
meta: {
durationMs: Date.now() - started,
agentMeta,
aborted,
systemPromptReport: attempt.systemPromptReport,
},
didSendViaMessagingTool: attempt.didSendViaMessagingTool,
messagingToolSentTexts: attempt.messagingToolSentTexts,
messagingToolSentTargets: attempt.messagingToolSentTargets,
};
}
2026-01-14 01:08:15 +00:00
log.debug(
`embedded run done: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - started} aborted=${aborted}`,
);
if (lastProfileId) {
await markAuthProfileGood({
store: authStore,
provider,
profileId: lastProfileId,
agentDir: params.agentDir,
2026-01-14 01:08:15 +00:00
});
await markAuthProfileUsed({
store: authStore,
profileId: lastProfileId,
agentDir: params.agentDir,
2026-01-14 01:08:15 +00:00
});
}
return {
payloads: payloads.length ? payloads : undefined,
meta: {
durationMs: Date.now() - started,
agentMeta,
aborted,
2026-01-15 01:06:19 +00:00
systemPromptReport: attempt.systemPromptReport,
// Handle client tool calls (OpenResponses hosted tools)
stopReason: attempt.clientToolCall ? "tool_calls" : undefined,
pendingToolCalls: attempt.clientToolCall
? [
{
id: `call_${Date.now()}`,
name: attempt.clientToolCall.name,
arguments: JSON.stringify(attempt.clientToolCall.params),
},
]
: undefined,
2026-01-14 01:08:15 +00:00
},
didSendViaMessagingTool: attempt.didSendViaMessagingTool,
messagingToolSentTexts: attempt.messagingToolSentTexts,
messagingToolSentTargets: attempt.messagingToolSentTargets,
};
}
} finally {
process.chdir(prevCwd);
}
}),
);
}