@astralyn/pi 0.84.2 → 0.84.4

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 (151) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/cli/args.js +1 -1
  3. package/dist/cli/args.js.map +1 -1
  4. package/dist/cli/config-selector.d.ts.map +1 -1
  5. package/dist/cli/config-selector.js +2 -3
  6. package/dist/cli/config-selector.js.map +1 -1
  7. package/dist/cli/project-trust.d.ts.map +1 -1
  8. package/dist/cli/project-trust.js +1 -4
  9. package/dist/cli/project-trust.js.map +1 -1
  10. package/dist/cli/startup-ui.d.ts.map +1 -1
  11. package/dist/cli/startup-ui.js +3 -5
  12. package/dist/cli/startup-ui.js.map +1 -1
  13. package/dist/config.d.ts +5 -0
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +8 -1
  16. package/dist/config.js.map +1 -1
  17. package/dist/core/agent-session.d.ts +1 -25
  18. package/dist/core/agent-session.d.ts.map +1 -1
  19. package/dist/core/agent-session.js +29 -408
  20. package/dist/core/agent-session.js.map +1 -1
  21. package/dist/core/compaction-controller.d.ts +89 -0
  22. package/dist/core/compaction-controller.d.ts.map +1 -0
  23. package/dist/core/compaction-controller.js +428 -0
  24. package/dist/core/compaction-controller.js.map +1 -0
  25. package/dist/core/extensions/index.d.ts +1 -1
  26. package/dist/core/extensions/index.d.ts.map +1 -1
  27. package/dist/core/extensions/index.js.map +1 -1
  28. package/dist/core/extensions/loader.js +3 -3
  29. package/dist/core/extensions/loader.js.map +1 -1
  30. package/dist/core/extensions/types.d.ts +2 -27
  31. package/dist/core/extensions/types.d.ts.map +1 -1
  32. package/dist/core/extensions/types.js.map +1 -1
  33. package/dist/core/provider-attribution.js +2 -2
  34. package/dist/core/provider-attribution.js.map +1 -1
  35. package/dist/core/settings-manager.js +1 -1
  36. package/dist/core/settings-manager.js.map +1 -1
  37. package/dist/core/telemetry.d.ts +1 -1
  38. package/dist/core/telemetry.d.ts.map +1 -1
  39. package/dist/core/telemetry.js +1 -1
  40. package/dist/core/telemetry.js.map +1 -1
  41. package/dist/extensions/deepwiki/index.d.ts.map +1 -1
  42. package/dist/extensions/deepwiki/index.js +23 -1
  43. package/dist/extensions/deepwiki/index.js.map +1 -1
  44. package/dist/extensions/deepwiki/render.d.ts +1 -1
  45. package/dist/extensions/deepwiki/render.d.ts.map +1 -1
  46. package/dist/extensions/deepwiki/render.js +4 -2
  47. package/dist/extensions/deepwiki/render.js.map +1 -1
  48. package/dist/extensions/question/index.d.ts.map +1 -1
  49. package/dist/extensions/question/index.js +0 -1
  50. package/dist/extensions/question/index.js.map +1 -1
  51. package/dist/extensions/subagent/agents-command.d.ts +4 -0
  52. package/dist/extensions/subagent/agents-command.d.ts.map +1 -0
  53. package/dist/extensions/subagent/agents-command.js +230 -0
  54. package/dist/extensions/subagent/agents-command.js.map +1 -0
  55. package/dist/extensions/subagent/agents.d.ts.map +1 -1
  56. package/dist/extensions/subagent/agents.js +2 -0
  57. package/dist/extensions/subagent/agents.js.map +1 -1
  58. package/dist/extensions/subagent/display-name.d.ts +6 -0
  59. package/dist/extensions/subagent/display-name.d.ts.map +1 -0
  60. package/dist/extensions/subagent/display-name.js +13 -0
  61. package/dist/extensions/subagent/display-name.js.map +1 -0
  62. package/dist/extensions/subagent/index.d.ts.map +1 -1
  63. package/dist/extensions/subagent/index.js +19 -126
  64. package/dist/extensions/subagent/index.js.map +1 -1
  65. package/dist/extensions/subagent/render.d.ts.map +1 -1
  66. package/dist/extensions/subagent/render.js +1 -10
  67. package/dist/extensions/subagent/render.js.map +1 -1
  68. package/dist/extensions/subagent/settings.d.ts +0 -1
  69. package/dist/extensions/subagent/settings.d.ts.map +1 -1
  70. package/dist/extensions/subagent/settings.js +3 -7
  71. package/dist/extensions/subagent/settings.js.map +1 -1
  72. package/dist/extensions/subagent/types.d.ts +2 -0
  73. package/dist/extensions/subagent/types.d.ts.map +1 -1
  74. package/dist/extensions/subagent/types.js.map +1 -1
  75. package/dist/extensions/subagent/ui/model-list.d.ts +67 -0
  76. package/dist/extensions/subagent/ui/model-list.d.ts.map +1 -0
  77. package/dist/extensions/subagent/ui/model-list.js +242 -0
  78. package/dist/extensions/subagent/ui/model-list.js.map +1 -0
  79. package/dist/extensions/subagent/ui/profile-editor.d.ts +54 -0
  80. package/dist/extensions/subagent/ui/profile-editor.d.ts.map +1 -0
  81. package/dist/extensions/subagent/ui/profile-editor.js +154 -0
  82. package/dist/extensions/subagent/ui/profile-editor.js.map +1 -0
  83. package/dist/extensions/subagent/ui/profile-picker.d.ts +32 -0
  84. package/dist/extensions/subagent/ui/profile-picker.d.ts.map +1 -0
  85. package/dist/extensions/subagent/ui/profile-picker.js +149 -0
  86. package/dist/extensions/subagent/ui/profile-picker.js.map +1 -0
  87. package/dist/extensions/todo/constants.d.ts +6 -17
  88. package/dist/extensions/todo/constants.d.ts.map +1 -1
  89. package/dist/extensions/todo/constants.js +20 -31
  90. package/dist/extensions/todo/constants.js.map +1 -1
  91. package/dist/extensions/todo/index.d.ts.map +1 -1
  92. package/dist/extensions/todo/index.js +36 -57
  93. package/dist/extensions/todo/index.js.map +1 -1
  94. package/dist/extensions/todo/schema.d.ts +20 -59
  95. package/dist/extensions/todo/schema.d.ts.map +1 -1
  96. package/dist/extensions/todo/schema.js +23 -69
  97. package/dist/extensions/todo/schema.js.map +1 -1
  98. package/dist/extensions/todo/state.d.ts +23 -65
  99. package/dist/extensions/todo/state.d.ts.map +1 -1
  100. package/dist/extensions/todo/state.js +238 -1262
  101. package/dist/extensions/todo/state.js.map +1 -1
  102. package/dist/extensions/todo/view.d.ts +19 -10
  103. package/dist/extensions/todo/view.d.ts.map +1 -1
  104. package/dist/extensions/todo/view.js +411 -463
  105. package/dist/extensions/todo/view.js.map +1 -1
  106. package/dist/extensions/todo/widget.d.ts +15 -0
  107. package/dist/extensions/todo/widget.d.ts.map +1 -0
  108. package/dist/extensions/todo/widget.js +50 -0
  109. package/dist/extensions/todo/widget.js.map +1 -0
  110. package/dist/modes/interactive/components/extension-selector.d.ts +1 -4
  111. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  112. package/dist/modes/interactive/components/extension-selector.js +6 -12
  113. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  114. package/dist/modes/interactive/components/settings-selector.js +1 -1
  115. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  116. package/dist/modes/interactive/components/tool-chrome.d.ts +28 -0
  117. package/dist/modes/interactive/components/tool-chrome.d.ts.map +1 -0
  118. package/dist/modes/interactive/components/tool-chrome.js +81 -0
  119. package/dist/modes/interactive/components/tool-chrome.js.map +1 -0
  120. package/dist/modes/interactive/components/tool-execution.d.ts +0 -3
  121. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  122. package/dist/modes/interactive/components/tool-execution.js +11 -108
  123. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  124. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  125. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  126. package/dist/modes/interactive/interactive-mode.js +35 -52
  127. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  128. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  129. package/dist/modes/rpc/rpc-mode.js +1 -8
  130. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  131. package/docs/bundled/README.md +1 -1
  132. package/docs/bundled/extensions/subagent.md +20 -5
  133. package/docs/bundled/extensions/todo.md +84 -24
  134. package/docs/bundled/tool-presentation.md +3 -3
  135. package/docs/extensions.md +6 -23
  136. package/docs/settings.md +1 -1
  137. package/docs/usage.md +1 -1
  138. package/npm-shrinkwrap.json +2 -2
  139. package/package.json +1 -1
  140. package/dist/extensions/subagent/picker.d.ts +0 -31
  141. package/dist/extensions/subagent/picker.d.ts.map +0 -1
  142. package/dist/extensions/subagent/picker.js +0 -109
  143. package/dist/extensions/subagent/picker.js.map +0 -1
  144. package/dist/extensions/todo/overlay.d.ts +0 -24
  145. package/dist/extensions/todo/overlay.d.ts.map +0 -1
  146. package/dist/extensions/todo/overlay.js +0 -135
  147. package/dist/extensions/todo/overlay.js.map +0 -1
  148. package/dist/utils/coalescing-terminal.d.ts +0 -132
  149. package/dist/utils/coalescing-terminal.d.ts.map +0 -1
  150. package/dist/utils/coalescing-terminal.js +0 -292
  151. package/dist/utils/coalescing-terminal.js.map +0 -1
@@ -15,7 +15,7 @@
15
15
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
16
  import { basename, dirname } from "node:path";
17
17
  import { contentText } from "@earendil-works/pi-ai";
18
- import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRecoverableLength, isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, } from "@earendil-works/pi-ai/compat";
18
+ import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, } from "@earendil-works/pi-ai/compat";
19
19
  import { getThemeByName, theme } from "../modes/interactive/theme/theme.js";
20
20
  import { stripFrontmatter } from "../utils/frontmatter.js";
21
21
  import { resolvePath } from "../utils/paths.js";
@@ -23,7 +23,8 @@ import { sleep } from "../utils/sleep.js";
23
23
  import { normalizeToolResultImages } from "../utils/tool-result-images.js";
24
24
  import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
25
25
  import { executeBashWithOperations } from "./bash-executor.js";
26
- import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
26
+ import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
27
+ import { CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD, CompactionController, estimateMessagesTokens, } from "./compaction-controller.js";
27
28
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
28
29
  import { exportSessionToHtml } from "./export-html/index.js";
29
30
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
@@ -61,19 +62,9 @@ function withoutDeletedHeaders(headers) {
61
62
  ? Object.fromEntries(Object.entries(headers).filter((entry) => entry[1] !== null))
62
63
  : undefined;
63
64
  }
64
- function estimateMessagesTokens(messages) {
65
- let tokens = 0;
66
- for (const message of messages) {
67
- tokens += estimateTokens(message);
68
- }
69
- return tokens;
70
- }
71
65
  // ============================================================================
72
66
  // Constants
73
67
  // ============================================================================
74
- const CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD = "Estimated retained context remains above the configured auto-compaction threshold. Reduce compaction.keepRecentTokens, remove large retained content, or switch to a model with a larger context window before continuing.";
75
- const NOTHING_TO_COMPACT_WITHIN_KEEP_WINDOW = "Nothing to compact while preserving the configured recent context. Reduce compaction.keepRecentTokens, remove large retained content, or switch to a model with a larger context window before continuing.";
76
- const AUTO_COMPACTION_STOPPED_BEFORE_NEXT_PROVIDER = "Stopped before the next provider request because auto-compaction did not produce a safe context. Review the compaction warning before continuing.";
77
68
  /** Standard thinking levels */
78
69
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
79
70
  // ============================================================================
@@ -91,11 +82,6 @@ export class AgentSession {
91
82
  this._pendingNextTurnMessages = [];
92
83
  // Compaction state
93
84
  this._compactionAbortController = undefined;
94
- this._autoCompactionAbortController = undefined;
95
- this._stopAfterTurnRequested = false;
96
- /** An extension cancelled a mid-turn compaction; skip further mid-turn checks this run. */
97
- this._midTurnCompactionDeclined = false;
98
- this._overflowRecoveryAttempted = false;
99
85
  // Branch summarization state
100
86
  this._branchSummaryAbortController = undefined;
101
87
  // Retry state
@@ -121,7 +107,7 @@ export class AgentSession {
121
107
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
122
108
  // This ensures the UI sees the updated queue state
123
109
  if (event.type === "message_start" && event.message.role === "user") {
124
- this._overflowRecoveryAttempted = false;
110
+ this._compaction.resetOverflowRecovery();
125
111
  const messageText = contentText(event.message.content, "");
126
112
  if (messageText) {
127
113
  // Check steering queue first
@@ -163,7 +149,7 @@ export class AgentSession {
163
149
  this._lastAssistantMessage = event.message;
164
150
  const assistantMsg = event.message;
165
151
  if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "length") {
166
- this._overflowRecoveryAttempted = false;
152
+ this._compaction.resetOverflowRecovery();
167
153
  }
168
154
  // Reset retry counter immediately on successful assistant response
169
155
  // This prevents accumulation across multiple LLM calls within a turn
@@ -192,6 +178,19 @@ export class AgentSession {
192
178
  this._excludedToolNames = config.excludedToolNames ? new Set(config.excludedToolNames) : undefined;
193
179
  this._baseToolsOverride = config.baseToolsOverride;
194
180
  this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
181
+ this._compaction = new CompactionController({
182
+ agent: this.agent,
183
+ sessionManager: this.sessionManager,
184
+ settingsManager: this.settingsManager,
185
+ getModel: () => this.model,
186
+ getThinkingLevel: () => this.thinkingLevel,
187
+ getExtensionRunner: () => this._extensionRunner,
188
+ isManualCompactionActive: () => this._compactionAbortController !== undefined,
189
+ emit: (event) => this._emit(event),
190
+ getRequiredRequestAuth: (model) => this._getRequiredRequestAuth(model),
191
+ getSummarizationRequestAuth: (model, streamFunction) => this._getSummarizationRequestAuth(model, streamFunction),
192
+ summarizationRetryCallbacks: (source) => this._summarizationRetryCallbacks(source),
193
+ });
195
194
  // Always subscribe to agent events for internal handling
196
195
  // (session persistence, extensions, auto-compaction, retry logic)
197
196
  this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
@@ -319,12 +318,12 @@ export class AgentSession {
319
318
  ? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
320
319
  : undefined);
321
320
  this.agent.prepareNextTurnWithContext = async (turn, signal) => {
322
- const compaction = await this._maybeCompactBeforeNextToolTurn(turn, signal);
321
+ const compaction = await this._compaction.maybeCompactBeforeNextToolTurn(turn, signal);
323
322
  if (compaction.stopErrorMessage) {
324
323
  // Agent does not expose the low-level graceful turn-stop hook. Throwing here
325
324
  // exits before queue polling or another provider request; the run records a
326
325
  // normal error/aborted lifecycle instead of risking an oversized context.
327
- this._stopAfterTurnRequested = true;
326
+ this._compaction.requestStopAfterTurn();
328
327
  throw new Error(compaction.stopErrorMessage);
329
328
  }
330
329
  const effectiveTurn = compaction.messages === undefined
@@ -640,7 +639,7 @@ export class AgentSession {
640
639
  }
641
640
  /** Whether compaction or branch summarization is currently running */
642
641
  get isCompacting() {
643
- return (this._autoCompactionAbortController !== undefined ||
642
+ return (this._compaction.isAutoCompacting ||
644
643
  this._compactionAbortController !== undefined ||
645
644
  this._branchSummaryAbortController !== undefined);
646
645
  }
@@ -738,8 +737,7 @@ export class AgentSession {
738
737
  // =========================================================================
739
738
  async _runAgentPrompt(messages) {
740
739
  this._isAgentRunActive = true;
741
- this._stopAfterTurnRequested = false;
742
- this._midTurnCompactionDeclined = false;
740
+ this._compaction.noteRunStart();
743
741
  try {
744
742
  await this.agent.prompt(messages);
745
743
  while (await this._handlePostAgentRun()) {
@@ -756,12 +754,10 @@ export class AgentSession {
756
754
  const msg = this._lastAssistantMessage;
757
755
  this._lastAssistantMessage = undefined;
758
756
  if (!msg) {
759
- this._stopAfterTurnRequested = false;
757
+ this._compaction.consumeStopAfterTurn();
760
758
  return false;
761
759
  }
762
- const stopAfterTurnRequested = this._stopAfterTurnRequested;
763
- this._stopAfterTurnRequested = false;
764
- if (stopAfterTurnRequested) {
760
+ if (this._compaction.consumeStopAfterTurn()) {
765
761
  return false;
766
762
  }
767
763
  if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
@@ -776,11 +772,10 @@ export class AgentSession {
776
772
  });
777
773
  this._retryAttempt = 0;
778
774
  }
779
- if (await this._checkCompaction(msg)) {
775
+ if (await this._compaction.checkCompaction(msg)) {
780
776
  return true;
781
777
  }
782
- if (this._stopAfterTurnRequested) {
783
- this._stopAfterTurnRequested = false;
778
+ if (this._compaction.consumeStopAfterTurn()) {
784
779
  return false;
785
780
  }
786
781
  // The agent loop drains both queues before emitting agent_end. Any messages
@@ -869,13 +864,13 @@ export class AgentSession {
869
864
  // The user's new prompt is sent below, so do not call agent.continue() here.
870
865
  const lastAssistant = this._findLastAssistantMessage();
871
866
  if (lastAssistant) {
872
- await this._checkCompaction(lastAssistant, false, "prePrompt");
867
+ await this._compaction.checkCompaction(lastAssistant, false, "prePrompt");
873
868
  }
874
869
  const compactionSettings = this.settingsManager.getCompactionSettings();
875
870
  // A compaction entry can be valid while its retained suffix is still too large.
876
871
  // Refuse the existing context here rather than discovering that via provider overflow.
877
872
  if (compactionSettings.enabled &&
878
- shouldCompact(this._estimateContextTokensForCompaction(this.agent.state.messages), this.model.contextWindow, compactionSettings)) {
873
+ shouldCompact(this._compaction.estimateContextTokens(this.agent.state.messages), this.model.contextWindow, compactionSettings)) {
879
874
  throw new Error(CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD);
880
875
  }
881
876
  // Build messages array (custom message if any, then user message)
@@ -1503,7 +1498,7 @@ export class AgentSession {
1503
1498
  */
1504
1499
  abortCompaction() {
1505
1500
  this._compactionAbortController?.abort();
1506
- this._autoCompactionAbortController?.abort();
1501
+ this._compaction.abort();
1507
1502
  }
1508
1503
  /**
1509
1504
  * Cancel in-progress branch summarization.
@@ -1511,380 +1506,6 @@ export class AgentSession {
1511
1506
  abortBranchSummary() {
1512
1507
  this._branchSummaryAbortController?.abort();
1513
1508
  }
1514
- _estimateContextTokensForCompaction(messages) {
1515
- const estimate = estimateContextTokens(messages);
1516
- const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
1517
- if (compactionEntry && estimate.lastUsageIndex !== null) {
1518
- const usageMessage = messages[estimate.lastUsageIndex];
1519
- if (usageMessage?.role === "assistant" &&
1520
- usageMessage.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
1521
- // A retained pre-compaction usage describes the old, larger context. Fall back
1522
- // to message estimates rather than immediately compacting again from stale data.
1523
- return estimateMessagesTokens(messages);
1524
- }
1525
- }
1526
- return estimate.tokens;
1527
- }
1528
- async _maybeCompactBeforeNextToolTurn(turn, signal) {
1529
- if (signal?.aborted) {
1530
- return { stopErrorMessage: AUTO_COMPACTION_STOPPED_BEFORE_NEXT_PROVIDER };
1531
- }
1532
- // Tool batches always continue the run; a turn without tool results only
1533
- // continues when steering/follow-up messages are queued, otherwise the
1534
- // post-run path covers it.
1535
- if (turn.toolResults.length === 0 && !this.agent.hasQueuedMessages()) {
1536
- return {};
1537
- }
1538
- if (this._midTurnCompactionDeclined) {
1539
- return {};
1540
- }
1541
- const settings = this.settingsManager.getCompactionSettings();
1542
- if (!settings.enabled) {
1543
- return {};
1544
- }
1545
- const contextTokens = this._estimateContextTokensForCompaction(turn.context.messages);
1546
- const contextWindow = this.model?.contextWindow ?? 0;
1547
- if (!shouldCompact(contextTokens, contextWindow, settings)) {
1548
- return {};
1549
- }
1550
- const outcome = await this._runAutoCompactionWithOutcome("threshold", false, "midTurn", signal);
1551
- if (!outcome.compacted) {
1552
- if (outcome.cancelled && signal?.aborted !== true) {
1553
- // The extension took ownership of the risk; don't re-ask on every batch.
1554
- this._midTurnCompactionDeclined = true;
1555
- return {};
1556
- }
1557
- return { stopErrorMessage: AUTO_COMPACTION_STOPPED_BEFORE_NEXT_PROVIDER };
1558
- }
1559
- return {
1560
- messages: this.agent.state.messages.slice(),
1561
- ...(signal?.aborted === true
1562
- ? { stopErrorMessage: AUTO_COMPACTION_STOPPED_BEFORE_NEXT_PROVIDER }
1563
- : outcome.mustStop === true
1564
- ? { stopErrorMessage: CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD }
1565
- : {}),
1566
- };
1567
- }
1568
- /**
1569
- * Check if compaction is needed and run it.
1570
- * Called after agent_end and before prompt submission.
1571
- *
1572
- * Two cases:
1573
- * 1. Recoverable failure: LLM returned context overflow or stopped below its desired output limit;
1574
- * remove the assistant message, compact, and auto-retry once
1575
- * 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)
1576
- *
1577
- * @param assistantMessage The assistant message to check
1578
- * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
1579
- */
1580
- async _checkCompaction(assistantMessage, skipAbortedCheck = true, timing = "postRun") {
1581
- const settings = this.settingsManager.getCompactionSettings();
1582
- if (!settings.enabled)
1583
- return false;
1584
- // Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
1585
- if (skipAbortedCheck && assistantMessage.stopReason === "aborted")
1586
- return false;
1587
- const contextWindow = this.model?.contextWindow ?? 0;
1588
- // Skip overflow check if the message came from a different model.
1589
- // This handles the case where user switched from a smaller-context model (e.g. opus)
1590
- // to a larger-context model (e.g. codex) - the overflow error from the old model
1591
- // shouldn't trigger compaction for the new model.
1592
- const sameModel = this.model && assistantMessage.provider === this.model.provider && assistantMessage.model === this.model.id;
1593
- // Skip compaction checks if this assistant message is older than the latest
1594
- // compaction boundary. This prevents a stale pre-compaction usage/error
1595
- // from retriggering compaction on the first prompt after compaction.
1596
- const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
1597
- const assistantIsFromBeforeCompaction = compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
1598
- if (assistantIsFromBeforeCompaction) {
1599
- return false;
1600
- }
1601
- // Case 1: Recoverable failure. Explicit/silent context overflow still uses context metadata.
1602
- // A length stop is recoverable when output ended below the model's original desired limit,
1603
- // independent of the configured context size or any context-clamped provider request limit.
1604
- // A successful response over the configured window should compact but must not retry: the
1605
- // assistant answer already completed and agent.continue() cannot continue from an assistant.
1606
- const recoverableLength = sameModel && isRecoverableLength(assistantMessage, this.model?.maxTokens ?? 0);
1607
- if (sameModel && (isContextOverflow(assistantMessage, contextWindow) || recoverableLength)) {
1608
- const willRetry = assistantMessage.stopReason !== "stop";
1609
- if (!willRetry) {
1610
- return await this._runAutoCompaction("overflow", false, timing);
1611
- }
1612
- if (this._overflowRecoveryAttempted) {
1613
- this._stopAfterTurnRequested = true;
1614
- this._emit({
1615
- type: "compaction_end",
1616
- reason: "overflow",
1617
- result: undefined,
1618
- aborted: false,
1619
- willRetry: false,
1620
- errorMessage: "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
1621
- });
1622
- return false;
1623
- }
1624
- this._overflowRecoveryAttempted = true;
1625
- // Remove the failed or truncated message from agent state. It remains in session history,
1626
- // but must not be included in the compact-and-retry context.
1627
- const messages = this.agent.state.messages;
1628
- if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
1629
- this.agent.state.messages = messages.slice(0, -1);
1630
- }
1631
- return await this._runAutoCompaction("overflow", willRetry, timing);
1632
- }
1633
- // Case 2: Threshold - context is getting large
1634
- // For error messages or all-zero usage messages, estimate from the last valid response.
1635
- // This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage
1636
- // responses can still compact and do not reset context accounting.
1637
- let contextTokens;
1638
- const directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0;
1639
- if (assistantMessage.stopReason === "error" || directContextTokens === 0) {
1640
- const messages = this.agent.state.messages;
1641
- const estimate = estimateContextTokens(messages);
1642
- if (estimate.lastUsageIndex === null)
1643
- return false; // No usage data at all
1644
- // Verify the usage source is post-compaction. Kept pre-compaction messages
1645
- // have stale usage reflecting the old (larger) context and would falsely
1646
- // trigger compaction right after one just finished.
1647
- const usageMsg = messages[estimate.lastUsageIndex];
1648
- if (compactionEntry &&
1649
- usageMsg.role === "assistant" &&
1650
- usageMsg.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
1651
- return false;
1652
- }
1653
- contextTokens = estimate.tokens;
1654
- }
1655
- else {
1656
- contextTokens = directContextTokens;
1657
- }
1658
- if (shouldCompact(contextTokens, contextWindow, settings)) {
1659
- return await this._runAutoCompaction("threshold", false, timing);
1660
- }
1661
- return false;
1662
- }
1663
- /**
1664
- * Internal: Run auto-compaction with events.
1665
- */
1666
- async _runAutoCompaction(reason, willRetry, timing = "postRun") {
1667
- const outcome = await this._runAutoCompactionWithOutcome(reason, willRetry, timing);
1668
- if (!outcome.compacted || outcome.mustStop) {
1669
- this._stopAfterTurnRequested = true;
1670
- }
1671
- return outcome.shouldContinue;
1672
- }
1673
- async _runAutoCompactionWithOutcome(reason, willRetry, timing, parentSignal) {
1674
- const settings = this.settingsManager.getCompactionSettings();
1675
- let started = false;
1676
- let controller;
1677
- let removeParentAbortListener;
1678
- try {
1679
- if (!this.model ||
1680
- this._autoCompactionAbortController ||
1681
- this._compactionAbortController ||
1682
- parentSignal?.aborted) {
1683
- return { compacted: false, shouldContinue: false };
1684
- }
1685
- const model = this.model;
1686
- const streamFunction = this.agent.streamFunction;
1687
- const thinkingLevel = this.thinkingLevel;
1688
- const retrySettings = this.settingsManager.getRetrySettings();
1689
- controller = new AbortController();
1690
- if (parentSignal) {
1691
- const abortFromParent = () => controller?.abort();
1692
- parentSignal.addEventListener("abort", abortFromParent, { once: true });
1693
- removeParentAbortListener = () => parentSignal.removeEventListener("abort", abortFromParent);
1694
- }
1695
- if (controller.signal.aborted) {
1696
- return { compacted: false, shouldContinue: false };
1697
- }
1698
- this._autoCompactionAbortController = controller;
1699
- started = true;
1700
- this._emit({ type: "compaction_start", reason });
1701
- if (controller.signal.aborted) {
1702
- this._emit({
1703
- type: "compaction_end",
1704
- reason,
1705
- result: undefined,
1706
- aborted: true,
1707
- willRetry: false,
1708
- });
1709
- return { compacted: false, shouldContinue: false };
1710
- }
1711
- let extensionCompaction;
1712
- let fromExtension = false;
1713
- const pathEntries = this.sessionManager.getBranch();
1714
- const preparation = prepareCompaction(pathEntries, settings);
1715
- if (!preparation) {
1716
- throw new Error(NOTHING_TO_COMPACT_WITHIN_KEEP_WINDOW);
1717
- }
1718
- if (this._extensionRunner.hasHandlers("session_before_compact")) {
1719
- const extensionResult = (await this._extensionRunner.emit({
1720
- type: "session_before_compact",
1721
- preparation,
1722
- branchEntries: pathEntries,
1723
- customInstructions: undefined,
1724
- reason,
1725
- timing,
1726
- willRetry,
1727
- signal: controller.signal,
1728
- }));
1729
- if (extensionResult?.cancel) {
1730
- this._emit({
1731
- type: "compaction_end",
1732
- reason,
1733
- result: undefined,
1734
- aborted: true,
1735
- willRetry: false,
1736
- });
1737
- // A cancel produced by an abort (abortCompaction/parent abort) is not an
1738
- // extension decision; only a voluntary cancel hands the risk to the extension.
1739
- return { compacted: false, shouldContinue: false, cancelled: !controller.signal.aborted };
1740
- }
1741
- if (extensionResult?.compaction) {
1742
- extensionCompaction = extensionResult.compaction;
1743
- fromExtension = true;
1744
- }
1745
- }
1746
- let summary;
1747
- let firstKeptEntryId;
1748
- let tokensBefore;
1749
- let usage;
1750
- let details;
1751
- if (extensionCompaction) {
1752
- // Extension provided compaction content
1753
- summary = extensionCompaction.summary;
1754
- firstKeptEntryId = extensionCompaction.firstKeptEntryId;
1755
- tokensBefore = extensionCompaction.tokensBefore;
1756
- usage = extensionCompaction.usage;
1757
- details = extensionCompaction.details;
1758
- }
1759
- else {
1760
- let requestModel = model;
1761
- let apiKey;
1762
- let headers;
1763
- let env;
1764
- if (streamFunction === streamSimple) {
1765
- ({ model: requestModel, apiKey, headers, env } = await this._getRequiredRequestAuth(model));
1766
- }
1767
- else {
1768
- ({
1769
- model: requestModel,
1770
- apiKey,
1771
- headers,
1772
- env,
1773
- } = await this._getSummarizationRequestAuth(model, streamFunction));
1774
- }
1775
- if (controller.signal.aborted) {
1776
- this._emit({
1777
- type: "compaction_end",
1778
- reason,
1779
- result: undefined,
1780
- aborted: true,
1781
- willRetry: false,
1782
- });
1783
- return { compacted: false, shouldContinue: false };
1784
- }
1785
- // Generate compaction result
1786
- const compactResult = await compact(preparation, requestModel, apiKey, headers, undefined, controller.signal, thinkingLevel, streamFunction, env, retrySettings, this._summarizationRetryCallbacks({ source: "compaction", reason }));
1787
- summary = compactResult.summary;
1788
- firstKeptEntryId = compactResult.firstKeptEntryId;
1789
- tokensBefore = compactResult.tokensBefore;
1790
- usage = compactResult.usage;
1791
- details = compactResult.details;
1792
- }
1793
- if (controller.signal.aborted) {
1794
- this._emit({
1795
- type: "compaction_end",
1796
- reason,
1797
- result: undefined,
1798
- aborted: true,
1799
- willRetry: false,
1800
- });
1801
- return { compacted: false, shouldContinue: false };
1802
- }
1803
- this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
1804
- const newEntries = this.sessionManager.getEntries();
1805
- const sessionContext = this.sessionManager.buildSessionContext();
1806
- this.agent.state.messages = sessionContext.messages;
1807
- if (willRetry) {
1808
- const messages = this.agent.state.messages;
1809
- const lastMsg = messages[messages.length - 1];
1810
- // The overflow response was persisted on message_end before _checkCompaction() removed it
1811
- // from agent state. Rebuilding state from the new compaction can restore that kept entry,
1812
- // leaving an assistant as the final message. agent.continue() rejects that state, so remove
1813
- // the retriable error or truncated-length response again before continuing the interrupted turn.
1814
- if (lastMsg?.role === "assistant" &&
1815
- (lastMsg.stopReason === "error" ||
1816
- lastMsg.stopReason === "length")) {
1817
- this.agent.state.messages = messages.slice(0, -1);
1818
- }
1819
- }
1820
- const estimatedTokensAfter = this._estimateContextTokensForCompaction(this.agent.state.messages);
1821
- const contextStillOverThreshold = shouldCompact(estimatedTokensAfter, model.contextWindow, settings);
1822
- const effectiveWillRetry = willRetry && !contextStillOverThreshold;
1823
- // Get the saved compaction entry for the extension event
1824
- const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
1825
- if (this._extensionRunner && savedCompactionEntry) {
1826
- await this._extensionRunner.emit({
1827
- type: "session_compact",
1828
- compactionEntry: savedCompactionEntry,
1829
- fromExtension,
1830
- reason,
1831
- willRetry: effectiveWillRetry,
1832
- });
1833
- }
1834
- const result = {
1835
- summary,
1836
- firstKeptEntryId,
1837
- tokensBefore,
1838
- estimatedTokensAfter,
1839
- usage,
1840
- details,
1841
- };
1842
- this._emit({
1843
- type: "compaction_end",
1844
- reason,
1845
- result,
1846
- aborted: false,
1847
- willRetry: effectiveWillRetry,
1848
- ...(contextStillOverThreshold ? { errorMessage: CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD } : {}),
1849
- });
1850
- if (contextStillOverThreshold) {
1851
- return { compacted: true, shouldContinue: false, mustStop: true };
1852
- }
1853
- if (effectiveWillRetry) {
1854
- return { compacted: true, shouldContinue: true };
1855
- }
1856
- // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
1857
- // Continue once so queued messages are delivered.
1858
- return { compacted: true, shouldContinue: this.agent.hasQueuedMessages() };
1859
- }
1860
- catch (error) {
1861
- const aborted = controller?.signal.aborted || (error instanceof Error && error.name === "AbortError");
1862
- const errorMessage = error instanceof Error ? error.message : "compaction failed";
1863
- if (started) {
1864
- this._emit({
1865
- type: "compaction_end",
1866
- reason,
1867
- result: undefined,
1868
- aborted,
1869
- willRetry: false,
1870
- ...(aborted
1871
- ? {}
1872
- : {
1873
- errorMessage: reason === "overflow"
1874
- ? `Context overflow recovery failed: ${errorMessage}`
1875
- : `Auto-compaction failed: ${errorMessage}`,
1876
- }),
1877
- });
1878
- }
1879
- return { compacted: false, shouldContinue: false };
1880
- }
1881
- finally {
1882
- removeParentAbortListener?.();
1883
- if (this._autoCompactionAbortController === controller) {
1884
- this._autoCompactionAbortController = undefined;
1885
- }
1886
- }
1887
- }
1888
1509
  /**
1889
1510
  * Toggle auto-compaction setting.
1890
1511
  */