@aexol/spectral 0.9.260 → 0.9.263

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,6 +31,7 @@
31
31
  * `compact_context`, and `subagent` remain excluded
32
32
  */
33
33
  import type { AgentEvent, AgentTool } from "../sdk/agent-core/index.js";
34
+ import type { AgentMessage } from "../sdk/agent-core/index.js";
34
35
  import { type ExtensionAPI, type ExtensionContext } from "../sdk/coding-agent/index.js";
35
36
  import { type AgentConfig } from "./agents.js";
36
37
  import type { ServerEvent } from "../server/wire.ts";
@@ -91,6 +92,31 @@ export declare function resolveSubagentMaxAttempts(explicit: number | undefined,
91
92
  enabled?: boolean;
92
93
  maxRetries?: number;
93
94
  } | undefined): number;
95
+ /**
96
+ * Deterministic compact-and-retry for a subagent context overflow.
97
+ *
98
+ * Builds a compacted message state from the child transcript: a deterministic
99
+ * DCP-lite summary of the older history plus a kept recent tail. The latest
100
+ * user message (the task prompt) is pinned so it survives verbatim. Returns the
101
+ * new message state, or `undefined` when there is nothing meaningful to
102
+ * compact (e.g. the transcript is just the task prompt) or the cut cannot be
103
+ * computed.
104
+ *
105
+ * No LLM calls — this is pure `prepareCompaction` + `compactDcpLite` over a
106
+ * synthetic session-entry list, so it is safe to run inline in the retry loop.
107
+ */
108
+ export declare function compactMessagesForOverflow(messages: AgentMessage[], contextWindow: number): AgentMessage[] | undefined;
109
+ /**
110
+ * Deterministic single-message size guard.
111
+ *
112
+ * If a SINGLE message's estimated tokens exceed ~50% of the model context
113
+ * window, its text content is truncated to a safe cap and a marker note is
114
+ * appended. This keeps the provider request sub-window even when one oversized
115
+ * message (e.g. a giant file read) would otherwise push the request over the
116
+ * limit and yield a silent 400. Purely local — no LLM calls. Returns the
117
+ * original array unchanged when no message needs truncation.
118
+ */
119
+ export declare function guardOversizedMessages(messages: AgentMessage[], contextWindow: number): AgentMessage[];
94
120
  export default function subagentExtension(ext: ExtensionAPI, opts?: {
95
121
  disabledAgents?: string[];
96
122
  emitSubagentEvent?: (event: ServerEvent) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAEX,UAAU,EAEV,SAAS,EAET,MAAM,4BAA4B,CAAC;AAoBpC,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,gBAAgB,EAKrB,MAAM,8BAA8B,CAAC;AAItC,OAAO,EACN,KAAK,WAAW,EAQhB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,uCAAuC,CAAC;AAS/C,OAAO,KAAK,EAAE,sBAAsB,EAAgB,MAAM,6CAA6C,CAAC;AAyGxG;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACxC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAC,GACpF,MAAM,CAYR;AAyND;;;GAGG;AACH,wBAAgB,oCAAoC,CACnD,cAAc,EAAE,gBAAgB,CAAC,gBAAgB,CAAC,GAChD,IAAI,CAAC,sBAAsB,EAAE,WAAW,GAAG,cAAc,CAAC,CAe5D;AAgCD,wBAAgB,oBAAoB,CACnC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,gBAAgB,EACrB,SAAS,CAAC,EAAE,MAAM,GAChB,SAAS,EAAE,CAkDb;AAyBD;;;;;;;;;;GAUG;AACH;;;;;;GAMG;AACH,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,6EAA6E;IAC7E,GAAG,EAAE,cAAc,CAAC;CACpB;AAED,8EAA8E;AAC9E,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,wBAAwB,CAAC;AAqF1E,2FAA2F;AAC3F,wBAAgB,+BAA+B,CAC9C,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GACtD,IAAI,CAEN;AAmBD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACzC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,QAAQ,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GAC9D,MAAM,CAiBR;AAwnBD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CACxC,GAAG,EAAE,YAAY,EACjB,IAAI,CAAC,EAAE;IACN,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,wBAAwB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAC3D,GACC,IAAI,CAwbN"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAEX,UAAU,EAEV,SAAS,EAET,MAAM,4BAA4B,CAAC;AAOpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAwB/D,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,gBAAgB,EAKrB,MAAM,8BAA8B,CAAC;AAItC,OAAO,EACN,KAAK,WAAW,EAQhB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,uCAAuC,CAAC;AAS/C,OAAO,KAAK,EAAE,sBAAsB,EAAgB,MAAM,6CAA6C,CAAC;AA+HxG;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACxC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAC,GACpF,MAAM,CAeR;AAyND;;;GAGG;AACH,wBAAgB,oCAAoC,CACnD,cAAc,EAAE,gBAAgB,CAAC,gBAAgB,CAAC,GAChD,IAAI,CAAC,sBAAsB,EAAE,WAAW,GAAG,cAAc,CAAC,CAe5D;AAgCD,wBAAgB,oBAAoB,CACnC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,gBAAgB,EACrB,SAAS,CAAC,EAAE,MAAM,GAChB,SAAS,EAAE,CAkDb;AAyBD;;;;;;;;;;GAUG;AACH;;;;;;GAMG;AACH,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,6EAA6E;IAC7E,GAAG,EAAE,cAAc,CAAC;CACpB;AAED,8EAA8E;AAC9E,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,wBAAwB,CAAC;AAqF1E,2FAA2F;AAC3F,wBAAgB,+BAA+B,CAC9C,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GACtD,IAAI,CAEN;AAmBD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACzC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,QAAQ,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GAC9D,MAAM,CAiBR;AAsBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CACzC,QAAQ,EAAE,YAAY,EAAE,EACxB,aAAa,EAAE,MAAM,GACnB,YAAY,EAAE,GAAG,SAAS,CAyE5B;AASD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACrC,QAAQ,EAAE,YAAY,EAAE,EACxB,aAAa,EAAE,MAAM,GACnB,YAAY,EAAE,CAchB;AA8tBD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CACxC,GAAG,EAAE,YAAY,EACjB,IAAI,CAAC,EAAE;IACN,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,wBAAwB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAC3D,GACC,IAAI,CAwbN"}
@@ -32,6 +32,9 @@
32
32
  */
33
33
  import { agentLoop, agentLoopContinue, } from "../sdk/agent-core/index.js";
34
34
  import { buildIsolatedRequestTimePrunedMessagesResult, COMPACT_CONTEXT_TOOL_NAME, } from "../memory/tools/compact-context.js";
35
+ import { compactDcpLite, prepareCompaction, DEFAULT_COMPACTION_SETTINGS, } from "../sdk/coding-agent/core/compaction/index.js";
36
+ import { estimateTokens } from "../sdk/coding-agent/core/compaction/tokens.js";
37
+ import { createCompactionSummaryMessage, convertToLlm, } from "../sdk/coding-agent/core/messages.js";
35
38
  import { isRateLimitError, isTransientApiError, isContextOverflow, isPaymentRequiredError, streamSimple, } from "../sdk/ai/index.js";
36
39
  import { RETRY_DEFAULT_BASE_DELAY_MS, RETRY_DEFAULT_MAX_RETRIES, RETRY_MAX_BACKOFF_DELAY_MS, } from "../sdk/coding-agent/index.js";
37
40
  import { mutateSystemPrompt } from "../sdk/coding-agent/core/system-prompt-mutator.js";
@@ -136,6 +139,25 @@ function compactProgressMessage(value) {
136
139
  return value === undefined ? undefined : "Tool progress updated";
137
140
  }
138
141
  const PRIMARY_AGENT_BLOCK_RE = /(?:^|\n)(?:# Primary Agent: [^\n]*\n(?:\n[^\n]*)*?)(?:\n\n---\n\n|$)/;
142
+ /**
143
+ * Strip `compact_context` tool-description lines from a base system prompt.
144
+ *
145
+ * Subagents exclude the `compact_context` tool (see SUBAGENT_EXCLUDED_TOOL_NAMES),
146
+ * so its promptSnippet (an "Available tools" bullet) and its promptGuidelines
147
+ * ("Guidelines" bullets) must not leak into the subagent's prompt — they would
148
+ * describe a tool the subagent cannot call. Both are `- ` bullets that mention
149
+ * the tool name, so a line-based filter is a clean deterministic anchor. Only
150
+ * the subagent base prompt is touched; the main-session prompt is unaffected.
151
+ */
152
+ function stripCompactContextFromPrompt(prompt) {
153
+ if (!prompt.includes(COMPACT_CONTEXT_TOOL_NAME))
154
+ return prompt;
155
+ return prompt
156
+ .split("\n")
157
+ .filter((line) => !(line.trimStart().startsWith("- ") &&
158
+ line.includes(COMPACT_CONTEXT_TOOL_NAME)))
159
+ .join("\n");
160
+ }
139
161
  /**
140
162
  * Build the system prompt for a subagent session.
141
163
  *
@@ -151,10 +173,13 @@ export function buildSubagentSystemPrompt(baseSystemPrompt, agent) {
151
173
  agentMetadataBlock(agent);
152
174
  if (!baseSystemPrompt)
153
175
  return subagentBlock;
154
- const stripped = baseSystemPrompt.replace(PRIMARY_AGENT_BLOCK_RE, "");
155
- if (stripped === baseSystemPrompt) {
176
+ // Drop the excluded compact_context tool's description lines before any
177
+ // further shaping, so the subagent never sees a tool it cannot call.
178
+ const base = stripCompactContextFromPrompt(baseSystemPrompt);
179
+ const stripped = base.replace(PRIMARY_AGENT_BLOCK_RE, "");
180
+ if (stripped === base) {
156
181
  // No existing primary block: inject the subagent block below the base.
157
- return `${baseSystemPrompt}\n\n---\n\n${subagentBlock}`;
182
+ return `${base}\n\n---\n\n${subagentBlock}`;
158
183
  }
159
184
  const remainder = stripped.replace(/^\n+|\n+$/g, "");
160
185
  return remainder ? `${remainder}\n\n---\n\n${subagentBlock}` : subagentBlock;
@@ -477,6 +502,147 @@ export function resolveSubagentMaxAttempts(explicit, settings) {
477
502
  return 1 + retries;
478
503
  }
479
504
  // ---------------------------------------------------------------------------
505
+ // Overflow recovery — deterministic DCP-lite compact-and-retry
506
+ // ---------------------------------------------------------------------------
507
+ /**
508
+ * Max compact-and-retry attempts on a context overflow. This budget is
509
+ * SEPARATE from the transient-error retry budget (`maxSegments`): an overflow
510
+ * is not a transient provider failure, so recovering it must not consume the
511
+ * transient retries (and vice versa).
512
+ */
513
+ const MAX_OVERFLOW_COMPACT_RETRIES = 2;
514
+ /**
515
+ * Recent-tail keep budget for an overflow compaction: `max(12_000, 10% of the
516
+ * model context window)` tokens, mirroring the main session's auto
517
+ * older-history keep-recent floor.
518
+ */
519
+ const OVERFLOW_KEEP_RECENT_TOKENS_FLOOR = 12_000;
520
+ const OVERFLOW_KEEP_RECENT_WINDOW_RATIO = 0.1;
521
+ /**
522
+ * Deterministic compact-and-retry for a subagent context overflow.
523
+ *
524
+ * Builds a compacted message state from the child transcript: a deterministic
525
+ * DCP-lite summary of the older history plus a kept recent tail. The latest
526
+ * user message (the task prompt) is pinned so it survives verbatim. Returns the
527
+ * new message state, or `undefined` when there is nothing meaningful to
528
+ * compact (e.g. the transcript is just the task prompt) or the cut cannot be
529
+ * computed.
530
+ *
531
+ * No LLM calls — this is pure `prepareCompaction` + `compactDcpLite` over a
532
+ * synthetic session-entry list, so it is safe to run inline in the retry loop.
533
+ */
534
+ export function compactMessagesForOverflow(messages, contextWindow) {
535
+ // Drop trailing failed assistant message(s) so the compaction summarizes
536
+ // real history, not the overflow error turn the child just produced.
537
+ const working = [...messages];
538
+ while (working.length > 0 &&
539
+ working[working.length - 1].role === "assistant") {
540
+ working.pop();
541
+ }
542
+ // Need at least a summarizable prefix plus a kept tail to be useful.
543
+ if (working.length < 2)
544
+ return undefined;
545
+ const entries = working.map((message, index) => ({
546
+ type: "message",
547
+ id: `subagent-compact-${index}`,
548
+ parentId: index > 0 ? `subagent-compact-${index - 1}` : null,
549
+ timestamp: new Date(index).toISOString(),
550
+ message,
551
+ }));
552
+ const keepRecentTokens = Math.max(OVERFLOW_KEEP_RECENT_TOKENS_FLOOR, Math.floor((contextWindow ?? 0) * OVERFLOW_KEEP_RECENT_WINDOW_RATIO));
553
+ const settings = {
554
+ ...DEFAULT_COMPACTION_SETTINGS,
555
+ keepRecentTokens,
556
+ };
557
+ // No `pinLatestUserMessage`: in a subagent the task prompt is the FIRST (and
558
+ // usually only) user message, so pinning it would pull the cut to the start
559
+ // and summarize nothing. The task prompt is preserved verbatim below instead.
560
+ const preparation = prepareCompaction(entries, settings, {
561
+ phaseBoundary: false,
562
+ });
563
+ if (!preparation)
564
+ return undefined;
565
+ // Nothing to summarize (the whole transcript is the recent tail) → the
566
+ // compaction would not shrink the request, so there is nothing to recover.
567
+ // A split-turn cut summarizes via `turnPrefixMessages` instead of
568
+ // `messagesToSummarize`, so both must be empty to be a true no-op.
569
+ if (preparation.messagesToSummarize.length === 0 &&
570
+ preparation.turnPrefixMessages.length === 0) {
571
+ return undefined;
572
+ }
573
+ const compactResult = compactDcpLite(preparation);
574
+ const firstKeptIndex = entries.findIndex((entry) => entry.id === compactResult.firstKeptEntryId);
575
+ const keptTail = working.slice(firstKeptIndex >= 0 ? firstKeptIndex : 0);
576
+ if (keptTail.length === 0)
577
+ return undefined;
578
+ const summaryMessage = createCompactionSummaryMessage(compactResult.summary, compactResult.tokensBefore, new Date().toISOString());
579
+ // Preserve the task prompt (first user message) verbatim if the cut
580
+ // summarized it away — the DCP-lite summary only keeps a bounded excerpt,
581
+ // and the goal must survive compaction intact.
582
+ const firstUserIndex = working.findIndex((m) => m.role === "user");
583
+ const taskPrompt = firstUserIndex >= 0 && firstKeptIndex > firstUserIndex
584
+ ? working[firstUserIndex]
585
+ : undefined;
586
+ return taskPrompt
587
+ ? [taskPrompt, summaryMessage, ...keptTail]
588
+ : [summaryMessage, ...keptTail];
589
+ }
590
+ /** Fraction of the window a single message may occupy before the guard cuts it. */
591
+ const MAX_SINGLE_MESSAGE_WINDOW_RATIO = 0.5;
592
+ /** Target size (fraction of the window) a truncated message is cut to. */
593
+ const TRUNCATED_MESSAGE_TARGET_WINDOW_RATIO = 0.4;
594
+ /** Rough chars-per-token heuristic for turning a token budget into a char cap. */
595
+ const CHARS_PER_TOKEN = 4;
596
+ /**
597
+ * Deterministic single-message size guard.
598
+ *
599
+ * If a SINGLE message's estimated tokens exceed ~50% of the model context
600
+ * window, its text content is truncated to a safe cap and a marker note is
601
+ * appended. This keeps the provider request sub-window even when one oversized
602
+ * message (e.g. a giant file read) would otherwise push the request over the
603
+ * limit and yield a silent 400. Purely local — no LLM calls. Returns the
604
+ * original array unchanged when no message needs truncation.
605
+ */
606
+ export function guardOversizedMessages(messages, contextWindow) {
607
+ if (!Number.isFinite(contextWindow) || contextWindow <= 0)
608
+ return messages;
609
+ const thresholdTokens = contextWindow * MAX_SINGLE_MESSAGE_WINDOW_RATIO;
610
+ const targetChars = Math.floor(contextWindow * TRUNCATED_MESSAGE_TARGET_WINDOW_RATIO * CHARS_PER_TOKEN);
611
+ let changed = false;
612
+ const guarded = messages.map((message) => {
613
+ if (estimateTokens(message) <= thresholdTokens)
614
+ return message;
615
+ changed = true;
616
+ return truncateMessageToSafeCap(message, targetChars);
617
+ });
618
+ return changed ? guarded : messages;
619
+ }
620
+ /**
621
+ * Truncate a message's text content to `targetChars`, appending a marker note.
622
+ * Non-text parts (images, tool calls) are preserved. Returns the message
623
+ * unchanged when its text is already within the cap.
624
+ */
625
+ function truncateMessageToSafeCap(message, targetChars) {
626
+ const rawContent = message.content;
627
+ const parts = Array.isArray(rawContent)
628
+ ? rawContent
629
+ : typeof rawContent === "string"
630
+ ? [{ type: "text", text: rawContent }]
631
+ : [];
632
+ const textParts = parts.filter((part) => part.type === "text" && typeof part.text === "string");
633
+ const totalText = textParts.map((part) => part.text).join("\n");
634
+ if (totalText.length <= targetChars)
635
+ return message;
636
+ const omitted = totalText.length - targetChars;
637
+ const truncatedText = totalText.slice(0, targetChars) +
638
+ `\n[spectral: truncated ${omitted} chars to fit context window]`;
639
+ const nonTextParts = parts.filter((part) => part.type !== "text");
640
+ return {
641
+ ...message,
642
+ content: [{ type: "text", text: truncatedText }, ...nonTextParts],
643
+ };
644
+ }
645
+ // ---------------------------------------------------------------------------
480
646
  // Core — run a single subagent in-process via agentLoop
481
647
  // ---------------------------------------------------------------------------
482
648
  async function runSingleAgent(defaultCwd, agents, activePrimary, agentName, task, cwd, step, signal, onUpdate, makeDetails, parentToolCallId, onInternalEvent, ext, ctx, maxRetries) {
@@ -662,23 +828,39 @@ async function runSingleAgent(defaultCwd, agents, activePrimary, agentName, task
662
828
  model,
663
829
  apiKey: auth.apiKey,
664
830
  headers: auth.headers,
665
- convertToLlm: (msgs) => msgs.filter((m) => m.role === "user" || m.role === "assistant" || m.role === "toolResult"),
831
+ // Full converter so a compact-and-retry's `compactionSummary` message
832
+ // is sent to the LLM (the old role filter would have silently dropped
833
+ // it, defeating the compaction). Standard user/assistant/toolResult
834
+ // messages pass through unchanged.
835
+ convertToLlm: (msgs) => convertToLlm(msgs),
666
836
  toolExecution: "parallel",
667
837
  transformContext: async (messages) => {
668
838
  // Pure request-time DCP adapter: policy-derived, invocation-local, and
669
839
  // never connected to the parent Runtime or SessionManager.
840
+ let transformed = messages;
670
841
  try {
671
842
  const pruned = buildIsolatedRequestTimePrunedMessagesResult(messages, undefined, undefined, isolatedPruneFrozen, model.contextWindow);
672
843
  isolatedPruneFrozen = pruned.frozen;
673
- return pruned.messages;
844
+ transformed = pruned.messages;
845
+ }
846
+ catch {
847
+ transformed = messages;
848
+ }
849
+ // Deterministic single-message guard: if one message is ~50%+ of
850
+ // the window, truncate it so the provider gets a sub-window request
851
+ // instead of a silent 400. Request-time only (no mutation, no LLM).
852
+ try {
853
+ transformed = guardOversizedMessages(transformed, model.contextWindow);
674
854
  }
675
855
  catch {
676
- return messages;
856
+ // Leave the (already pruned) messages as-is on an unexpected
857
+ // guard failure — never block the request on the guard.
677
858
  }
859
+ return transformed;
678
860
  },
679
861
  afterToolCall: async ({ toolCall, args, result, isError }) => {
680
862
  try {
681
- const content = compressSubagentToolResult(toolCall.name, args, result, ctx);
863
+ const content = compressSubagentToolResult(toolCall.name, args, result, ctx, isError);
682
864
  return content ? { content, details: result.details, isError, terminate: result.terminate } : undefined;
683
865
  }
684
866
  catch {
@@ -702,6 +884,10 @@ async function runSingleAgent(defaultCwd, agents, activePrimary, agentName, task
702
884
  const collectedMessages = [];
703
885
  let hadError = false;
704
886
  let segments = 0;
887
+ // Compact-and-retry budget for context overflow. Separate from the
888
+ // transient-error retry budget (`maxSegments`) so recovering an overflow
889
+ // does not consume transient retries (and vice versa).
890
+ let overflowCompactions = 0;
705
891
  // Per-LLM-round timing stamps (TPS contract), mirroring the bridge's
706
892
  // main-session `pending` stamps: message_start opens the round, the
707
893
  // first text/thinking delta ends the TTFT window, message_end closes it.
@@ -854,8 +1040,6 @@ async function runSingleAgent(defaultCwd, agents, activePrimary, agentName, task
854
1040
  // Stream result error — already handled by message events
855
1041
  }
856
1042
  // ── Run-level retry decision (mirrors AgentSession._prepareRetry) ──
857
- if (segments >= maxSegments)
858
- break;
859
1043
  if (currentResult.stopReason !== "error")
860
1044
  break;
861
1045
  // Abort after a completed segment (parent stop or budget timeout):
@@ -867,13 +1051,55 @@ async function runSingleAgent(defaultCwd, agents, activePrimary, agentName, task
867
1051
  // Explicit for parity with AgentSession._isRetryableError.
868
1052
  if (isPaymentRequiredError(errorMessage))
869
1053
  break;
870
- // Context overflow is a hard failure, not transient. It must be
871
- // checked before the classifiers because overflow messages can
872
- // contain bare status-code-like numerals (e.g. "501234 tokens >
873
- // 500000 maximum").
1054
+ // Context overflow is NOT transient — it is recovered with a
1055
+ // deterministic DCP-lite compact-and-retry on a SEPARATE budget
1056
+ // (`overflowCompactions`), so it does not consume the transient
1057
+ // retry budget (`maxSegments`). Checked before the transient
1058
+ // classifiers because overflow messages can contain bare
1059
+ // status-code-like numerals (e.g. "501234 tokens > 500000 maximum").
874
1060
  if (isContextOverflow(buildOverflowProbe(errorMessage), model.contextWindow)) {
875
- break;
1061
+ if (overflowCompactions >= MAX_OVERFLOW_COMPACT_RETRIES) {
1062
+ // Second overflow after compaction: recovery is exhausted.
1063
+ currentResult.stopReason = "error";
1064
+ const message = "Context overflow recovery failed after compact-and-retry attempts. Try reducing context or switching to a larger-context model.";
1065
+ currentResult.errorMessage = message;
1066
+ currentResult.stderr = message;
1067
+ break;
1068
+ }
1069
+ let compacted;
1070
+ try {
1071
+ compacted = compactMessagesForOverflow(collectedMessages, model.contextWindow);
1072
+ }
1073
+ catch (err) {
1074
+ console.error(`[subagent] overflow compaction failed: ${err instanceof Error ? err.message : String(err)}`);
1075
+ compacted = undefined;
1076
+ }
1077
+ if (!compacted) {
1078
+ // Nothing compactable (e.g. only the task prompt) or the cut
1079
+ // could not be computed: hard-fail with a clear message.
1080
+ currentResult.stopReason = "error";
1081
+ const message = "Context overflow: no compactable history to summarize. Try reducing context or switching to a larger-context model.";
1082
+ currentResult.errorMessage = message;
1083
+ currentResult.stderr = message;
1084
+ break;
1085
+ }
1086
+ // Apply the compacted state and resume on the next iteration via
1087
+ // agentLoopContinue (the last kept message is a toolResult/user).
1088
+ collectedMessages.length = 0;
1089
+ for (const msg of compacted)
1090
+ collectedMessages.push(msg);
1091
+ currentResult.messages = [...collectedMessages];
1092
+ // Reset the frozen request-time prune so the new (smaller)
1093
+ // prefix re-freezes and stays byte-stable within the resumed run.
1094
+ isolatedPruneFrozen = null;
1095
+ overflowCompactions += 1;
1096
+ continue;
876
1097
  }
1098
+ // Transient provider error: bounded by the maxSegments retry budget.
1099
+ // Compact-and-retry segments are excluded so overflow recovery does
1100
+ // not consume the transient budget.
1101
+ if (segments - overflowCompactions >= maxSegments)
1102
+ break;
877
1103
  if (!isRateLimitError(errorMessage) && !isTransientApiError(errorMessage)) {
878
1104
  break;
879
1105
  }
@@ -1 +1 @@
1
- {"version":3,"file":"const.d.ts","sourceRoot":"","sources":["../../../src/generated/zeus/const.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAC,GAAG,CA47B5C,CAAA;AAED,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAC,GAAG,CA24C1C,CAAA;AAED,eAAO,MAAM,GAAG;;;;CAIf,CAAA"}
1
+ {"version":3,"file":"const.d.ts","sourceRoot":"","sources":["../../../src/generated/zeus/const.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAC,GAAG,CA47B5C,CAAA;AAED,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAC,GAAG,CAg5C1C,CAAA;AAED,eAAO,MAAM,GAAG;;;;CAIf,CAAA"}
@@ -91,8 +91,8 @@ export const AllTypesProps = {
91
91
  UpdateLeadInput: {
92
92
  leadType: "LeadType"
93
93
  },
94
- IngestCloudDocumentInput: {},
95
94
  UpdateCloudDocumentInput: {},
95
+ RequestCloudDocumentUploadInput: {},
96
96
  InferenceStatus: "enum",
97
97
  RegisterInput: {},
98
98
  LoginInput: {},
@@ -319,8 +319,8 @@ export const AllTypesProps = {
319
319
  input: "GenerateAexolFromTextInput"
320
320
  },
321
321
  cancelFromTask: {},
322
- ingestCloudDocument: {
323
- input: "IngestCloudDocumentInput"
322
+ requestCloudDocumentUpload: {
323
+ input: "RequestCloudDocumentUploadInput"
324
324
  },
325
325
  updateCloudDocument: {
326
326
  input: "UpdateCloudDocumentInput"
@@ -967,6 +967,11 @@ export const ReturnTypes = {
967
967
  score: "Float",
968
968
  content: "String"
969
969
  },
970
+ CloudDocumentUpload: {
971
+ id: "ID",
972
+ putUrl: "String",
973
+ expiresAt: "DateTime"
974
+ },
970
975
  TokenUsage: {
971
976
  inputTokens: "Long",
972
977
  outputTokens: "Long",
@@ -1199,7 +1204,7 @@ export const ReturnTypes = {
1199
1204
  deleteProjectFile: "Boolean",
1200
1205
  generateAexolFromText: "FromTask",
1201
1206
  cancelFromTask: "Boolean",
1202
- ingestCloudDocument: "CloudDocument",
1207
+ requestCloudDocumentUpload: "CloudDocumentUpload",
1203
1208
  updateCloudDocument: "CloudDocument",
1204
1209
  deleteCloudDocument: "Boolean",
1205
1210
  cleanupOrphanCloudChunks: "Int",
@@ -940,15 +940,6 @@ export type ValueTypes = {
940
940
  __typename?: boolean | `@${string}`;
941
941
  ['...on CloudSearchResult']?: Omit<ValueTypes["CloudSearchResult"], "...on CloudSearchResult">;
942
942
  }>;
943
- ["IngestCloudDocumentInput"]: {
944
- teamId: ValueTypes["ID"] | Variable<any, string>;
945
- projectId: ValueTypes["ID"] | Variable<any, string>;
946
- name: string | Variable<any, string>;
947
- contentType: string | Variable<any, string>;
948
- content: string | Variable<any, string>;
949
- metadata?: string | undefined | null | Variable<any, string>;
950
- tags?: string | undefined | null | Variable<any, string>;
951
- };
952
943
  ["UpdateCloudDocumentInput"]: {
953
944
  name?: string | undefined | null | Variable<any, string>;
954
945
  contentType?: string | undefined | null | Variable<any, string>;
@@ -956,6 +947,22 @@ export type ValueTypes = {
956
947
  metadata?: string | undefined | null | Variable<any, string>;
957
948
  tags?: string | undefined | null | Variable<any, string>;
958
949
  };
950
+ ["RequestCloudDocumentUploadInput"]: {
951
+ teamId: ValueTypes["ID"] | Variable<any, string>;
952
+ projectId: ValueTypes["ID"] | Variable<any, string>;
953
+ name: string | Variable<any, string>;
954
+ contentType: string | Variable<any, string>;
955
+ contentLength?: number | undefined | null | Variable<any, string>;
956
+ metadata?: string | undefined | null | Variable<any, string>;
957
+ tags?: string | undefined | null | Variable<any, string>;
958
+ };
959
+ ["CloudDocumentUpload"]: AliasType<{
960
+ id?: boolean | `@${string}`;
961
+ putUrl?: boolean | `@${string}`;
962
+ expiresAt?: boolean | `@${string}`;
963
+ __typename?: boolean | `@${string}`;
964
+ ['...on CloudDocumentUpload']?: Omit<ValueTypes["CloudDocumentUpload"], "...on CloudDocumentUpload">;
965
+ }>;
959
966
  ["InferenceStatus"]: InferenceStatus;
960
967
  ["TokenUsage"]: AliasType<{
961
968
  inputTokens?: boolean | `@${string}`;
@@ -1627,9 +1634,9 @@ export type ValueTypes = {
1627
1634
  cancelFromTask?: [{
1628
1635
  taskId: ValueTypes["ID"] | Variable<any, string>;
1629
1636
  }, boolean | `@${string}`];
1630
- ingestCloudDocument?: [{
1631
- input: ValueTypes["IngestCloudDocumentInput"] | Variable<any, string>;
1632
- }, ValueTypes["CloudDocument"]];
1637
+ requestCloudDocumentUpload?: [{
1638
+ input: ValueTypes["RequestCloudDocumentUploadInput"] | Variable<any, string>;
1639
+ }, ValueTypes["CloudDocumentUpload"]];
1633
1640
  updateCloudDocument?: [{
1634
1641
  id: ValueTypes["ID"] | Variable<any, string>;
1635
1642
  teamId: ValueTypes["ID"] | Variable<any, string>;
@@ -3767,15 +3774,6 @@ export type ResolverInputTypes = {
3767
3774
  content?: boolean | `@${string}`;
3768
3775
  __typename?: boolean | `@${string}`;
3769
3776
  }>;
3770
- ["IngestCloudDocumentInput"]: {
3771
- teamId: ResolverInputTypes["ID"];
3772
- projectId: ResolverInputTypes["ID"];
3773
- name: string;
3774
- contentType: string;
3775
- content: string;
3776
- metadata?: string | undefined | null;
3777
- tags?: string | undefined | null;
3778
- };
3779
3777
  ["UpdateCloudDocumentInput"]: {
3780
3778
  name?: string | undefined | null;
3781
3779
  contentType?: string | undefined | null;
@@ -3783,6 +3781,21 @@ export type ResolverInputTypes = {
3783
3781
  metadata?: string | undefined | null;
3784
3782
  tags?: string | undefined | null;
3785
3783
  };
3784
+ ["RequestCloudDocumentUploadInput"]: {
3785
+ teamId: ResolverInputTypes["ID"];
3786
+ projectId: ResolverInputTypes["ID"];
3787
+ name: string;
3788
+ contentType: string;
3789
+ contentLength?: number | undefined | null;
3790
+ metadata?: string | undefined | null;
3791
+ tags?: string | undefined | null;
3792
+ };
3793
+ ["CloudDocumentUpload"]: AliasType<{
3794
+ id?: boolean | `@${string}`;
3795
+ putUrl?: boolean | `@${string}`;
3796
+ expiresAt?: boolean | `@${string}`;
3797
+ __typename?: boolean | `@${string}`;
3798
+ }>;
3786
3799
  ["InferenceStatus"]: InferenceStatus;
3787
3800
  ["TokenUsage"]: AliasType<{
3788
3801
  inputTokens?: boolean | `@${string}`;
@@ -4441,9 +4454,9 @@ export type ResolverInputTypes = {
4441
4454
  cancelFromTask?: [{
4442
4455
  taskId: ResolverInputTypes["ID"];
4443
4456
  }, boolean | `@${string}`];
4444
- ingestCloudDocument?: [{
4445
- input: ResolverInputTypes["IngestCloudDocumentInput"];
4446
- }, ResolverInputTypes["CloudDocument"]];
4457
+ requestCloudDocumentUpload?: [{
4458
+ input: ResolverInputTypes["RequestCloudDocumentUploadInput"];
4459
+ }, ResolverInputTypes["CloudDocumentUpload"]];
4447
4460
  updateCloudDocument?: [{
4448
4461
  id: ResolverInputTypes["ID"];
4449
4462
  teamId: ResolverInputTypes["ID"];
@@ -6476,21 +6489,26 @@ export type ModelTypes = {
6476
6489
  score: number;
6477
6490
  content: string;
6478
6491
  };
6479
- ["IngestCloudDocumentInput"]: {
6492
+ ["UpdateCloudDocumentInput"]: {
6493
+ name?: string | undefined | null;
6494
+ contentType?: string | undefined | null;
6495
+ content?: string | undefined | null;
6496
+ metadata?: string | undefined | null;
6497
+ tags?: string | undefined | null;
6498
+ };
6499
+ ["RequestCloudDocumentUploadInput"]: {
6480
6500
  teamId: ModelTypes["ID"];
6481
6501
  projectId: ModelTypes["ID"];
6482
6502
  name: string;
6483
6503
  contentType: string;
6484
- content: string;
6504
+ contentLength?: number | undefined | null;
6485
6505
  metadata?: string | undefined | null;
6486
6506
  tags?: string | undefined | null;
6487
6507
  };
6488
- ["UpdateCloudDocumentInput"]: {
6489
- name?: string | undefined | null;
6490
- contentType?: string | undefined | null;
6491
- content?: string | undefined | null;
6492
- metadata?: string | undefined | null;
6493
- tags?: string | undefined | null;
6508
+ ["CloudDocumentUpload"]: {
6509
+ id: ModelTypes["ID"];
6510
+ putUrl: string;
6511
+ expiresAt?: ModelTypes["DateTime"] | undefined | null;
6494
6512
  };
6495
6513
  ["InferenceStatus"]: InferenceStatus;
6496
6514
  ["TokenUsage"]: {
@@ -6787,7 +6805,7 @@ export type ModelTypes = {
6787
6805
  deleteProjectFile: boolean;
6788
6806
  generateAexolFromText: ModelTypes["FromTask"];
6789
6807
  cancelFromTask: boolean;
6790
- ingestCloudDocument: ModelTypes["CloudDocument"];
6808
+ requestCloudDocumentUpload: ModelTypes["CloudDocumentUpload"];
6791
6809
  updateCloudDocument: ModelTypes["CloudDocument"];
6792
6810
  deleteCloudDocument: boolean;
6793
6811
  cleanupOrphanCloudChunks: number;
@@ -8548,21 +8566,28 @@ export type GraphQLTypes = {
8548
8566
  content: string;
8549
8567
  ['...on CloudSearchResult']: Omit<GraphQLTypes["CloudSearchResult"], "...on CloudSearchResult">;
8550
8568
  };
8551
- ["IngestCloudDocumentInput"]: {
8569
+ ["UpdateCloudDocumentInput"]: {
8570
+ name?: string | undefined | null;
8571
+ contentType?: string | undefined | null;
8572
+ content?: string | undefined | null;
8573
+ metadata?: string | undefined | null;
8574
+ tags?: string | undefined | null;
8575
+ };
8576
+ ["RequestCloudDocumentUploadInput"]: {
8552
8577
  teamId: GraphQLTypes["ID"];
8553
8578
  projectId: GraphQLTypes["ID"];
8554
8579
  name: string;
8555
8580
  contentType: string;
8556
- content: string;
8581
+ contentLength?: number | undefined | null;
8557
8582
  metadata?: string | undefined | null;
8558
8583
  tags?: string | undefined | null;
8559
8584
  };
8560
- ["UpdateCloudDocumentInput"]: {
8561
- name?: string | undefined | null;
8562
- contentType?: string | undefined | null;
8563
- content?: string | undefined | null;
8564
- metadata?: string | undefined | null;
8565
- tags?: string | undefined | null;
8585
+ ["CloudDocumentUpload"]: {
8586
+ __typename: "CloudDocumentUpload";
8587
+ id: GraphQLTypes["ID"];
8588
+ putUrl: string;
8589
+ expiresAt?: GraphQLTypes["DateTime"] | undefined | null;
8590
+ ['...on CloudDocumentUpload']: Omit<GraphQLTypes["CloudDocumentUpload"], "...on CloudDocumentUpload">;
8566
8591
  };
8567
8592
  ["InferenceStatus"]: InferenceStatus;
8568
8593
  ["TokenUsage"]: {
@@ -8886,7 +8911,7 @@ export type GraphQLTypes = {
8886
8911
  deleteProjectFile: boolean;
8887
8912
  generateAexolFromText: GraphQLTypes["FromTask"];
8888
8913
  cancelFromTask: boolean;
8889
- ingestCloudDocument: GraphQLTypes["CloudDocument"];
8914
+ requestCloudDocumentUpload: GraphQLTypes["CloudDocumentUpload"];
8890
8915
  updateCloudDocument: GraphQLTypes["CloudDocument"];
8891
8916
  deleteCloudDocument: boolean;
8892
8917
  cleanupOrphanCloudChunks: number;
@@ -10307,8 +10332,8 @@ type ZEUS_VARIABLES = {
10307
10332
  ["UpdateLeadStatusInput"]: ValueTypes["UpdateLeadStatusInput"];
10308
10333
  ["CreateLeadInput"]: ValueTypes["CreateLeadInput"];
10309
10334
  ["UpdateLeadInput"]: ValueTypes["UpdateLeadInput"];
10310
- ["IngestCloudDocumentInput"]: ValueTypes["IngestCloudDocumentInput"];
10311
10335
  ["UpdateCloudDocumentInput"]: ValueTypes["UpdateCloudDocumentInput"];
10336
+ ["RequestCloudDocumentUploadInput"]: ValueTypes["RequestCloudDocumentUploadInput"];
10312
10337
  ["InferenceStatus"]: ValueTypes["InferenceStatus"];
10313
10338
  ["RegisterInput"]: ValueTypes["RegisterInput"];
10314
10339
  ["LoginInput"]: ValueTypes["LoginInput"];