@astralyn/pi 0.84.10 → 0.84.12

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 (186) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cli/startup-ui.d.ts.map +1 -1
  3. package/dist/cli/startup-ui.js +2 -1
  4. package/dist/cli/startup-ui.js.map +1 -1
  5. package/dist/core/agent-session.d.ts +62 -8
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +395 -78
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  10. package/dist/core/compaction/branch-summarization.js +4 -3
  11. package/dist/core/compaction/branch-summarization.js.map +1 -1
  12. package/dist/core/compaction/compaction.d.ts +5 -0
  13. package/dist/core/compaction/compaction.d.ts.map +1 -1
  14. package/dist/core/compaction/compaction.js +19 -5
  15. package/dist/core/compaction/compaction.js.map +1 -1
  16. package/dist/core/extensions/index.d.ts +1 -1
  17. package/dist/core/extensions/index.d.ts.map +1 -1
  18. package/dist/core/extensions/index.js.map +1 -1
  19. package/dist/core/extensions/runner.d.ts +5 -0
  20. package/dist/core/extensions/runner.d.ts.map +1 -1
  21. package/dist/core/extensions/runner.js +44 -1
  22. package/dist/core/extensions/runner.js.map +1 -1
  23. package/dist/core/extensions/types.d.ts +18 -10
  24. package/dist/core/extensions/types.d.ts.map +1 -1
  25. package/dist/core/extensions/types.js.map +1 -1
  26. package/dist/core/session-manager.d.ts.map +1 -1
  27. package/dist/core/session-manager.js +4 -2
  28. package/dist/core/session-manager.js.map +1 -1
  29. package/dist/core/settings-manager.d.ts +8 -1
  30. package/dist/core/settings-manager.d.ts.map +1 -1
  31. package/dist/core/settings-manager.js +17 -0
  32. package/dist/core/settings-manager.js.map +1 -1
  33. package/dist/core/tools/bash.d.ts +5 -0
  34. package/dist/core/tools/bash.d.ts.map +1 -1
  35. package/dist/core/tools/bash.js +5 -1
  36. package/dist/core/tools/bash.js.map +1 -1
  37. package/dist/extensions/background/actions.d.ts +21 -0
  38. package/dist/extensions/background/actions.d.ts.map +1 -0
  39. package/dist/extensions/background/actions.js +264 -0
  40. package/dist/extensions/background/actions.js.map +1 -0
  41. package/dist/extensions/background/constants.d.ts +24 -0
  42. package/dist/extensions/background/constants.d.ts.map +1 -0
  43. package/dist/extensions/background/constants.js +24 -0
  44. package/dist/extensions/background/constants.js.map +1 -0
  45. package/dist/extensions/background/index.d.ts +6 -115
  46. package/dist/extensions/background/index.d.ts.map +1 -1
  47. package/dist/extensions/background/index.js +38 -509
  48. package/dist/extensions/background/index.js.map +1 -1
  49. package/dist/extensions/background/manager.d.ts +2 -1
  50. package/dist/extensions/background/manager.d.ts.map +1 -1
  51. package/dist/extensions/background/manager.js +8 -10
  52. package/dist/extensions/background/manager.js.map +1 -1
  53. package/dist/extensions/background/notify.d.ts +16 -0
  54. package/dist/extensions/background/notify.d.ts.map +1 -0
  55. package/dist/extensions/background/notify.js +114 -0
  56. package/dist/extensions/background/notify.js.map +1 -0
  57. package/dist/extensions/background/output-file.d.ts +58 -0
  58. package/dist/extensions/background/output-file.d.ts.map +1 -0
  59. package/dist/extensions/background/output-file.js +122 -0
  60. package/dist/extensions/background/output-file.js.map +1 -0
  61. package/dist/extensions/background/registry.d.ts +40 -72
  62. package/dist/extensions/background/registry.d.ts.map +1 -1
  63. package/dist/extensions/background/registry.js +71 -184
  64. package/dist/extensions/background/registry.js.map +1 -1
  65. package/dist/extensions/background/render.d.ts +3 -10
  66. package/dist/extensions/background/render.d.ts.map +1 -1
  67. package/dist/extensions/background/render.js +20 -60
  68. package/dist/extensions/background/render.js.map +1 -1
  69. package/dist/extensions/background/schema.d.ts +34 -0
  70. package/dist/extensions/background/schema.d.ts.map +1 -0
  71. package/dist/extensions/background/schema.js +89 -0
  72. package/dist/extensions/background/schema.js.map +1 -0
  73. package/dist/extensions/background/task-view.d.ts +48 -0
  74. package/dist/extensions/background/task-view.d.ts.map +1 -0
  75. package/dist/extensions/background/task-view.js +90 -0
  76. package/dist/extensions/background/task-view.js.map +1 -0
  77. package/dist/extensions/background/text.d.ts +15 -0
  78. package/dist/extensions/background/text.d.ts.map +1 -0
  79. package/dist/extensions/background/text.js +31 -0
  80. package/dist/extensions/background/text.js.map +1 -0
  81. package/dist/extensions/background/types.d.ts +74 -0
  82. package/dist/extensions/background/types.d.ts.map +1 -0
  83. package/dist/extensions/background/types.js +17 -0
  84. package/dist/extensions/background/types.js.map +1 -0
  85. package/dist/extensions/llama/client.d.ts +6 -0
  86. package/dist/extensions/llama/client.d.ts.map +1 -1
  87. package/dist/extensions/llama/client.js +7 -0
  88. package/dist/extensions/llama/client.js.map +1 -1
  89. package/dist/extensions/llama/provider.d.ts +3 -1
  90. package/dist/extensions/llama/provider.d.ts.map +1 -1
  91. package/dist/extensions/llama/provider.js +27 -6
  92. package/dist/extensions/llama/provider.js.map +1 -1
  93. package/dist/extensions/subagent/activity.d.ts +5 -0
  94. package/dist/extensions/subagent/activity.d.ts.map +1 -1
  95. package/dist/extensions/subagent/activity.js +5 -0
  96. package/dist/extensions/subagent/activity.js.map +1 -1
  97. package/dist/extensions/subagent/agents-command.d.ts.map +1 -1
  98. package/dist/extensions/subagent/agents-command.js +6 -391
  99. package/dist/extensions/subagent/agents-command.js.map +1 -1
  100. package/dist/extensions/subagent/constants.d.ts +1 -1
  101. package/dist/extensions/subagent/constants.d.ts.map +1 -1
  102. package/dist/extensions/subagent/constants.js +2 -1
  103. package/dist/extensions/subagent/constants.js.map +1 -1
  104. package/dist/extensions/subagent/index.js +2 -2
  105. package/dist/extensions/subagent/index.js.map +1 -1
  106. package/dist/extensions/subagent/render.d.ts +5 -5
  107. package/dist/extensions/subagent/render.d.ts.map +1 -1
  108. package/dist/extensions/subagent/render.js +106 -36
  109. package/dist/extensions/subagent/render.js.map +1 -1
  110. package/dist/extensions/subagent/resolve.d.ts.map +1 -1
  111. package/dist/extensions/subagent/resolve.js +2 -4
  112. package/dist/extensions/subagent/resolve.js.map +1 -1
  113. package/dist/extensions/subagent/runner.d.ts +1 -1
  114. package/dist/extensions/subagent/runner.d.ts.map +1 -1
  115. package/dist/extensions/subagent/runner.js +8 -8
  116. package/dist/extensions/subagent/runner.js.map +1 -1
  117. package/dist/extensions/subagent/sdk-runner.d.ts +11 -0
  118. package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
  119. package/dist/extensions/subagent/sdk-runner.js +90 -79
  120. package/dist/extensions/subagent/sdk-runner.js.map +1 -1
  121. package/dist/extensions/subagent/state.d.ts.map +1 -1
  122. package/dist/extensions/subagent/state.js +145 -126
  123. package/dist/extensions/subagent/state.js.map +1 -1
  124. package/dist/extensions/subagent/text.d.ts.map +1 -1
  125. package/dist/extensions/subagent/text.js +4 -0
  126. package/dist/extensions/subagent/text.js.map +1 -1
  127. package/dist/extensions/subagent/ui/agents-dialog.d.ts +4 -0
  128. package/dist/extensions/subagent/ui/agents-dialog.d.ts.map +1 -0
  129. package/dist/extensions/subagent/ui/agents-dialog.js +84 -0
  130. package/dist/extensions/subagent/ui/agents-dialog.js.map +1 -0
  131. package/dist/extensions/subagent/ui/choice-menu.d.ts +39 -0
  132. package/dist/extensions/subagent/ui/choice-menu.d.ts.map +1 -0
  133. package/dist/extensions/subagent/ui/choice-menu.js +93 -0
  134. package/dist/extensions/subagent/ui/choice-menu.js.map +1 -0
  135. package/dist/extensions/subagent/ui/model-picker.d.ts +47 -0
  136. package/dist/extensions/subagent/ui/model-picker.d.ts.map +1 -0
  137. package/dist/extensions/subagent/ui/model-picker.js +142 -0
  138. package/dist/extensions/subagent/ui/model-picker.js.map +1 -0
  139. package/dist/extensions/subagent/ui/profile-settings-menu.d.ts +41 -0
  140. package/dist/extensions/subagent/ui/profile-settings-menu.d.ts.map +1 -0
  141. package/dist/extensions/subagent/ui/profile-settings-menu.js +98 -0
  142. package/dist/extensions/subagent/ui/profile-settings-menu.js.map +1 -0
  143. package/dist/index.d.ts +2 -1
  144. package/dist/index.d.ts.map +1 -1
  145. package/dist/index.js +1 -0
  146. package/dist/index.js.map +1 -1
  147. package/dist/main.d.ts.map +1 -1
  148. package/dist/main.js +2 -0
  149. package/dist/main.js.map +1 -1
  150. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  151. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  152. package/dist/modes/interactive/components/settings-selector.js +10 -0
  153. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  154. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  155. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  156. package/dist/modes/interactive/interactive-mode.js +71 -58
  157. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  158. package/dist/modes/rpc/rpc-client.d.ts +7 -0
  159. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  160. package/dist/modes/rpc/rpc-client.js +7 -0
  161. package/dist/modes/rpc/rpc-client.js.map +1 -1
  162. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  163. package/dist/modes/rpc/rpc-mode.js +3 -0
  164. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  165. package/dist/modes/rpc/rpc-types.d.ts +12 -0
  166. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  167. package/dist/modes/rpc/rpc-types.js.map +1 -1
  168. package/dist/utils/shell.d.ts.map +1 -1
  169. package/dist/utils/shell.js +6 -4
  170. package/dist/utils/shell.js.map +1 -1
  171. package/docs/bundled/extensions/subagent.md +1 -1
  172. package/docs/compaction.md +5 -12
  173. package/docs/environment-variables.md +3 -0
  174. package/docs/extensions.md +21 -5
  175. package/docs/keybindings.md +1 -1
  176. package/docs/quickstart.md +1 -1
  177. package/docs/rpc.md +23 -0
  178. package/docs/settings.md +7 -0
  179. package/docs/terminal-setup.md +12 -0
  180. package/docs/usage.md +4 -3
  181. package/npm-shrinkwrap.json +29 -29
  182. package/package.json +6 -6
  183. package/dist/core/compaction-controller.d.ts +0 -90
  184. package/dist/core/compaction-controller.d.ts.map +0 -1
  185. package/dist/core/compaction-controller.js +0 -471
  186. package/dist/core/compaction-controller.js.map +0 -1
@@ -15,15 +15,14 @@
15
15
  import { readFileSync } 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, isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, } from "@earendil-works/pi-ai/compat";
18
+ import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRecoverableLength, 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 { sleep } from "../utils/sleep.js";
22
22
  import { normalizeToolResultImages } from "../utils/tool-result-images.js";
23
23
  import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
24
24
  import { executeBashWithOperations } from "./bash-executor.js";
25
- import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
26
- import { CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD, CompactionController, estimateMessagesTokens, } from "./compaction-controller.js";
25
+ import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
27
26
  import { DEFAULT_THINKING_LEVEL, THINKING_LEVEL_OPTIONS } from "./defaults.js";
28
27
  import { exportSessionToHtml } from "./export-html/index.js";
29
28
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
@@ -62,6 +61,13 @@ function withoutDeletedHeaders(headers) {
62
61
  ? Object.fromEntries(Object.entries(headers).filter((entry) => entry[1] !== null))
63
62
  : undefined;
64
63
  }
64
+ function estimateMessagesTokens(messages) {
65
+ let tokens = 0;
66
+ for (const message of messages) {
67
+ tokens += estimateTokens(message);
68
+ }
69
+ return tokens;
70
+ }
65
71
  // ============================================================================
66
72
  // Constants
67
73
  // ============================================================================
@@ -78,8 +84,12 @@ export class AgentSession {
78
84
  this._followUpMessages = [];
79
85
  /** Messages queued to be included with the next user prompt as context ("asides"). */
80
86
  this._pendingNextTurnMessages = [];
87
+ /** Context-only custom messages queued during a run, flushed once the current turn's tool results are in. */
88
+ this._pendingCustomMessages = [];
81
89
  // Compaction state
82
90
  this._compactionAbortController = undefined;
91
+ this._autoCompactionAbortController = undefined;
92
+ this._overflowRecoveryAttempted = false;
83
93
  // Branch summarization state
84
94
  this._branchSummaryAbortController = undefined;
85
95
  // Retry state
@@ -105,7 +115,7 @@ export class AgentSession {
105
115
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
106
116
  // This ensures the UI sees the updated queue state
107
117
  if (event.type === "message_start" && event.message.role === "user") {
108
- this._compaction.resetOverflowRecovery();
118
+ this._overflowRecoveryAttempted = false;
109
119
  const messageText = contentText(event.message.content, "");
110
120
  if (messageText) {
111
121
  // Check steering queue first
@@ -147,7 +157,7 @@ export class AgentSession {
147
157
  this._lastAssistantMessage = event.message;
148
158
  const assistantMsg = event.message;
149
159
  if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "length") {
150
- this._compaction.resetOverflowRecovery();
160
+ this._overflowRecoveryAttempted = false;
151
161
  }
152
162
  // Reset retry counter immediately on successful assistant response
153
163
  // This prevents accumulation across multiple LLM calls within a turn
@@ -161,6 +171,14 @@ export class AgentSession {
161
171
  }
162
172
  }
163
173
  }
174
+ // A turn ends after its assistant message and every tool result has been appended,
175
+ // so this is the first point in the run where a context-only custom message can be
176
+ // inserted without landing between a tool call and its result. Flushing after the
177
+ // extension and listener dispatch above also picks up messages that turn_end
178
+ // handlers queued.
179
+ if (event.type === "turn_end") {
180
+ this._flushPendingCustomMessages();
181
+ }
164
182
  };
165
183
  this.agent = config.agent;
166
184
  this.sessionManager = config.sessionManager;
@@ -176,20 +194,6 @@ export class AgentSession {
176
194
  this._excludedToolNames = config.excludedToolNames ? new Set(config.excludedToolNames) : undefined;
177
195
  this._baseToolsOverride = config.baseToolsOverride;
178
196
  this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
179
- this._compaction = new CompactionController({
180
- agent: this.agent,
181
- sessionManager: this.sessionManager,
182
- settingsManager: this.settingsManager,
183
- getModel: () => this.model,
184
- getThinkingLevel: () => this.thinkingLevel,
185
- getExtensionRunner: () => this._extensionRunner,
186
- isManualCompactionActive: () => this._compactionAbortController !== undefined,
187
- emit: (event) => this._emit(event),
188
- emitSessionCompactFailed: (event) => this._emitSessionCompactFailed(event),
189
- getRequiredRequestAuth: (model) => this._getRequiredRequestAuth(model),
190
- getSummarizationRequestAuth: (model, streamFunction) => this._getSummarizationRequestAuth(model, streamFunction),
191
- summarizationRetryCallbacks: (source) => this._summarizationRetryCallbacks(source),
192
- });
193
197
  // Always subscribe to agent events for internal handling
194
198
  // (session persistence, extensions, auto-compaction, retry logic)
195
199
  this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
@@ -232,6 +236,14 @@ export class AgentSession {
232
236
  }
233
237
  throw new Error(formatNoApiKeyFoundMessage(model.provider));
234
238
  }
239
+ /**
240
+ * Resolve summarization auth against a caller-supplied stream function.
241
+ *
242
+ * Callers that await this while the session stays live must capture
243
+ * `this.agent.streamFunction` first and pass it in: a model switch during the
244
+ * await would otherwise select auth for one stream function and generate the
245
+ * summary with another.
246
+ */
235
247
  async _getSummarizationRequestAuth(model, streamFunction = this.agent.streamFunction) {
236
248
  if (streamFunction === streamSimple) {
237
249
  return this._getRequiredRequestAuth(model);
@@ -311,35 +323,33 @@ export class AgentSession {
311
323
  };
312
324
  };
313
325
  }
326
+ async _compactBeforeNextAssistantResponse(context) {
327
+ const model = this.model;
328
+ const settings = this.settingsManager.getCompactionSettings();
329
+ if (!model ||
330
+ model.contextWindow <= 0 ||
331
+ !shouldCompact(estimateContextTokens(context.messages).tokens, model.contextWindow, settings)) {
332
+ return context;
333
+ }
334
+ await this._runAutoCompaction("threshold", false);
335
+ return {
336
+ ...context,
337
+ messages: this.agent.state.messages.slice(),
338
+ };
339
+ }
314
340
  _installAgentNextTurnRefresh() {
315
341
  const previousPrepareNextTurnWithContext = this.agent.prepareNextTurnWithContext ??
316
342
  (this.agent.prepareNextTurn
317
343
  ? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
318
344
  : undefined);
319
345
  this.agent.prepareNextTurnWithContext = async (turn, signal) => {
320
- const compaction = await this._compaction.maybeCompactBeforeNextToolTurn(turn, signal);
321
- if (compaction.stopErrorMessage) {
322
- // Agent does not expose the low-level graceful turn-stop hook. Throwing here
323
- // exits before queue polling or another provider request; the run records a
324
- // normal error/aborted lifecycle instead of risking an oversized context.
325
- this._compaction.requestStopAfterTurn();
326
- throw new Error(compaction.stopErrorMessage);
327
- }
328
- const effectiveTurn = compaction.messages === undefined
329
- ? turn
330
- : {
331
- ...turn,
332
- context: {
333
- ...turn.context,
334
- messages: compaction.messages,
335
- },
336
- };
337
- const previousSnapshot = await previousPrepareNextTurnWithContext?.(effectiveTurn, signal);
338
- const previousContext = previousSnapshot?.context ?? effectiveTurn.context;
346
+ const context = await this._compactBeforeNextAssistantResponse(turn.context);
347
+ const previousSnapshot = await previousPrepareNextTurnWithContext?.({ ...turn, context }, signal);
348
+ const nextContext = previousSnapshot?.context ?? context;
339
349
  return {
340
350
  ...previousSnapshot,
341
351
  context: {
342
- ...previousContext,
352
+ ...nextContext,
343
353
  systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
344
354
  tools: this.agent.state.tools.slice(),
345
355
  },
@@ -643,7 +653,7 @@ export class AgentSession {
643
653
  }
644
654
  /** Whether compaction or branch summarization is currently running */
645
655
  get isCompacting() {
646
- return (this._compaction.isAutoCompacting ||
656
+ return (this._autoCompactionAbortController !== undefined ||
647
657
  this._compactionAbortController !== undefined ||
648
658
  this._branchSummaryAbortController !== undefined);
649
659
  }
@@ -741,7 +751,6 @@ export class AgentSession {
741
751
  // =========================================================================
742
752
  async _runAgentPrompt(messages) {
743
753
  this._isAgentRunActive = true;
744
- this._compaction.noteRunStart();
745
754
  try {
746
755
  await this.agent.prompt(messages);
747
756
  while (await this._handlePostAgentRun()) {
@@ -751,6 +760,7 @@ export class AgentSession {
751
760
  finally {
752
761
  this._systemPromptOverride = undefined;
753
762
  this._flushPendingBashMessages();
763
+ this._flushPendingCustomMessages();
754
764
  await this._emitAgentSettled();
755
765
  }
756
766
  }
@@ -758,10 +768,6 @@ export class AgentSession {
758
768
  const msg = this._lastAssistantMessage;
759
769
  this._lastAssistantMessage = undefined;
760
770
  if (!msg) {
761
- this._compaction.consumeStopAfterTurn();
762
- return false;
763
- }
764
- if (this._compaction.consumeStopAfterTurn()) {
765
771
  return false;
766
772
  }
767
773
  if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
@@ -776,12 +782,9 @@ export class AgentSession {
776
782
  });
777
783
  this._retryAttempt = 0;
778
784
  }
779
- if (await this._compaction.checkCompaction(msg)) {
785
+ if (await this._checkCompaction(msg)) {
780
786
  return true;
781
787
  }
782
- if (this._compaction.consumeStopAfterTurn()) {
783
- return false;
784
- }
785
788
  // The agent loop drains both queues before emitting agent_end. Any messages
786
789
  // here were queued by agent_end extension handlers and need a continuation.
787
790
  return this.agent.hasQueuedMessages();
@@ -847,8 +850,9 @@ export class AgentSession {
847
850
  preflightResult?.(true);
848
851
  return;
849
852
  }
850
- // Flush any pending bash messages before the new prompt
853
+ // Flush any pending bash and custom messages before the new prompt
851
854
  this._flushPendingBashMessages();
855
+ this._flushPendingCustomMessages();
852
856
  // Validate model
853
857
  if (!this.model) {
854
858
  throw new Error(formatNoModelSelectedMessage());
@@ -868,14 +872,7 @@ export class AgentSession {
868
872
  // The user's new prompt is sent below, so do not call agent.continue() here.
869
873
  const lastAssistant = this._findLastAssistantMessage();
870
874
  if (lastAssistant) {
871
- await this._compaction.checkCompaction(lastAssistant, false, "prePrompt");
872
- }
873
- const compactionSettings = this.settingsManager.getCompactionSettings();
874
- // A compaction entry can be valid while its retained suffix is still too large.
875
- // Refuse the existing context here rather than discovering that via provider overflow.
876
- if (compactionSettings.enabled &&
877
- shouldCompact(this._compaction.estimateContextTokens(this.agent.state.messages), this.model.contextWindow, compactionSettings)) {
878
- throw new Error(CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD);
875
+ await this._checkCompaction(lastAssistant, false);
879
876
  }
880
877
  // Build messages array (custom message if any, then user message)
881
878
  messages = [];
@@ -1069,8 +1066,9 @@ export class AgentSession {
1069
1066
  /**
1070
1067
  * Send a custom message to the session. Creates a CustomMessageEntry.
1071
1068
  *
1072
- * Handles three cases:
1069
+ * Handles four cases:
1073
1070
  * - Streaming: queues message, processed when loop pulls from queue
1071
+ * - Streaming + triggerTurn false: appended to state/session once the current turn ends
1074
1072
  * - Not streaming + triggerTurn: appends to state/session, starts new turn
1075
1073
  * - Not streaming + no trigger: appends to state/session, no turn
1076
1074
  *
@@ -1102,11 +1100,34 @@ export class AgentSession {
1102
1100
  else if (options?.triggerTurn) {
1103
1101
  await this._runAgentPrompt(appMessage);
1104
1102
  }
1103
+ else if (this.isStreaming) {
1104
+ // Appending now would put the message between an assistant tool call and its
1105
+ // result, which providers that validate message order reject on replay. Defer
1106
+ // to the end of the turn. Nothing is emitted yet: message events must not
1107
+ // describe messages the session tree does not contain.
1108
+ this._pendingCustomMessages.push(appMessage);
1109
+ }
1105
1110
  else {
1106
- this.agent.state.messages.push(appMessage);
1107
- this.sessionManager.appendCustomMessageEntry(message.customType, message.content, message.display, message.details);
1108
- this._emit({ type: "message_start", message: appMessage });
1109
- this._emit({ type: "message_end", message: appMessage });
1111
+ this._appendCustomMessage(appMessage);
1112
+ }
1113
+ }
1114
+ _appendCustomMessage(appMessage) {
1115
+ this.agent.state.messages.push(appMessage);
1116
+ this.sessionManager.appendCustomMessageEntry(appMessage.customType, appMessage.content, appMessage.display, appMessage.details);
1117
+ this._emit({ type: "message_start", message: appMessage });
1118
+ this._emit({ type: "message_end", message: appMessage });
1119
+ }
1120
+ /**
1121
+ * Append custom messages queued while the agent was running.
1122
+ * Called once the current turn's tool results are in agent state and session history.
1123
+ */
1124
+ _flushPendingCustomMessages() {
1125
+ if (this._pendingCustomMessages.length === 0)
1126
+ return;
1127
+ const pending = this._pendingCustomMessages;
1128
+ this._pendingCustomMessages = [];
1129
+ for (const appMessage of pending) {
1130
+ this._appendCustomMessage(appMessage);
1110
1131
  }
1111
1132
  }
1112
1133
  /**
@@ -1139,7 +1160,6 @@ export class AgentSession {
1139
1160
  if (images.length === 0)
1140
1161
  images = undefined;
1141
1162
  }
1142
- // Use prompt() with the requested command and template expansion behavior
1143
1163
  await this.prompt(text, {
1144
1164
  expandPromptTemplates: options?.expandPromptTemplates ?? false,
1145
1165
  streamingBehavior: options?.deliverAs,
@@ -1181,7 +1201,6 @@ export class AgentSession {
1181
1201
  */
1182
1202
  async abort() {
1183
1203
  this.abortRetry();
1184
- this.abortCompaction();
1185
1204
  this.agent.abort();
1186
1205
  await this.waitForIdle();
1187
1206
  }
@@ -1220,6 +1239,7 @@ export class AgentSession {
1220
1239
  this.sessionManager.appendModelChange(model.provider, model.id);
1221
1240
  if (options.persist) {
1222
1241
  this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
1242
+ this._addPersistedDefaultToNonEmptyScope(model);
1223
1243
  }
1224
1244
  // Apply thinking level for the new model.
1225
1245
  // Per-model thinking level overrides take priority over the global default.
@@ -1227,6 +1247,20 @@ export class AgentSession {
1227
1247
  this.setThinkingLevel(thinkingLevel);
1228
1248
  await this._emitModelSelect(model, previousModel, "set");
1229
1249
  }
1250
+ _addPersistedDefaultToNonEmptyScope(model) {
1251
+ if (this._scopedModels.length === 0)
1252
+ return;
1253
+ if (this._scopedModels.some((scoped) => modelsAreEqual(scoped.model, model)))
1254
+ return;
1255
+ this._scopedModels = [...this._scopedModels, { model }];
1256
+ const enabledModels = this.settingsManager.getEnabledModels();
1257
+ if (!enabledModels?.length)
1258
+ return;
1259
+ const modelReference = `${model.provider}/${model.id}`;
1260
+ if (enabledModels.some((pattern) => pattern.toLowerCase() === modelReference.toLowerCase()))
1261
+ return;
1262
+ this.settingsManager.setEnabledModels([...enabledModels, modelReference]);
1263
+ }
1230
1264
  /**
1231
1265
  * Cycle to next/previous model.
1232
1266
  * Uses scoped models (from --models flag) if available, otherwise all available models.
@@ -1257,6 +1291,7 @@ export class AgentSession {
1257
1291
  this.sessionManager.appendModelChange(next.model.provider, next.model.id);
1258
1292
  if (options.persist) {
1259
1293
  this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
1294
+ this._addPersistedDefaultToNonEmptyScope(next.model);
1260
1295
  }
1261
1296
  // Apply thinking level for the new model.
1262
1297
  // - Explicit scoped model thinking level overrides defaults
@@ -1283,6 +1318,7 @@ export class AgentSession {
1283
1318
  this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
1284
1319
  if (options.persist) {
1285
1320
  this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
1321
+ this._addPersistedDefaultToNonEmptyScope(nextModel);
1286
1322
  }
1287
1323
  // Apply thinking level for the new model.
1288
1324
  // Model persistence does not implicitly rewrite the global thinking default.
@@ -1390,18 +1426,19 @@ export class AgentSession {
1390
1426
  // =========================================================================
1391
1427
  // Compaction
1392
1428
  // =========================================================================
1393
- /** Generate Pi's built-in manual compaction summary. */
1394
- async _runDefaultCompaction(preparation, requestModel, apiKey, headers, customInstructions, signal, env, reason) {
1395
- return compact(preparation, requestModel, apiKey, headers, customInstructions, signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }), undefined);
1429
+ /** Generate Pi's built-in compaction summary for manual and automatic compaction. */
1430
+ async _runDefaultCompaction(preparation, requestModel, apiKey, headers, customInstructions, signal, env, reason, streamFunction = this.agent.streamFunction) {
1431
+ return compact(preparation, requestModel, apiKey, headers, customInstructions, signal, this.thinkingLevel, streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }), undefined);
1396
1432
  }
1397
1433
  /**
1398
1434
  * Manually compact the session context.
1399
1435
  *
1400
1436
  * This is the manual entry point used by `/compact`, RPC, and extensions. It is
1401
- * separate from automatic threshold/overflow compaction, which is delegated to
1402
- * CompactionController. After preparation and the `session_before_compact` hook,
1403
- * the default manual path calls the lower-level `compact()` function imported
1404
- * from `./compaction/index.ts`, unless the hook cancels or supplies a custom result.
1437
+ * separate from automatic threshold/overflow compaction, which enters through
1438
+ * `_checkCompaction()` and `_runAutoCompaction()`. After preparation and the
1439
+ * `session_before_compact` hook, both paths call the lower-level `compact()`
1440
+ * function imported from `./compaction/index.ts`, unless the hook cancels or
1441
+ * supplies a custom result.
1405
1442
  *
1406
1443
  * Aborts the current agent operation first. Manual compaction never retries or
1407
1444
  * continues the interrupted agent turn.
@@ -1437,7 +1474,6 @@ export class AgentSession {
1437
1474
  branchEntries: pathEntries,
1438
1475
  customInstructions,
1439
1476
  reason: "manual",
1440
- timing: "manual",
1441
1477
  willRetry: false,
1442
1478
  signal: this._compactionAbortController.signal,
1443
1479
  }));
@@ -1540,7 +1576,7 @@ export class AgentSession {
1540
1576
  */
1541
1577
  abortCompaction() {
1542
1578
  this._compactionAbortController?.abort();
1543
- this._compaction.abort();
1579
+ this._autoCompactionAbortController?.abort();
1544
1580
  }
1545
1581
  /**
1546
1582
  * Cancel in-progress branch summarization.
@@ -1548,6 +1584,291 @@ export class AgentSession {
1548
1584
  abortBranchSummary() {
1549
1585
  this._branchSummaryAbortController?.abort();
1550
1586
  }
1587
+ /**
1588
+ * Dispatch automatic compaction after `agent_end` or before prompt submission.
1589
+ * Manual compaction does not call this method; it enters through `compact()`.
1590
+ *
1591
+ * Automatic cases:
1592
+ * 1. Overflow with retry: a context-overflow error or recoverable length stop;
1593
+ * remove the failed assistant message, compact, and retry the turn once.
1594
+ * 2. Overflow without retry: a successful response exceeded the configured
1595
+ * context window; compact but preserve the completed response.
1596
+ * 3. Threshold without retry: valid or estimated context usage crossed the
1597
+ * configured threshold; compact without retrying the completed response.
1598
+ *
1599
+ * Each case calls `_runAutoCompaction()`. After preparation and the
1600
+ * `session_before_compact` hook, that method calls the lower-level `compact()`
1601
+ * function imported from `./compaction/index.ts`, unless the hook cancels or
1602
+ * supplies a custom result.
1603
+ *
1604
+ * @param assistantMessage The assistant message to check
1605
+ * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
1606
+ * @returns Whether the post-run loop should call `agent.continue()` for overflow recovery or queued messages
1607
+ */
1608
+ async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
1609
+ const settings = this.settingsManager.getCompactionSettings();
1610
+ if (!settings.enabled)
1611
+ return false;
1612
+ // Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
1613
+ if (skipAbortedCheck && assistantMessage.stopReason === "aborted")
1614
+ return false;
1615
+ const contextWindow = this.model?.contextWindow ?? 0;
1616
+ // Skip overflow check if the message came from a different model.
1617
+ // This handles the case where user switched from a smaller-context model (e.g. opus)
1618
+ // to a larger-context model (e.g. codex) - the overflow error from the old model
1619
+ // shouldn't trigger compaction for the new model.
1620
+ const sameModel = this.model && assistantMessage.provider === this.model.provider && assistantMessage.model === this.model.id;
1621
+ // Skip compaction checks if this assistant message is older than the latest
1622
+ // compaction boundary. This prevents a stale pre-compaction usage/error
1623
+ // from retriggering compaction on the first prompt after compaction.
1624
+ const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
1625
+ const assistantIsFromBeforeCompaction = compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
1626
+ if (assistantIsFromBeforeCompaction) {
1627
+ return false;
1628
+ }
1629
+ // Automatic cases 1 and 2: context overflow.
1630
+ // A length stop is recoverable when output ended below the model's original desired limit,
1631
+ // independent of the configured context size or any context-clamped provider request limit.
1632
+ const contextOverflow = sameModel && isContextOverflow(assistantMessage, contextWindow);
1633
+ const recoverableLength = sameModel && isRecoverableLength(assistantMessage, this.model?.maxTokens ?? 0);
1634
+ if (contextOverflow || recoverableLength) {
1635
+ const willRetry = assistantMessage.stopReason !== "stop";
1636
+ // Case 2: the response completed successfully. Compact, but do not retry because
1637
+ // agent.continue() cannot continue from a completed assistant response.
1638
+ if (!willRetry) {
1639
+ return await this._runAutoCompaction("overflow", false);
1640
+ }
1641
+ if (this._overflowRecoveryAttempted) {
1642
+ const errorMessage = contextOverflow
1643
+ ? "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model."
1644
+ : "Truncated response recovery failed after one compact-and-retry attempt.";
1645
+ this._emit({
1646
+ type: "compaction_end",
1647
+ reason: "overflow",
1648
+ result: undefined,
1649
+ aborted: false,
1650
+ willRetry: false,
1651
+ errorMessage,
1652
+ });
1653
+ await this._emitSessionCompactFailed({
1654
+ reason: "overflow",
1655
+ errorMessage,
1656
+ aborted: false,
1657
+ willRetry: false,
1658
+ fromExtension: false,
1659
+ });
1660
+ return false;
1661
+ }
1662
+ // Case 1: remove the failed or truncated message from agent state, compact, and
1663
+ // retry once. The message remains in session history but is excluded from retry context.
1664
+ this._overflowRecoveryAttempted = true;
1665
+ const messages = this.agent.state.messages;
1666
+ if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
1667
+ this.agent.state.messages = messages.slice(0, -1);
1668
+ }
1669
+ return await this._runAutoCompaction("overflow", willRetry);
1670
+ }
1671
+ // Case 3: threshold compaction without retry.
1672
+ // For error messages or all-zero usage messages, estimate from the last valid response.
1673
+ // This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage
1674
+ // responses can still compact and do not reset context accounting.
1675
+ let contextTokens;
1676
+ const directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0;
1677
+ if (assistantMessage.stopReason === "error" || directContextTokens === 0) {
1678
+ const messages = this.agent.state.messages;
1679
+ const estimate = estimateContextTokens(messages);
1680
+ // Without provider usage, estimate.tokens is the pure message-size estimate.
1681
+ // Only usage-backed estimates need the stale pre-compaction check.
1682
+ if (estimate.lastUsageIndex !== null) {
1683
+ // Verify the usage source is post-compaction. Kept pre-compaction messages
1684
+ // have stale usage reflecting the old (larger) context and would falsely
1685
+ // trigger compaction right after one just finished.
1686
+ const usageMsg = messages[estimate.lastUsageIndex];
1687
+ if (compactionEntry &&
1688
+ usageMsg.role === "assistant" &&
1689
+ usageMsg.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
1690
+ return false;
1691
+ }
1692
+ }
1693
+ contextTokens = estimate.tokens;
1694
+ }
1695
+ else {
1696
+ contextTokens = directContextTokens;
1697
+ }
1698
+ if (shouldCompact(contextTokens, contextWindow, settings)) {
1699
+ return await this._runAutoCompaction("threshold", false);
1700
+ }
1701
+ return false;
1702
+ }
1703
+ /**
1704
+ * Execute threshold or overflow compaction. Manual compaction uses
1705
+ * `AgentSession.compact()` instead. Both paths call the lower-level `compact()`
1706
+ * function imported from `./compaction/index.ts` after preparation and extension
1707
+ * interception.
1708
+ *
1709
+ * @param reason Automatic trigger selected by `_checkCompaction()`
1710
+ * @param willRetry Whether to continue the interrupted turn after overflow compaction
1711
+ * @returns Whether the post-run loop should call `agent.continue()`
1712
+ */
1713
+ async _runAutoCompaction(reason, willRetry) {
1714
+ const settings = this.settingsManager.getCompactionSettings();
1715
+ let started = false;
1716
+ let fromExtension = false;
1717
+ try {
1718
+ if (!this.model) {
1719
+ return false;
1720
+ }
1721
+ // Capture before awaiting auth: a model switch during the await would leave the
1722
+ // resolved auth and the summary generator disagreeing about the stream function.
1723
+ const streamFunction = this.agent.streamFunction;
1724
+ const { model: requestModel, apiKey, headers, env, } = await this._getSummarizationRequestAuth(this.model, streamFunction);
1725
+ const pathEntries = this.sessionManager.getBranch();
1726
+ const preparation = prepareCompaction(pathEntries, settings);
1727
+ if (!preparation) {
1728
+ return false;
1729
+ }
1730
+ this._emit({ type: "compaction_start", reason });
1731
+ this._autoCompactionAbortController = new AbortController();
1732
+ started = true;
1733
+ let extensionCompaction;
1734
+ if (this._extensionRunner.hasHandlers("session_before_compact")) {
1735
+ const extensionResult = (await this._extensionRunner.emit({
1736
+ type: "session_before_compact",
1737
+ preparation,
1738
+ branchEntries: pathEntries,
1739
+ customInstructions: undefined,
1740
+ reason,
1741
+ willRetry,
1742
+ signal: this._autoCompactionAbortController.signal,
1743
+ }));
1744
+ if (extensionResult?.cancel) {
1745
+ this._emit({
1746
+ type: "compaction_end",
1747
+ reason,
1748
+ result: undefined,
1749
+ aborted: true,
1750
+ willRetry: false,
1751
+ });
1752
+ await this._emitSessionCompactFailed({
1753
+ reason,
1754
+ aborted: true,
1755
+ willRetry: false,
1756
+ fromExtension: false,
1757
+ });
1758
+ return false;
1759
+ }
1760
+ if (extensionResult?.compaction) {
1761
+ extensionCompaction = extensionResult.compaction;
1762
+ fromExtension = true;
1763
+ }
1764
+ }
1765
+ let summary;
1766
+ let firstKeptEntryId;
1767
+ let tokensBefore;
1768
+ let usage;
1769
+ let details;
1770
+ if (extensionCompaction) {
1771
+ // Extension provided compaction content
1772
+ summary = extensionCompaction.summary;
1773
+ firstKeptEntryId = extensionCompaction.firstKeptEntryId;
1774
+ tokensBefore = extensionCompaction.tokensBefore;
1775
+ usage = extensionCompaction.usage;
1776
+ details = extensionCompaction.details;
1777
+ }
1778
+ else {
1779
+ // Shared default summary generator, also used by manual compaction.
1780
+ const compactResult = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, undefined, this._autoCompactionAbortController.signal, env, reason, streamFunction);
1781
+ summary = compactResult.summary;
1782
+ firstKeptEntryId = compactResult.firstKeptEntryId;
1783
+ tokensBefore = compactResult.tokensBefore;
1784
+ usage = compactResult.usage;
1785
+ details = compactResult.details;
1786
+ }
1787
+ if (this._autoCompactionAbortController.signal.aborted) {
1788
+ this._emit({
1789
+ type: "compaction_end",
1790
+ reason,
1791
+ result: undefined,
1792
+ aborted: true,
1793
+ willRetry: false,
1794
+ });
1795
+ await this._emitSessionCompactFailed({
1796
+ reason,
1797
+ aborted: true,
1798
+ willRetry: false,
1799
+ fromExtension,
1800
+ });
1801
+ return 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
+ const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
1808
+ // Get the saved compaction entry for the extension event
1809
+ const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
1810
+ if (this._extensionRunner && savedCompactionEntry) {
1811
+ await this._extensionRunner.emit({
1812
+ type: "session_compact",
1813
+ compactionEntry: savedCompactionEntry,
1814
+ fromExtension,
1815
+ reason,
1816
+ willRetry,
1817
+ });
1818
+ }
1819
+ const result = {
1820
+ summary,
1821
+ firstKeptEntryId,
1822
+ tokensBefore,
1823
+ estimatedTokensAfter,
1824
+ usage,
1825
+ details,
1826
+ };
1827
+ this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
1828
+ if (willRetry) {
1829
+ const messages = this.agent.state.messages;
1830
+ const lastMsg = messages[messages.length - 1];
1831
+ // The overflow response was persisted on message_end before _checkCompaction() removed it
1832
+ // from agent state. Rebuilding state from the new compaction can restore that kept entry,
1833
+ // leaving an assistant as the final message. agent.continue() rejects that state, so remove
1834
+ // the retriable error or truncated-length response again before continuing the interrupted turn.
1835
+ if (lastMsg?.role === "assistant" && (lastMsg.stopReason === "error" || lastMsg.stopReason === "length")) {
1836
+ this.agent.state.messages = messages.slice(0, -1);
1837
+ }
1838
+ return true;
1839
+ }
1840
+ // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
1841
+ // Continue once so queued messages are delivered.
1842
+ return this.agent.hasQueuedMessages();
1843
+ }
1844
+ catch (error) {
1845
+ const errorMessage = error instanceof Error ? error.message : "compaction failed";
1846
+ if (started) {
1847
+ const formattedErrorMessage = reason === "overflow"
1848
+ ? `Context overflow recovery failed: ${errorMessage}`
1849
+ : `Auto-compaction failed: ${errorMessage}`;
1850
+ this._emit({
1851
+ type: "compaction_end",
1852
+ reason,
1853
+ result: undefined,
1854
+ aborted: false,
1855
+ willRetry: false,
1856
+ errorMessage: formattedErrorMessage,
1857
+ });
1858
+ await this._emitSessionCompactFailed({
1859
+ reason,
1860
+ errorMessage: formattedErrorMessage,
1861
+ aborted: false,
1862
+ willRetry: false,
1863
+ fromExtension,
1864
+ });
1865
+ }
1866
+ return false;
1867
+ }
1868
+ finally {
1869
+ this._autoCompactionAbortController = undefined;
1870
+ }
1871
+ }
1551
1872
  /**
1552
1873
  * Toggle auto-compaction setting.
1553
1874
  */
@@ -1742,10 +2063,6 @@ export class AgentSession {
1742
2063
  },
1743
2064
  getSystemPrompt: () => this.systemPrompt,
1744
2065
  getSystemPromptOptions: () => this._baseSystemPromptOptions,
1745
- getShellSettings: () => ({
1746
- shellPath: this.settingsManager.getShellPath(),
1747
- commandPrefix: this.settingsManager.getShellCommandPrefix(),
1748
- }),
1749
2066
  }, {
1750
2067
  registerProvider: (name, config) => {
1751
2068
  this._modelRuntime.registerProvider(name, config);