@pentoshi/clai 3.8.20 → 3.8.22

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.
Files changed (161) hide show
  1. package/README.md +8 -5
  2. package/dist/agent/confirm-port.d.ts +5 -2
  3. package/dist/agent/confirm-port.js +11 -5
  4. package/dist/agent/confirm-port.js.map +1 -1
  5. package/dist/agent/context-breakdown.d.ts +37 -0
  6. package/dist/agent/context-breakdown.js +145 -0
  7. package/dist/agent/context-breakdown.js.map +1 -0
  8. package/dist/agent/plan-decision.js +7 -3
  9. package/dist/agent/plan-decision.js.map +1 -1
  10. package/dist/agent/plan-tool.js +63 -20
  11. package/dist/agent/plan-tool.js.map +1 -1
  12. package/dist/agent/progress-pause-policy.d.ts +21 -0
  13. package/dist/agent/progress-pause-policy.js +28 -0
  14. package/dist/agent/progress-pause-policy.js.map +1 -0
  15. package/dist/agent/reliability-policy.d.ts +85 -0
  16. package/dist/agent/reliability-policy.js +158 -0
  17. package/dist/agent/reliability-policy.js.map +1 -0
  18. package/dist/agent/runner.js +352 -164
  19. package/dist/agent/runner.js.map +1 -1
  20. package/dist/agent/session-state.d.ts +11 -1
  21. package/dist/agent/session-state.js +22 -15
  22. package/dist/agent/session-state.js.map +1 -1
  23. package/dist/agent/stop-summary.js +3 -1
  24. package/dist/agent/stop-summary.js.map +1 -1
  25. package/dist/agent/task-evidence.js +13 -0
  26. package/dist/agent/task-evidence.js.map +1 -1
  27. package/dist/agent/tool-history.d.ts +12 -0
  28. package/dist/agent/tool-history.js +159 -0
  29. package/dist/agent/tool-history.js.map +1 -1
  30. package/dist/agent/tool-output-formatting.d.ts +4 -0
  31. package/dist/agent/tool-output-formatting.js +29 -4
  32. package/dist/agent/tool-output-formatting.js.map +1 -1
  33. package/dist/app/controllers/session-controller.d.ts +6 -0
  34. package/dist/app/controllers/session-controller.js +21 -1
  35. package/dist/app/controllers/session-controller.js.map +1 -1
  36. package/dist/app/ports/confirm-port.d.ts +1 -1
  37. package/dist/commands/providers.js +57 -26
  38. package/dist/commands/providers.js.map +1 -1
  39. package/dist/index.js +3 -3
  40. package/dist/index.js.map +1 -1
  41. package/dist/llm/gemini.js +4 -1
  42. package/dist/llm/gemini.js.map +1 -1
  43. package/dist/llm/key-rotation.d.ts +52 -0
  44. package/dist/llm/key-rotation.js +122 -0
  45. package/dist/llm/key-rotation.js.map +1 -0
  46. package/dist/llm/provider.d.ts +2 -0
  47. package/dist/llm/provider.js +8 -0
  48. package/dist/llm/provider.js.map +1 -1
  49. package/dist/llm/router.d.ts +15 -2
  50. package/dist/llm/router.js +318 -125
  51. package/dist/llm/router.js.map +1 -1
  52. package/dist/prompts/embedded.js +1 -1
  53. package/dist/prompts/embedded.js.map +1 -1
  54. package/dist/prompts/index.d.ts +25 -0
  55. package/dist/prompts/index.js +61 -7
  56. package/dist/prompts/index.js.map +1 -1
  57. package/dist/prompts/system.agent.md +3 -1
  58. package/dist/repl/slash-commands.js +14 -8
  59. package/dist/repl/slash-commands.js.map +1 -1
  60. package/dist/repl.d.ts +4 -1
  61. package/dist/repl.js +12 -2
  62. package/dist/repl.js.map +1 -1
  63. package/dist/store/config.d.ts +18 -0
  64. package/dist/store/config.js +9 -0
  65. package/dist/store/config.js.map +1 -1
  66. package/dist/store/history.d.ts +8 -0
  67. package/dist/store/history.js +38 -0
  68. package/dist/store/history.js.map +1 -1
  69. package/dist/store/keys.d.ts +55 -0
  70. package/dist/store/keys.js +226 -21
  71. package/dist/store/keys.js.map +1 -1
  72. package/dist/store/logs.js +35 -12
  73. package/dist/store/logs.js.map +1 -1
  74. package/dist/store/paths.d.ts +13 -0
  75. package/dist/store/paths.js +23 -1
  76. package/dist/store/paths.js.map +1 -1
  77. package/dist/store/session-workspace.d.ts +79 -0
  78. package/dist/store/session-workspace.js +199 -0
  79. package/dist/store/session-workspace.js.map +1 -0
  80. package/dist/tools/definitions.js +9 -1
  81. package/dist/tools/definitions.js.map +1 -1
  82. package/dist/tools/http.d.ts +8 -0
  83. package/dist/tools/http.js +86 -2
  84. package/dist/tools/http.js.map +1 -1
  85. package/dist/tools/pentest-workflows.js +38 -19
  86. package/dist/tools/pentest-workflows.js.map +1 -1
  87. package/dist/tools/registry.js +25 -2
  88. package/dist/tools/registry.js.map +1 -1
  89. package/dist/tools/shell.d.ts +1 -1
  90. package/dist/tools/web/search.js +22 -3
  91. package/dist/tools/web/search.js.map +1 -1
  92. package/dist/tui/format-keys.js +29 -6
  93. package/dist/tui/format-keys.js.map +1 -1
  94. package/dist/tui-v2/app/command-handlers.js +1 -2
  95. package/dist/tui-v2/app/command-handlers.js.map +1 -1
  96. package/dist/tui-v2/app/commands/config-commands.d.ts +1 -2
  97. package/dist/tui-v2/app/commands/config-commands.js +1 -11
  98. package/dist/tui-v2/app/commands/config-commands.js.map +1 -1
  99. package/dist/tui-v2/app/commands/key-commands.d.ts +1 -1
  100. package/dist/tui-v2/app/commands/key-commands.js +148 -63
  101. package/dist/tui-v2/app/commands/key-commands.js.map +1 -1
  102. package/dist/tui-v2/app/commands/picker-commands.js +6 -0
  103. package/dist/tui-v2/app/commands/picker-commands.js.map +1 -1
  104. package/dist/tui-v2/bootstrap/composition-root.js +8 -5
  105. package/dist/tui-v2/bootstrap/composition-root.js.map +1 -1
  106. package/dist/tui-v2/bootstrap/overlay-ports.js +7 -2
  107. package/dist/tui-v2/bootstrap/overlay-ports.js.map +1 -1
  108. package/dist/tui-v2/components/modal/keys-modal.d.ts +19 -0
  109. package/dist/tui-v2/components/modal/keys-modal.js +203 -0
  110. package/dist/tui-v2/components/modal/keys-modal.js.map +1 -0
  111. package/dist/tui-v2/components/overlay/overlay-host.js +6 -2
  112. package/dist/tui-v2/components/overlay/overlay-host.js.map +1 -1
  113. package/dist/tui-v2/components/pager/pager-line.js +5 -2
  114. package/dist/tui-v2/components/pager/pager-line.js.map +1 -1
  115. package/dist/tui-v2/components/pager/pager.js +6 -3
  116. package/dist/tui-v2/components/pager/pager.js.map +1 -1
  117. package/dist/tui-v2/components/plan/use-pane-presence.d.ts +6 -4
  118. package/dist/tui-v2/components/plan/use-pane-presence.js +8 -45
  119. package/dist/tui-v2/components/plan/use-pane-presence.js.map +1 -1
  120. package/dist/tui-v2/components/transcript/assistant-message.js +4 -1
  121. package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -1
  122. package/dist/tui-v2/components/transcript/compacted-row.js +1 -1
  123. package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -1
  124. package/dist/tui-v2/components/transcript/file-diff-card.js +3 -23
  125. package/dist/tui-v2/components/transcript/file-diff-card.js.map +1 -1
  126. package/dist/tui-v2/components/transcript/linkable-text.js +9 -2
  127. package/dist/tui-v2/components/transcript/linkable-text.js.map +1 -1
  128. package/dist/tui-v2/components/transcript/selectable-line.d.ts +22 -0
  129. package/dist/tui-v2/components/transcript/selectable-line.js +24 -0
  130. package/dist/tui-v2/components/transcript/selectable-line.js.map +1 -0
  131. package/dist/tui-v2/components/transcript/thinking-block.js +3 -1
  132. package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -1
  133. package/dist/tui-v2/components/transcript/tool-card.d.ts +5 -3
  134. package/dist/tui-v2/components/transcript/tool-card.js +39 -33
  135. package/dist/tui-v2/components/transcript/tool-card.js.map +1 -1
  136. package/dist/tui-v2/components/transcript/use-click-without-drag.js +4 -1
  137. package/dist/tui-v2/components/transcript/use-click-without-drag.js.map +1 -1
  138. package/dist/tui-v2/components/transcript/user-message.js +1 -1
  139. package/dist/tui-v2/components/transcript/user-message.js.map +1 -1
  140. package/dist/tui-v2/controllers/overlay-controller.d.ts +36 -0
  141. package/dist/tui-v2/controllers/overlay-controller.js +28 -0
  142. package/dist/tui-v2/controllers/overlay-controller.js.map +1 -1
  143. package/dist/tui-v2/rendering/batch-sections.d.ts +15 -4
  144. package/dist/tui-v2/rendering/batch-sections.js +157 -64
  145. package/dist/tui-v2/rendering/batch-sections.js.map +1 -1
  146. package/dist/tui-v2/rendering/format-help.js +1 -1
  147. package/dist/tui-v2/rendering/format-help.js.map +1 -1
  148. package/dist/tui-v2/rendering/open-tool-output.js +10 -7
  149. package/dist/tui-v2/rendering/open-tool-output.js.map +1 -1
  150. package/dist/tui-v2/rendering/pager-markdown.d.ts +9 -0
  151. package/dist/tui-v2/rendering/pager-markdown.js +46 -0
  152. package/dist/tui-v2/rendering/pager-markdown.js.map +1 -1
  153. package/dist/tui-v2/rendering/pager-view-policy.d.ts +6 -3
  154. package/dist/tui-v2/rendering/pager-view-policy.js +9 -13
  155. package/dist/tui-v2/rendering/pager-view-policy.js.map +1 -1
  156. package/dist/tui-v2/rendering/sanitize-display.js +3 -1
  157. package/dist/tui-v2/rendering/sanitize-display.js.map +1 -1
  158. package/dist/types.d.ts +7 -0
  159. package/dist/version.generated.d.ts +2 -2
  160. package/dist/version.generated.js +2 -2
  161. package/package.json +1 -1
@@ -8,13 +8,16 @@ import { randomUUID } from "node:crypto";
8
8
  import { jobManager } from "../tools/jobs.js";
9
9
  import { agentModeDirective, planModeDirective, renderAgentSystemPrompt, renderCompactAgentSystemPrompt, scratchDirFor, toolNudge, } from "../prompts/index.js";
10
10
  import { getConfig } from "../store/config.js";
11
+ import { beginSessionWorkspace, getActiveSessionWorkspace, } from "../store/session-workspace.js";
11
12
  import { groqInputTokenBudget } from "../llm/groq.js";
12
13
  import { classifyToolCall, isPentestToolCall, scopeHint, scopeTargetForToolCall, } from "../safety/classifier.js";
13
14
  import { availableToolNames, normalizeToolCall, runToolCall, BATCH_SAFE_TOOLS, } from "../tools/registry.js";
14
15
  import { getToolDefinitions, getCompactToolDefinitions, PLAN_TOOL_NAMES, } from "../tools/definitions.js";
15
- import { appendAssistantWithTools, appendToolResult, assertValidToolProtocol, fillMissingToolResults, } from "./tool-history.js";
16
+ import { appendAssistantWithTools, appendToolResult, assertValidToolProtocol, fillMissingToolResults, repairToolProtocol, } from "./tool-history.js";
16
17
  import { formatViewportHint, registerViewport } from "../ui/output-pane.js";
17
- import { compactMessagesWithSummary, estimateMessagesTokens, AUTO_COMPACT_TOKEN_BUDGET, shouldApplyAutoCompact, COMPACTION_MEMORY_PREFIX, PLAN_IMPLEMENT_MEMORY_PREFIX, isCompactionMemoryMessage, } from "./context-manager.js";
18
+ import { compactMessagesWithSummary, estimateMessagesTokens, shouldApplyAutoCompact, COMPACTION_MEMORY_PREFIX, PLAN_IMPLEMENT_MEMORY_PREFIX, isCompactionMemoryMessage, } from "./context-manager.js";
19
+ import { buildContextBreakdown, contextBreakdownAuditPayload, } from "./context-breakdown.js";
20
+ import { autoCompactTriggerTokens, dedupeToolContextOutput, freeTierGuardNotices, getReliabilityPolicy, resolveStepMaxTokens, } from "./reliability-policy.js";
18
21
  import { auditLog } from "../store/logs.js";
19
22
  import { loadProjectContext } from "../store/project.js";
20
23
  import { loadScope, isScopeActive } from "../store/scope.js";
@@ -34,6 +37,7 @@ import { fsWrite, isOutsideWorkingDirectory, resolveFsToolPath, } from "../tools
34
37
  import { stripSentinelTokens, parseToolCall, recognizeBareToolJson, looksLikeTruncatedToolCall, salvageTruncatedWrite, salvageTruncatedWriteFromNative, countToolFences, parseAllToolCalls, groupToolCallsForExecution, buildTurnHistory, collapseRepeatedText, textBeforeToolCall, formatToolArgs, looksLikePentestTask, looksLikeBuildTask, looksLikeInformationalQuery, looksLikeIdleOrSocialPrompt, looksLikeActionNarration, looksLikeWebActionNarration, looksLikePlanNarration, looksLikeErrorDiagnosisWithFixIntent, localHttpProbeIsFailure, localHttpProbeIsSuccess, requiresFreshWebSearch, freshnessGuardMessage, buildWorkflowDirective, narrowNmapOperationDirective, pentestWorkflowDirective, pentestNoLocalServerDirective, shouldDimToolChatter, looksLikePromptLeak, } from "./tool-call-parser.js";
35
38
  import { createSessionPolicy, isPreApprovalAllowedTool, isPlanModeAllowedShellCommand, isPlanModeAllowedTool, isPlanApprovedByStatus, planHasOpenWork, isAbortError, shouldEnableImageOcr, } from "./session-policy.js";
36
39
  import { saveToolOutput, summarizeOutput, formatToolContext, } from "./tool-output-formatting.js";
40
+ import { codingSessionFromContext, isProtocolPlaceholderOutput, progressPauseMode, } from "./progress-pause-policy.js";
37
41
  import { renderPlanForTerminal, planContextMessage, handlePlanTool, resolvePlanTaskId, } from "./plan-tool.js";
38
42
  import { absorbLooseWorkIntoLedger, applyDestinationCwd, canMarkTaskDone, hasLocalRuntimeProof, hasRemoteWorkProof, isDevServerCall, isEvidenceWorkTool, isFeatureImplementationCall, isPackageInstallCommand, isPlanPreflightTool, isPortListeningOutput, isReadOnlyReconTool, isRemoteActiveTestCall, isRemoteObservationTask, isRemoteReconToolCall, isRuntimeObservationTask, isScaffoldCreateCommand, isServerReadyOutput, ledgerFromTaskEvidence, pickPendingTaskForToolCall, recordTaskWorkSuccess, resolveUserDestinationHint, taskEvidenceFromLedger, TOOL_ABORT_GRACE_MS, toolHardBudgetMs, toolStallBudgetMs, userAskedForFeatureApp, } from "./task-evidence.js";
39
43
  import { buildSessionStateBlock, inferNextHint, upsertSessionStateMessage, } from "./session-state.js";
@@ -85,9 +89,17 @@ export async function runAgentTurn(prompt, options = {}) {
85
89
  if (/\/output\b|open full output|Ctrl\+O or|\.clai\/outputs/i.test(cleaned)) {
86
90
  return;
87
91
  }
88
- if (cleaned.length > 64) {
89
- const short = cleaned.match(/^[\w./-]+/);
90
- cleaned = short ? short[0] : cleaned.slice(0, 61) + "…";
92
+ // API key rotation / retry lines need more room than tool-name chips.
93
+ const keyLine = /^(using |switching |⏳ |all .+ API keys)/i.test(cleaned);
94
+ const maxLen = keyLine ? 96 : 64;
95
+ if (cleaned.length > maxLen) {
96
+ if (keyLine) {
97
+ cleaned = cleaned.slice(0, maxLen - 1) + "…";
98
+ }
99
+ else {
100
+ const short = cleaned.match(/^[\w./-]+/);
101
+ cleaned = short ? short[0] : cleaned.slice(0, maxLen - 3) + "…";
102
+ }
91
103
  }
92
104
  emit({ type: "status", text: cleaned || "working" });
93
105
  if (writesDirectly)
@@ -278,6 +290,11 @@ export async function runAgentTurn(prompt, options = {}) {
278
290
  };
279
291
  let lastAnswer = "";
280
292
  const session = options.session ?? createSessionPolicy();
293
+ // One-shot CLI / tests that never entered TUI/REPL still need an isolated
294
+ // scratch+output workspace. No-op when a session already bound one.
295
+ if (!getActiveSessionWorkspace()) {
296
+ beginSessionWorkspace();
297
+ }
281
298
  // Active plan context
282
299
  // If this session already has a plan, inject it so the model keeps it in
283
300
  // context. When the user has approved it (via /implement) we instruct the
@@ -323,11 +340,16 @@ export async function runAgentTurn(prompt, options = {}) {
323
340
  setActiveProjectRootIfValid(discoveredProjects[0]);
324
341
  }
325
342
  const buildSystemContent = (native) => {
343
+ const reliability = getReliabilityPolicy();
326
344
  const sections = [
327
345
  (useCompactSystemPrompt
328
346
  ? renderCompactAgentSystemPrompt
329
347
  : renderAgentSystemPrompt)(toolNames.join(", "), {
330
348
  nativeTools: native,
349
+ // E6: slim native constitution when API tool schemas are attached.
350
+ ...(native
351
+ ? { slimNative: reliability.slimNativePrompt }
352
+ : {}),
331
353
  }),
332
354
  ];
333
355
  if (projectContext) {
@@ -772,6 +794,15 @@ export async function runAgentTurn(prompt, options = {}) {
772
794
  const hasHistory = (options.history?.length ?? 0) > 0;
773
795
  const buildLike = buildLikeTurn;
774
796
  const pentestLike = looksLikePentestTask(prompt, options.history);
797
+ /** Coding/build sessions never hard-pause mid-turn for the progress governor. */
798
+ let codingSession = codingSessionFromContext({
799
+ buildLike,
800
+ planKind: activePlan?.kind,
801
+ });
802
+ let pauseMode = progressPauseMode({
803
+ codingSession,
804
+ autoConfirm: Boolean(options.autoConfirm),
805
+ });
775
806
  const continueExistingOutcome = /^(?:continue|resume|proceed|keep\s+going|finish|next)\b/i.test(prompt.trim()) ||
776
807
  Boolean(activePlan && !isPlanTerminal(activePlan));
777
808
  const outcomeState = await openOutcomeState({
@@ -1746,27 +1777,52 @@ export async function runAgentTurn(prompt, options = {}) {
1746
1777
  ((call.name === "shell.exec" || call.name === "shell.start") &&
1747
1778
  isPackageInstallCommand(String(call.args.command ?? "")));
1748
1779
  }
1749
- const governed = governProgress(governorState, "activity", {
1750
- evidenceDelta: newEvidence.length,
1751
- hypothesisDelta,
1752
- repetitionScore: loopGuard.getAttemptCount(call.name, call.args) > 1 ? 1 : 0,
1753
- policy: {
1754
- resourceEnvelope: Math.max(12, maxSteps),
1755
- emergencyCeiling: Math.max(70, maxSteps * 3),
1756
- reflectionAfterNoDelta: 3,
1757
- pauseAfterNoDelta: 6,
1758
- repetitionThreshold: 0.8,
1759
- },
1760
- });
1761
- governorState = governed.state;
1762
- if (governed.recommendation === "reflect") {
1763
- deferredPostToolMessages.push({
1764
- role: "system",
1765
- content: `PROGRESS GOVERNOR: ${governed.reason}. Reassess the current premise and choose the next action that can produce criterion-linked evidence.`,
1780
+ // Protocol-repair placeholders are not live work — never let them
1781
+ // accumulate into a mid-turn pause (they used to look like failed tools).
1782
+ if (!isProtocolPlaceholderOutput(result.output)) {
1783
+ const governed = governProgress(governorState, "activity", {
1784
+ evidenceDelta: newEvidence.length,
1785
+ hypothesisDelta,
1786
+ repetitionScore: loopGuard.getAttemptCount(call.name, call.args) > 1 ? 1 : 0,
1787
+ policy: {
1788
+ resourceEnvelope: Math.max(12, maxSteps),
1789
+ // Coding builds get a much higher ceiling; never use the tight
1790
+ // default that stopped multi-file scaffolds after a handful of steps.
1791
+ emergencyCeiling: codingSession
1792
+ ? Math.max(200, maxSteps * 5)
1793
+ : Math.max(70, maxSteps * 3),
1794
+ reflectionAfterNoDelta: codingSession ? 5 : 3,
1795
+ pauseAfterNoDelta: codingSession ? 24 : 6,
1796
+ repetitionThreshold: 0.8,
1797
+ },
1766
1798
  });
1767
- }
1768
- else if (governed.recommendation === "paused_budget") {
1769
- governorPauseReason = governed.reason;
1799
+ governorState = governed.state;
1800
+ if (governed.recommendation === "reflect") {
1801
+ deferredPostToolMessages.push({
1802
+ role: "system",
1803
+ content: `PROGRESS GOVERNOR: ${governed.reason}. Reassess the current premise and choose the next action that can produce criterion-linked evidence.` +
1804
+ (codingSession
1805
+ ? " Keep working — coding builds do not stop for a continue prompt."
1806
+ : ""),
1807
+ });
1808
+ }
1809
+ else if (governed.recommendation === "paused_budget") {
1810
+ if (pauseMode === "never") {
1811
+ // Soft reset so we do not re-trip every subsequent tool.
1812
+ governorState = {
1813
+ ...governed.state,
1814
+ consecutiveNoDelta: 0,
1815
+ };
1816
+ deferredPostToolMessages.push({
1817
+ role: "system",
1818
+ content: `PROGRESS GOVERNOR (soft, coding build): ${governed.reason}. ` +
1819
+ "Change approach if stuck, but keep implementing — do not stop for user confirmation.",
1820
+ });
1821
+ }
1822
+ else {
1823
+ governorPauseReason = governed.reason;
1824
+ }
1825
+ }
1770
1826
  }
1771
1827
  await saveOutcomeState(outcomeState);
1772
1828
  loopGuard.recordAttempt(step, call.name, call.args, result.ok, result.exitCode);
@@ -1911,6 +1967,11 @@ export async function runAgentTurn(prompt, options = {}) {
1911
1967
  // Align with /compact default: small recency + dense memory (not keepRecent=6 fat tails).
1912
1968
  const AUTO_COMPACT_KEEP_RECENT = 2;
1913
1969
  let lastCompactionMsgCount = 0;
1970
+ /** E5: identical tool bodies within this turn → pointer instead of re-append. */
1971
+ const toolResultHashes = new Map();
1972
+ /** E4: consecutive free-tier stream failures this turn. */
1973
+ let freeTierConsecutiveFailures = 0;
1974
+ let freeTierLargeContextWarned = false;
1914
1975
  const summarizeForCompaction = async (summaryPrompt) => {
1915
1976
  const response = await completeWithProvider({
1916
1977
  provider,
@@ -1928,7 +1989,9 @@ export async function runAgentTurn(prompt, options = {}) {
1928
1989
  };
1929
1990
  async function maybeAutoCompact(reason, force = false) {
1930
1991
  const beforeTokens = estimateMessagesTokens(messages);
1931
- if (!force && beforeTokens < AUTO_COMPACT_TOKEN_BUDGET)
1992
+ // E1: soft early compact (default 70k) while hard ceiling remains 100k.
1993
+ const compactTrigger = autoCompactTriggerTokens();
1994
+ if (!force && beforeTokens < compactTrigger)
1932
1995
  return;
1933
1996
  if (messages.length <= AUTO_COMPACT_KEEP_RECENT + 2)
1934
1997
  return;
@@ -2005,14 +2068,53 @@ export async function runAgentTurn(prompt, options = {}) {
2005
2068
  // advances when the previous iteration actually executed a tool.
2006
2069
  step = productiveSteps;
2007
2070
  if (governorPauseReason) {
2008
- const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
2009
- outcomeState.outcome.status = "paused_budget";
2010
- await saveOutcomeState(outcomeState);
2011
- moveTurn("paused_budget", governorPauseReason);
2012
- lastAnswer = richSummary;
2013
- return finishTurn(lastAnswer, productiveSteps, "paused_budget", outcomeState.outcome.criteria
2014
- .filter((criterion) => criterion.required && criterion.status !== "proven")
2015
- .map((criterion) => criterion.statement), governorPauseReason);
2071
+ // Non-coding: always ask continue/stop. Coding never sets this reason
2072
+ // (pauseMode === "never"), but guard anyway.
2073
+ if (pauseMode === "never") {
2074
+ governorPauseReason = undefined;
2075
+ governorState = {
2076
+ ...governorState,
2077
+ consecutiveNoDelta: 0,
2078
+ };
2079
+ }
2080
+ else {
2081
+ const confirmPort = options.confirm;
2082
+ let keepGoing = false;
2083
+ if (confirmPort?.confirmContinue) {
2084
+ try {
2085
+ keepGoing = await confirmPort.confirmContinue(productiveSteps, governorPauseReason);
2086
+ }
2087
+ catch {
2088
+ keepGoing = false;
2089
+ }
2090
+ finally {
2091
+ restoreInteractiveStdin();
2092
+ }
2093
+ }
2094
+ if (keepGoing) {
2095
+ writeNotice("info", "continuing after progress pause", chalk.dim(` ℹ continuing after pause (${governorPauseReason}) — change approach if stuck\n`));
2096
+ deferredPostToolMessages.push({
2097
+ role: "system",
2098
+ content: `User chose CONTINUE after progress pause (${governorPauseReason}). ` +
2099
+ "Do not repeat the same failing step; change approach and produce new evidence.",
2100
+ });
2101
+ governorPauseReason = undefined;
2102
+ governorState = {
2103
+ ...governorState,
2104
+ consecutiveNoDelta: 0,
2105
+ };
2106
+ }
2107
+ else {
2108
+ const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
2109
+ outcomeState.outcome.status = "paused_budget";
2110
+ await saveOutcomeState(outcomeState);
2111
+ moveTurn("paused_budget", governorPauseReason);
2112
+ lastAnswer = richSummary;
2113
+ return finishTurn(lastAnswer, productiveSteps, "paused_budget", outcomeState.outcome.criteria
2114
+ .filter((criterion) => criterion.required && criterion.status !== "proven")
2115
+ .map((criterion) => criterion.statement), governorPauseReason);
2116
+ }
2117
+ }
2016
2118
  }
2017
2119
  options.signal?.throwIfAborted();
2018
2120
  let call;
@@ -2057,159 +2159,222 @@ export async function runAgentTurn(prompt, options = {}) {
2057
2159
  ({ dialect: toolDialect, native: nativeToolsActive } =
2058
2160
  resolveNativeTools(provider, model));
2059
2161
  if (messages[0]?.role === "system") {
2060
- messages[0] = {
2061
- role: "system",
2062
- content: composeCurrentSystemPrompt(nativeToolsActive),
2063
- };
2162
+ // Recompose only when content actually changes (hour-stable env clock
2163
+ // keeps the constitution prefix identical across steps, which helps
2164
+ // provider prompt caching and avoids needless object churn).
2165
+ const nextSystem = composeCurrentSystemPrompt(nativeToolsActive);
2166
+ if (messages[0].content !== nextSystem) {
2167
+ messages[0] = {
2168
+ role: "system",
2169
+ content: nextSystem,
2170
+ };
2171
+ }
2064
2172
  }
2065
2173
  const turnTools = selectToolDefs(nativeToolsActive, useCompactSystemPrompt);
2066
2174
  toolsAttached = Boolean(turnTools?.length);
2175
+ const contextBreakdown = buildContextBreakdown(messages, toolsAttached ? turnTools : undefined);
2176
+ // E4: advisory only — never blocks free-tier users.
2177
+ if (!freeTierLargeContextWarned) {
2178
+ const notices = freeTierGuardNotices({
2179
+ provider,
2180
+ estimatedInputTokens: contextBreakdown.estimatedTotalTokens,
2181
+ consecutiveFailures: freeTierConsecutiveFailures,
2182
+ });
2183
+ for (const notice of notices) {
2184
+ if (notice.includes("Large context")) {
2185
+ freeTierLargeContextWarned = true;
2186
+ }
2187
+ writeNotice("info", notice, chalk.dim(` ℹ ${notice}\n`));
2188
+ }
2189
+ }
2067
2190
  await auditLog("agent.turn", {
2068
2191
  provider,
2069
2192
  model,
2070
2193
  tool_protocol: toolsAttached ? "native" : "text",
2071
2194
  dialect: toolDialect,
2072
2195
  step,
2196
+ // Metadata-only composition metrics (no prompt/tool text).
2197
+ ...contextBreakdownAuditPayload(contextBreakdown),
2198
+ compactTriggerTokens: autoCompactTriggerTokens(),
2199
+ maxTokensBudget: resolveStepMaxTokens({
2200
+ nativeToolsActive,
2201
+ toolsAttached,
2202
+ recoveryNudge: retryWithoutThinking,
2203
+ }),
2073
2204
  });
2205
+ // Resume / mid-turn abort can leave orphan tool rows or a user
2206
+ // "continue" before tool results. Heal first so multi-key retry and
2207
+ // history reloads don't hard-fail on protocol asserts.
2208
+ // Heal once per step if needed; silent (no toast) — placeholders are
2209
+ // ok=true so the model doesn't thrash on fake exit=130 failures.
2210
+ repairToolProtocol(messages);
2074
2211
  assertValidToolProtocol(messages);
2075
- completion = await streamWithProvider({
2076
- provider,
2077
- model,
2078
- allowModelFallback: false,
2079
- messages,
2080
- temperature: /minimax-m3/i.test(model) ? 1.0 : 0.2,
2081
- maxTokens: 32_768,
2082
- signal: options.signal,
2083
- thinking: retryWithoutThinking
2084
- ? { ...config.thinking, enabled: false, effort: "low" }
2085
- : config.thinking,
2086
- ...(toolsAttached
2087
- ? {
2088
- tools: turnTools,
2089
- toolChoice: freshWebSearchRequired && !sawFreshWebSearch
2090
- ? { type: "function", name: "web.search" }
2091
- : "auto",
2092
- parallelToolCalls: true,
2093
- // P2-3: emit tool cards as soon as the function name arrives.
2094
- onToolCallDelta: (delta) => {
2095
- if (!delta.name)
2096
- return;
2097
- const name = fromWireName(delta.name) ?? delta.name;
2098
- const existing = deferredToolCalls[delta.index];
2099
- if (existing) {
2100
- if (delta.argumentsBytes &&
2101
- delta.argumentsBytes >= 4096 &&
2102
- !writesDirectly) {
2103
- emit({
2104
- type: "status",
2105
- text: `${name} (${Math.round(delta.argumentsBytes / 1024)}KB args)`,
2212
+ // E3: adaptive completion budget (still large enough for writes).
2213
+ const stepMaxTokens = resolveStepMaxTokens({
2214
+ nativeToolsActive,
2215
+ toolsAttached,
2216
+ recoveryNudge: retryWithoutThinking,
2217
+ });
2218
+ try {
2219
+ completion = await streamWithProvider({
2220
+ provider,
2221
+ model,
2222
+ allowModelFallback: false,
2223
+ messages,
2224
+ temperature: /minimax-m3/i.test(model) ? 1.0 : 0.2,
2225
+ maxTokens: stepMaxTokens,
2226
+ signal: options.signal,
2227
+ thinking: retryWithoutThinking
2228
+ ? { ...config.thinking, enabled: false, effort: "low" }
2229
+ : config.thinking,
2230
+ ...(toolsAttached
2231
+ ? {
2232
+ tools: turnTools,
2233
+ toolChoice: freshWebSearchRequired && !sawFreshWebSearch
2234
+ ? { type: "function", name: "web.search" }
2235
+ : "auto",
2236
+ parallelToolCalls: true,
2237
+ // P2-3: emit tool cards as soon as the function name arrives.
2238
+ onToolCallDelta: (delta) => {
2239
+ if (!delta.name)
2240
+ return;
2241
+ const name = fromWireName(delta.name) ?? delta.name;
2242
+ const existing = deferredToolCalls[delta.index];
2243
+ if (existing) {
2244
+ if (delta.argumentsBytes &&
2245
+ delta.argumentsBytes >= 4096 &&
2246
+ !writesDirectly) {
2247
+ emit({
2248
+ type: "status",
2249
+ text: `${name} (${Math.round(delta.argumentsBytes / 1024)}KB args)`,
2250
+ });
2251
+ }
2252
+ return;
2253
+ }
2254
+ // Ensure slots are dense so index maps to deferredToolCalls[i].
2255
+ while (deferredToolCalls.length < delta.index) {
2256
+ deferredToolCalls.push({
2257
+ eventId: `tool-${++nextToolEventId}`,
2258
+ call: { name: "…", args: {} },
2259
+ rendered: "",
2106
2260
  });
2107
2261
  }
2108
- return;
2262
+ const call = normalizeToolCall({
2263
+ name,
2264
+ args: {},
2265
+ });
2266
+ const eventId = `tool-${++nextToolEventId}`;
2267
+ callIds.push(eventId);
2268
+ alreadyPrintedIds.add(eventId);
2269
+ const toolCallLine = chalk.cyan(` ▶ ${call.name}`) +
2270
+ chalk.gray(` ${formatToolArgs(call)}`);
2271
+ const entry = {
2272
+ eventId,
2273
+ call,
2274
+ rendered: styleToolChatter(call, toolCallLine) + "\n",
2275
+ };
2276
+ if (deferredToolCalls.length === delta.index) {
2277
+ deferredToolCalls.push(entry);
2278
+ }
2279
+ else {
2280
+ deferredToolCalls[delta.index] = entry;
2281
+ }
2282
+ streamedCallsCount = Math.max(streamedCallsCount, deferredToolCalls.length);
2283
+ if (!writesDirectly) {
2284
+ emit({ type: "status", text: call.name });
2285
+ }
2286
+ else {
2287
+ spinner.stop();
2288
+ spinner = startThinkingSpinner(`tool ${call.name}…`, options.signal);
2289
+ }
2290
+ },
2291
+ }
2292
+ : {}),
2293
+ }, (token) => {
2294
+ deltaParser?.push(token);
2295
+ generatedTokens += 1;
2296
+ accumulatedText += token;
2297
+ // Early UI cards from text fences only when native tools are off
2298
+ // (native args stream as structured deltas, not prose).
2299
+ if (!toolsAttached) {
2300
+ const parsedCalls = parseAllToolCalls(accumulatedText);
2301
+ if (parsedCalls.length > streamedCallsCount) {
2302
+ if (writesDirectly) {
2303
+ spinner.stop();
2109
2304
  }
2110
- // Ensure slots are dense so index maps to deferredToolCalls[i].
2111
- while (deferredToolCalls.length < delta.index) {
2305
+ while (streamedCallsCount < parsedCalls.length) {
2306
+ const call = parsedCalls[streamedCallsCount];
2307
+ const eventId = `tool-${++nextToolEventId}`;
2308
+ callIds.push(eventId);
2309
+ alreadyPrintedIds.add(eventId);
2310
+ const toolCallLine = chalk.cyan(` ▶ ${call.name}`) +
2311
+ chalk.gray(` ${formatToolArgs(call)}`);
2112
2312
  deferredToolCalls.push({
2113
- eventId: `tool-${++nextToolEventId}`,
2114
- call: { name: "…", args: {} },
2115
- rendered: "",
2313
+ eventId,
2314
+ call,
2315
+ rendered: styleToolChatter(call, toolCallLine) + "\n",
2116
2316
  });
2317
+ if (!writesDirectly) {
2318
+ emit({ type: "status", text: call.name });
2319
+ }
2320
+ streamedCallsCount += 1;
2117
2321
  }
2118
- const call = normalizeToolCall({
2119
- name,
2120
- args: {},
2121
- });
2122
- const eventId = `tool-${++nextToolEventId}`;
2123
- callIds.push(eventId);
2124
- alreadyPrintedIds.add(eventId);
2125
- const toolCallLine = chalk.cyan(` ▶ ${call.name}`) +
2126
- chalk.gray(` ${formatToolArgs(call)}`);
2127
- const entry = {
2128
- eventId,
2129
- call,
2130
- rendered: styleToolChatter(call, toolCallLine) + "\n",
2131
- };
2132
- if (deferredToolCalls.length === delta.index) {
2133
- deferredToolCalls.push(entry);
2134
- }
2135
- else {
2136
- deferredToolCalls[delta.index] = entry;
2137
- }
2138
- streamedCallsCount = Math.max(streamedCallsCount, deferredToolCalls.length);
2139
- if (!writesDirectly) {
2140
- emit({ type: "status", text: call.name });
2322
+ if (writesDirectly) {
2323
+ spinner = startThinkingSpinner(`generating response (${generatedTokens} tokens)`, options.signal);
2141
2324
  }
2142
- else {
2143
- spinner.stop();
2144
- spinner = startThinkingSpinner(`tool ${call.name}…`, options.signal);
2145
- }
2146
- },
2147
- }
2148
- : {}),
2149
- }, (token) => {
2150
- deltaParser?.push(token);
2151
- generatedTokens += 1;
2152
- accumulatedText += token;
2153
- // Early UI cards from text fences only when native tools are off
2154
- // (native args stream as structured deltas, not prose).
2155
- if (!toolsAttached) {
2156
- const parsedCalls = parseAllToolCalls(accumulatedText);
2157
- if (parsedCalls.length > streamedCallsCount) {
2158
- if (writesDirectly) {
2159
- spinner.stop();
2160
- }
2161
- while (streamedCallsCount < parsedCalls.length) {
2162
- const call = parsedCalls[streamedCallsCount];
2163
- const eventId = `tool-${++nextToolEventId}`;
2164
- callIds.push(eventId);
2165
- alreadyPrintedIds.add(eventId);
2166
- const toolCallLine = chalk.cyan(` ▶ ${call.name}`) +
2167
- chalk.gray(` ${formatToolArgs(call)}`);
2168
- deferredToolCalls.push({
2169
- eventId,
2170
- call,
2171
- rendered: styleToolChatter(call, toolCallLine) + "\n",
2172
- });
2173
- if (!writesDirectly) {
2174
- emit({ type: "status", text: call.name });
2175
- }
2176
- streamedCallsCount += 1;
2177
2325
  }
2178
- if (writesDirectly) {
2179
- spinner = startThinkingSpinner(`generating response (${generatedTokens} tokens)`, options.signal);
2326
+ }
2327
+ if (!sawReasoning && /<think/i.test(token)) {
2328
+ sawReasoning = true;
2329
+ inThinking = true;
2330
+ spinner.setLabel("thinking");
2331
+ if (!writesDirectly)
2332
+ emit({ type: "status", text: "thinking" });
2333
+ }
2334
+ if (/<\/think>/i.test(token)) {
2335
+ inThinking = false;
2336
+ spinner.setLabel("generating response (0 tokens)");
2337
+ generatedTokens = 0;
2338
+ }
2339
+ if (inThinking) {
2340
+ const cleaned = token.replace(/<\/?think[^>]*>/gi, "");
2341
+ if (cleaned) {
2342
+ spinner.pushPreview(cleaned);
2343
+ const approx = cleaned.split(/\s+/).filter(Boolean).length;
2344
+ if (approx > 0)
2345
+ spinner.bumpReasoning(approx);
2180
2346
  }
2181
2347
  }
2182
- }
2183
- if (!sawReasoning && /<think/i.test(token)) {
2184
- sawReasoning = true;
2185
- inThinking = true;
2186
- spinner.setLabel("thinking");
2187
- if (!writesDirectly)
2188
- emit({ type: "status", text: "thinking" });
2189
- }
2190
- if (/<\/think>/i.test(token)) {
2191
- inThinking = false;
2192
- spinner.setLabel("generating response (0 tokens)");
2193
- generatedTokens = 0;
2194
- }
2195
- if (inThinking) {
2196
- const cleaned = token.replace(/<\/?think[^>]*>/gi, "");
2197
- if (cleaned) {
2198
- spinner.pushPreview(cleaned);
2199
- const approx = cleaned.split(/\s+/).filter(Boolean).length;
2200
- if (approx > 0)
2201
- spinner.bumpReasoning(approx);
2348
+ else {
2349
+ if (generatedTokens % 10 === 0) {
2350
+ spinner.setLabel(`generating response (${generatedTokens} tokens)`);
2351
+ }
2202
2352
  }
2203
- }
2204
- else {
2205
- if (generatedTokens % 10 === 0) {
2206
- spinner.setLabel(`generating response (${generatedTokens} tokens)`);
2353
+ }, (status) => {
2354
+ spinner.stop();
2355
+ writeStatus(status, chalk.dim(status));
2356
+ // Toast only on key *switch* after a failure — never on sticky
2357
+ // "using" or retry countdown ticks (those stay in composer status).
2358
+ if (/^switching /i.test(status.trim())) {
2359
+ writeNotice("warn", status.trim(), chalk.yellow(` ${status.trim()}\n`));
2207
2360
  }
2361
+ });
2362
+ freeTierConsecutiveFailures = 0;
2363
+ }
2364
+ catch (streamError) {
2365
+ // E4: track free-tier failures for advisory notices (never blocks).
2366
+ freeTierConsecutiveFailures += 1;
2367
+ for (const notice of freeTierGuardNotices({
2368
+ provider,
2369
+ estimatedInputTokens: contextBreakdown.estimatedTotalTokens,
2370
+ consecutiveFailures: freeTierConsecutiveFailures,
2371
+ })) {
2372
+ if (notice.includes("Large context"))
2373
+ continue; // already shown above
2374
+ writeNotice("warn", notice, chalk.yellow(` ⚠ ${notice}\n`));
2208
2375
  }
2209
- }, (status) => {
2210
- spinner.stop();
2211
- writeStatus(status, chalk.dim(status));
2212
- });
2376
+ throw streamError;
2377
+ }
2213
2378
  }
2214
2379
  finally {
2215
2380
  // Always clear the spinner — abort, network error, or success.
@@ -2977,9 +3142,17 @@ export async function runAgentTurn(prompt, options = {}) {
2977
3142
  planCreatedThisTurn = true;
2978
3143
  }
2979
3144
  productiveSteps += 1;
3145
+ // E5: collapse identical large tool bodies within this turn to a pointer.
3146
+ const deduped = dedupeToolContextOutput({
3147
+ content: res.contextOutput,
3148
+ toolName: res.call.name,
3149
+ artifactPath: res.result.outputPath,
3150
+ seenHashes: toolResultHashes,
3151
+ });
3152
+ const contextForHistory = deduped.content;
2980
3153
  // Soft plan-mode note on tool payloads only (never a user message).
2981
3154
  // Stop once a plan with tasks exists so we don't nag after plan.create.
2982
- let toolContent = `Tool ${res.call.name} result (exit=${res.result.exitCode ?? 0}, ok=${res.result.ok}):\n${res.contextOutput}`;
3155
+ let toolContent = `Tool ${res.call.name} result (exit=${res.result.exitCode ?? 0}, ok=${res.result.ok}):\n${contextForHistory}`;
2983
3156
  const reminded = maybeAppendPlanModeReminder(toolContent, {
2984
3157
  isPlanMode,
2985
3158
  planApproved: session.planApproved.value,
@@ -3117,6 +3290,19 @@ export async function runAgentTurn(prompt, options = {}) {
3117
3290
  else {
3118
3291
  session.planApproved.value = true;
3119
3292
  }
3293
+ // Re-derive pause policy from the new plan kind (coding builds
3294
+ // must not hard-pause even if the free-text prompt was generic).
3295
+ const kindArg = typeof res.call.args.kind === "string"
3296
+ ? res.call.args.kind
3297
+ : undefined;
3298
+ codingSession = codingSessionFromContext({
3299
+ buildLike,
3300
+ planKind: kindArg,
3301
+ });
3302
+ pauseMode = progressPauseMode({
3303
+ codingSession,
3304
+ autoConfirm: Boolean(options.autoConfirm),
3305
+ });
3120
3306
  }
3121
3307
  // User Esc/Ctrl+C only — never cancel siblings because a delete failed
3122
3308
  // or a confirm was declined; the model must see every tool result.
@@ -3236,6 +3422,8 @@ export async function runAgentTurn(prompt, options = {}) {
3236
3422
  }
3237
3423
  }
3238
3424
  }
3425
+ // Hard iteration ceiling (hundreds of steps) — rare. Mid-turn governor
3426
+ // pauses already confirm for non-coding; coding never hard-pauses there.
3239
3427
  const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
3240
3428
  lastAnswer = richSummary;
3241
3429
  outcomeState.outcome.status = "paused_budget";