@selesai/code 0.13.26 → 0.13.27

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 (119) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/cli/args.js +1 -0
  3. package/dist/cli/session-picker.d.ts +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/core/agent-session-auto-handoff.test.js +2 -1
  6. package/dist/core/agent-session-length-continuation.test.js +2 -1
  7. package/dist/core/agent-session.d.ts +58 -7
  8. package/dist/core/agent-session.js +334 -167
  9. package/dist/core/bug-report-upload.d.ts +11 -0
  10. package/dist/core/bug-report-upload.js +21 -0
  11. package/dist/core/bug-report.d.ts +175 -0
  12. package/dist/core/bug-report.js +291 -0
  13. package/dist/core/cache-stats.js +12 -1
  14. package/dist/core/cache-warmer.d.ts +101 -0
  15. package/dist/core/cache-warmer.js +341 -0
  16. package/dist/core/compaction/branch-summarization.js +2 -2
  17. package/dist/core/compaction/compaction.d.ts +6 -5
  18. package/dist/core/compaction/compaction.js +49 -42
  19. package/dist/core/crash-log.d.ts +21 -0
  20. package/dist/core/crash-log.js +70 -0
  21. package/dist/core/experimental.d.ts +0 -4
  22. package/dist/core/experimental.js +0 -4
  23. package/dist/core/extensions/index.d.ts +1 -1
  24. package/dist/core/extensions/jiti-loader.d.ts +1 -0
  25. package/dist/core/extensions/jiti-loader.js +3 -0
  26. package/dist/core/extensions/jiti-static-loader.d.ts +1 -0
  27. package/dist/core/extensions/jiti-static-loader.js +3 -0
  28. package/dist/core/extensions/loader.js +39 -49
  29. package/dist/core/extensions/runner.d.ts +10 -7
  30. package/dist/core/extensions/runner.js +85 -67
  31. package/dist/core/extensions/types.d.ts +66 -52
  32. package/dist/core/extensions/virtual-modules.d.ts +2 -0
  33. package/dist/core/extensions/virtual-modules.js +39 -0
  34. package/dist/core/extensions/wrapper.js +1 -20
  35. package/dist/core/index.d.ts +2 -1
  36. package/dist/core/keybindings.d.ts +1 -1
  37. package/dist/core/keybindings.js +1 -1
  38. package/dist/core/messages.js +1 -0
  39. package/dist/core/model-config.d.ts +101 -20
  40. package/dist/core/model-config.js +25 -18
  41. package/dist/core/model-resolver.js +2 -1
  42. package/dist/core/model-runtime.js +5 -3
  43. package/dist/core/provider-composer.d.ts +7 -2
  44. package/dist/core/provider-composer.js +10 -5
  45. package/dist/core/radius.d.ts +3 -0
  46. package/dist/core/radius.js +6 -0
  47. package/dist/core/sdk.js +60 -38
  48. package/dist/core/session-export.d.ts +6 -2
  49. package/dist/core/session-export.js +14 -13
  50. package/dist/core/session-format.test.d.ts +1 -0
  51. package/dist/core/session-format.test.js +96 -0
  52. package/dist/core/session-manager.d.ts +29 -6
  53. package/dist/core/session-manager.js +159 -96
  54. package/dist/core/settings-manager.d.ts +22 -5
  55. package/dist/core/settings-manager.js +56 -23
  56. package/dist/core/slash-commands.js +1 -0
  57. package/dist/core/system-prompt.d.ts +49 -4
  58. package/dist/core/system-prompt.js +155 -102
  59. package/dist/core/system-prompt.test.js +61 -23
  60. package/dist/core/tools/bash.d.ts +2 -1
  61. package/dist/core/tools/bash.js +10 -4
  62. package/dist/core/tools/edit.js +1 -2
  63. package/dist/core/tools/read.js +1 -2
  64. package/dist/core/tools/renderers/bash.js +9 -1
  65. package/dist/core/tools/write.js +1 -2
  66. package/dist/core/usage-totals.d.ts +1 -1
  67. package/dist/core/usage-totals.js +5 -1
  68. package/dist/extensions/pi-web-agent/src/backends/factory.ts +14 -6
  69. package/dist/extensions/pi-web-agent/src/extension.ts +3 -1
  70. package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +7 -3
  71. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +3 -0
  72. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +2 -0
  73. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +4 -2
  74. package/dist/extensions/pi-web-agent/src/search/tokenin.ts +4 -1
  75. package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +2 -1
  76. package/dist/extensions/pi-web-agent/src/types.ts +4 -0
  77. package/dist/index.d.ts +3 -2
  78. package/dist/main.js +13 -9
  79. package/dist/migrations.js +2 -2
  80. package/dist/modes/interactive/bug-report.d.ts +15 -0
  81. package/dist/modes/interactive/bug-report.js +205 -0
  82. package/dist/modes/interactive/chat-viewport.js +1 -1
  83. package/dist/modes/interactive/components/branch-summary-message.js +12 -5
  84. package/dist/modes/interactive/components/compaction-summary-message.js +12 -5
  85. package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
  86. package/dist/modes/interactive/components/extension-editor.d.ts +4 -1
  87. package/dist/modes/interactive/components/extension-editor.js +7 -2
  88. package/dist/modes/interactive/components/extension-input.d.ts +2 -0
  89. package/dist/modes/interactive/components/extension-input.js +6 -0
  90. package/dist/modes/interactive/components/extension-selector.d.ts +1 -0
  91. package/dist/modes/interactive/components/extension-selector.js +4 -0
  92. package/dist/modes/interactive/components/footer.js +4 -1
  93. package/dist/modes/interactive/components/session-selector.d.ts +5 -5
  94. package/dist/modes/interactive/components/session-selector.js +74 -48
  95. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  96. package/dist/modes/interactive/components/settings-selector.js +11 -0
  97. package/dist/modes/interactive/components/skill-invocation-message.js +11 -4
  98. package/dist/modes/interactive/components/status-indicator.d.ts +2 -2
  99. package/dist/modes/interactive/components/status-indicator.js +7 -7
  100. package/dist/modes/interactive/components/tool-execution.js +18 -9
  101. package/dist/modes/interactive/components/tree-selector.js +2 -0
  102. package/dist/modes/interactive/interactive-mode.d.ts +15 -2
  103. package/dist/modes/interactive/interactive-mode.js +257 -81
  104. package/dist/modes/interactive/session-share.d.ts +2 -0
  105. package/dist/modes/interactive/session-share.js +8 -4
  106. package/dist/modes/interactive/tui-renderer.js +2 -2
  107. package/dist/modes/rpc/rpc-mode.js +2 -2
  108. package/dist/utils/clipboard-command.d.ts +6 -0
  109. package/dist/utils/clipboard-command.js +44 -0
  110. package/dist/utils/clipboard-image.js +55 -89
  111. package/dist/utils/clipboard.d.ts +1 -1
  112. package/dist/utils/clipboard.js +118 -108
  113. package/dist/utils/wsl.d.ts +2 -0
  114. package/dist/utils/wsl.js +14 -0
  115. package/dist/utils/zip.d.ts +6 -0
  116. package/dist/utils/zip.js +59 -0
  117. package/package.json +9 -9
  118. package/dist/utils/clipboard-native.d.ts +0 -10
  119. package/dist/utils/clipboard-native.js +0 -19
@@ -14,8 +14,8 @@
14
14
  */
15
15
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
16
  import { basename, dirname } from "node:path";
17
- import { contentText } from "@earendil-works/pi-ai";
18
- import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, } from "@earendil-works/pi-ai/compat";
17
+ import { contentText, getCurrentSystemMessage, retryDelayMs } from "@earendil-works/pi-ai";
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 { resolvePath } from "../utils/paths.js";
@@ -23,6 +23,7 @@ import { sleep } from "../utils/sleep.js";
23
23
  import { normalizeAssistantThinkingTags } from "../utils/thinking-tags.js";
24
24
  import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
25
25
  import { executeBashWithOperations } from "./bash-executor.js";
26
+ import { generateBugReportSummary } from "./bug-report.js";
26
27
  import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
27
28
  import { DEFAULT_THINKING_LEVEL, THINKING_LEVEL_OPTIONS } from "./defaults.js";
28
29
  import { exportSessionToHtml } from "./export-html/index.js";
@@ -34,7 +35,7 @@ import { ModelRegistry } from "./model-registry.js";
34
35
  import { expandPromptTemplate } from "./prompt-templates.js";
35
36
  import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
36
37
  import { createSyntheticSourceInfo } from "./source-info.js";
37
- import { buildSystemPrompt } from "./system-prompt.js";
38
+ import { buildSystemPrompt, buildSystemPromptSections, diffSystemPromptSections, normalizeBuildSystemPromptOptions, } from "./system-prompt.js";
38
39
  import { createLocalBashOperations } from "./tools/bash.js";
39
40
  import { createAllToolDefinitions } from "./tools/index.js";
40
41
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
@@ -99,6 +100,7 @@ export class AgentSession {
99
100
  _unsubscribeAgent;
100
101
  _eventListeners = [];
101
102
  _isAgentRunActive = false;
103
+ _agentRunAbortRequested = false;
102
104
  _idleWaitPromise;
103
105
  _resolveIdleWait;
104
106
  /** Tracks pending steering messages for UI display. Removed when delivered. */
@@ -144,15 +146,15 @@ export class AgentSession {
144
146
  _extensionErrorListener;
145
147
  _extensionErrorUnsubscriber;
146
148
  _modelRuntime;
149
+ _cacheWarmer;
147
150
  // Tool registry for extension getTools/setTools
148
151
  _toolRegistry = new Map();
149
152
  _toolDefinitions = new Map();
150
153
  _toolPromptSnippets = new Map();
151
154
  _toolPromptGuidelines = new Map();
152
- // Base system prompt (without extension appends) - used to apply fresh appends each turn
153
- _baseSystemPrompt = "";
154
155
  _baseSystemPromptOptions;
155
- _systemPromptOverride;
156
+ /** Prompt options after before_agent_start mutations for the active run. */
157
+ _runSystemPromptOptions;
156
158
  constructor(config) {
157
159
  this.agent = config.agent;
158
160
  this.sessionManager = config.sessionManager;
@@ -162,6 +164,10 @@ export class AgentSession {
162
164
  this._customTools = config.customTools ?? [];
163
165
  this._cwd = config.cwd;
164
166
  this._modelRuntime = config.modelRuntime;
167
+ this._cacheWarmer = config.cacheWarmer;
168
+ if (this._cacheWarmer) {
169
+ this._cacheWarmer.onWarmed = (entry) => this._emit({ type: "entry_appended", entry });
170
+ }
165
171
  this._extensionRunnerRef = config.extensionRunnerRef;
166
172
  this._initialActiveToolNames = config.initialActiveToolNames;
167
173
  this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
@@ -173,18 +179,21 @@ export class AgentSession {
173
179
  this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
174
180
  this._installAgentToolHooks();
175
181
  this._installAgentNextTurnRefresh();
182
+ this._installAgentForcedPromptProjection();
176
183
  this._buildRuntime({
177
184
  activeToolNames: this._initialActiveToolNames,
178
185
  includeAllExtensionTools: true,
179
186
  });
187
+ if (this._initialActiveToolNames === undefined)
188
+ this._restoreToolsFromTranscript();
180
189
  }
181
190
  get modelRuntime() {
182
191
  return this._modelRuntime;
183
192
  }
184
- async _getRequiredRequestAuth(model) {
193
+ async _getRequiredRequestAuth(model, signal) {
185
194
  let result;
186
195
  try {
187
- result = await this._modelRuntime.getAuth(model);
196
+ result = await this._modelRuntime.getAuth(model, { signal });
188
197
  }
189
198
  catch (error) {
190
199
  const cause = error instanceof Error ? error.cause : undefined;
@@ -194,7 +203,9 @@ export class AgentSession {
194
203
  throw error;
195
204
  }
196
205
  if (result && (result.auth.apiKey || result.auth.headers)) {
206
+ const requestModel = result.auth.baseUrl ? { ...model, baseUrl: result.auth.baseUrl } : model;
197
207
  return {
208
+ model: requestModel,
198
209
  apiKey: result.auth.apiKey,
199
210
  headers: withoutDeletedHeaders(result.auth.headers),
200
211
  env: result.env,
@@ -208,18 +219,26 @@ export class AgentSession {
208
219
  }
209
220
  throw new Error(formatNoApiKeyFoundMessage(model.provider));
210
221
  }
211
- async _getSummarizationRequestAuth(model) {
222
+ async _getSummarizationRequestAuth(model, signal) {
212
223
  if (this.agent.streamFunction === streamSimple) {
213
- return this._getRequiredRequestAuth(model);
224
+ return this._getRequiredRequestAuth(model, signal);
214
225
  }
215
226
  try {
216
- const result = await this._modelRuntime.getAuth(model);
217
- return result
218
- ? { apiKey: result.auth.apiKey, headers: withoutDeletedHeaders(result.auth.headers), env: result.env }
219
- : {};
227
+ const result = await this._modelRuntime.getAuth(model, { signal });
228
+ if (!result)
229
+ return { model };
230
+ const requestModel = result.auth.baseUrl ? { ...model, baseUrl: result.auth.baseUrl } : model;
231
+ return {
232
+ model: requestModel,
233
+ apiKey: result.auth.apiKey,
234
+ headers: withoutDeletedHeaders(result.auth.headers),
235
+ env: result.env,
236
+ };
220
237
  }
221
- catch {
222
- return {};
238
+ catch (error) {
239
+ if (signal?.aborted)
240
+ throw error;
241
+ return { model };
223
242
  }
224
243
  }
225
244
  /**
@@ -280,7 +299,7 @@ export class AgentSession {
280
299
  }
281
300
  async _compactBeforeNextAssistantResponse(context) {
282
301
  const model = this.model;
283
- const settings = this.settingsManager.getCompactionSettings();
302
+ const settings = this.settingsManager.getCompactionSettings(model);
284
303
  if (!model ||
285
304
  model.contextWindow <= 0 ||
286
305
  !shouldCompact(estimateContextTokens(context.messages).tokens, model.contextWindow, settings)) {
@@ -301,13 +320,25 @@ export class AgentSession {
301
320
  const context = await this._compactBeforeNextAssistantResponse(turn.context);
302
321
  const previousSnapshot = await previousPrepareNextTurnWithContext?.({ ...turn, context }, signal);
303
322
  const nextContext = previousSnapshot?.context ?? context;
323
+ const runOptions = this._runSystemPromptOptions ?? this._baseSystemPromptOptions;
324
+ const options = normalizeBuildSystemPromptOptions({
325
+ ...runOptions,
326
+ selectedTools: this.getActiveToolNames(),
327
+ toolSnippets: { ...this._baseSystemPromptOptions.toolSnippets, ...runOptions.toolSnippets },
328
+ toolGuidelines: { ...this._baseSystemPromptOptions.toolGuidelines, ...runOptions.toolGuidelines },
329
+ });
330
+ const updateMessage = this._preparePromptAndToolLoadout(options, nextContext.messages);
331
+ // Keep session.systemPrompt and ctx.getSystemPrompt() in step with what the provider sees.
332
+ this._runSystemPromptOptions = options;
304
333
  return {
305
334
  ...previousSnapshot,
306
335
  context: {
307
336
  ...nextContext,
308
- systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
309
337
  tools: this.agent.state.tools.slice(),
310
338
  },
339
+ messages: updateMessage
340
+ ? [...(previousSnapshot?.messages ?? []), updateMessage]
341
+ : previousSnapshot?.messages,
311
342
  model: this.agent.state.model,
312
343
  thinkingLevel: this.agent.state.thinkingLevel,
313
344
  };
@@ -352,6 +383,7 @@ export class AgentSession {
352
383
  resolve();
353
384
  }
354
385
  async _emitAgentSettled() {
386
+ this._cacheWarmer?.onAgentSettled();
355
387
  this._isAgentRunActive = false;
356
388
  try {
357
389
  await this._extensionRunner.emit({ type: "agent_settled" });
@@ -436,7 +468,8 @@ export class AgentSession {
436
468
  // Persist as CustomMessageEntry
437
469
  this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
438
470
  }
439
- else if (event.message.role === "user" ||
471
+ else if (event.message.role === "system" ||
472
+ event.message.role === "user" ||
440
473
  event.message.role === "assistant" ||
441
474
  event.message.role === "toolResult") {
442
475
  // Regular LLM message - persist as SessionMessageEntry
@@ -447,7 +480,9 @@ export class AgentSession {
447
480
  if (event.message.role === "assistant") {
448
481
  this._lastAssistantMessage = event.message;
449
482
  const assistantMsg = event.message;
450
- if (assistantMsg.stopReason !== "error") {
483
+ // A length stop is itself a recovery trigger, so it must not clear the
484
+ // one-shot overflow-recovery guard before the retry has run.
485
+ if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "length") {
451
486
  this._overflowRecoveryAttempted = false;
452
487
  }
453
488
  // Reset the length-continuation counter on any non-length stop so the
@@ -477,6 +512,8 @@ export class AgentSession {
477
512
  }
478
513
  };
479
514
  _willRetryAfterAgentEnd(event) {
515
+ if (this._agentRunAbortRequested)
516
+ return false;
480
517
  const settings = this.settingsManager.getRetrySettings();
481
518
  if (!settings.enabled || this._retryAttempt >= settings.maxRetries) {
482
519
  return false;
@@ -665,6 +702,10 @@ export class AgentSession {
665
702
  this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
666
703
  this._disconnectFromAgent();
667
704
  this._eventListeners = [];
705
+ if (this._cacheWarmer) {
706
+ this._cacheWarmer.onWarmed = undefined;
707
+ this._cacheWarmer.cancel();
708
+ }
668
709
  cleanupSessionResources(this.sessionId);
669
710
  }
670
711
  // =========================================================================
@@ -674,6 +715,15 @@ export class AgentSession {
674
715
  get state() {
675
716
  return this.agent.state;
676
717
  }
718
+ /** Current cache-warming state and the policy inputs that produced it. */
719
+ get cacheWarmingStatus() {
720
+ return this._cacheWarmer?.status;
721
+ }
722
+ /** Persist the cache-warming mode and immediately reconcile active warming. */
723
+ setCacheWarmingMode(mode) {
724
+ this.settingsManager.setCacheWarmingMode(mode);
725
+ this._cacheWarmer?.onModeChanged();
726
+ }
677
727
  /** Current model (may be undefined if not yet selected) */
678
728
  get model() {
679
729
  return this.agent.state.model;
@@ -690,9 +740,9 @@ export class AgentSession {
690
740
  get isIdle() {
691
741
  return !this._isAgentRunActive && !this.isCompacting;
692
742
  }
693
- /** Current effective system prompt (includes any per-turn extension modifications) */
743
+ /** Current effective system prompt, including changes not yet sent to the model. */
694
744
  get systemPrompt() {
695
- return this.agent.state.systemPrompt;
745
+ return buildSystemPrompt(this._runSystemPromptOptions ?? this._baseSystemPromptOptions);
696
746
  }
697
747
  /** Current retry attempt (0 if not retrying) */
698
748
  get retryAttempt() {
@@ -738,9 +788,7 @@ export class AgentSession {
738
788
  }
739
789
  }
740
790
  this.agent.state.tools = tools;
741
- // Rebuild base system prompt with new tool set
742
- this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
743
- this.agent.state.systemPrompt = this._systemPromptOverride ?? this._baseSystemPrompt;
791
+ this._rebuildSystemPrompt(validToolNames);
744
792
  }
745
793
  /** Whether compaction or branch summarization is currently running */
746
794
  get isCompacting() {
@@ -809,16 +857,10 @@ export class AgentSession {
809
857
  _rebuildSystemPrompt(toolNames) {
810
858
  const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
811
859
  const toolSnippets = {};
812
- const promptGuidelines = [];
813
- for (const name of validToolNames) {
860
+ for (const name of this._toolRegistry.keys()) {
814
861
  const snippet = this._toolPromptSnippets.get(name);
815
- if (snippet) {
862
+ if (snippet)
816
863
  toolSnippets[name] = snippet;
817
- }
818
- const toolGuidelines = this._toolPromptGuidelines.get(name);
819
- if (toolGuidelines) {
820
- promptGuidelines.push(...toolGuidelines);
821
- }
822
864
  }
823
865
  const loaderSystemPrompt = this._resourceLoader.getSystemPrompt();
824
866
  const loaderAppendSystemPrompt = this._resourceLoader.getAppendSystemPrompt();
@@ -826,7 +868,7 @@ export class AgentSession {
826
868
  const loadedSkills = this._resourceLoader.getPromptSkills();
827
869
  const loadedAgents = this._resourceLoader.getAgents().agents;
828
870
  const loadedContextFiles = this._resourceLoader.getAgentsFiles().agentsFiles;
829
- this._baseSystemPromptOptions = {
871
+ this._baseSystemPromptOptions = normalizeBuildSystemPromptOptions({
830
872
  cwd: this._cwd,
831
873
  skills: loadedSkills,
832
874
  agents: loadedAgents,
@@ -835,23 +877,87 @@ export class AgentSession {
835
877
  appendSystemPrompt,
836
878
  selectedTools: validToolNames,
837
879
  toolSnippets,
838
- promptGuidelines,
880
+ toolGuidelines: Object.fromEntries(this._toolPromptGuidelines),
881
+ });
882
+ }
883
+ /**
884
+ * Apply a prompt and tool loadout for the next request. Sets the executable tools and
885
+ * returns a system message patching the prompt sections the model currently has (replayed
886
+ * from `messages`), or undefined when the prompt is unchanged. Tool changes are declared by
887
+ * the agent loop before the request.
888
+ *
889
+ * A forced prompt does not affect the transcript: the structured sections are still diffed
890
+ * and persisted, and the forced text is projected onto the request by
891
+ * {@link _installAgentForcedPromptProjection}.
892
+ */
893
+ _preparePromptAndToolLoadout(options, messages = this.agent.state.messages) {
894
+ options.selectedTools = [...new Set(options.selectedTools)].filter((name) => this._toolRegistry.has(name));
895
+ this.agent.state.tools = options.selectedTools.flatMap((name) => {
896
+ const tool = this._toolRegistry.get(name);
897
+ return tool ? [tool] : [];
898
+ });
899
+ const sections = diffSystemPromptSections(getCurrentSystemMessage(messages)?.sections ?? {}, buildSystemPromptSections(options));
900
+ return sections ? { role: "system", content: "", sections, timestamp: Date.now() } : undefined;
901
+ }
902
+ /**
903
+ * Send a forced prompt as the provider's leading system prompt without recording it.
904
+ *
905
+ * A `before_agent_start` handler that returns `systemPrompt` needs that exact text at the
906
+ * head of the request; a mid-conversation system message would leave the original prompt
907
+ * in place. The forced text is a rendering of the current prompt, so the transcript keeps
908
+ * its structured sections and the request is projected instead: the system messages
909
+ * collapse into one head holding the forced text and the current tools. Runs after the
910
+ * `context` extension handlers.
911
+ */
912
+ _installAgentForcedPromptProjection() {
913
+ const previousTransformContext = this.agent.transformContext;
914
+ this.agent.transformContext = async (messages, signal) => {
915
+ const transformed = previousTransformContext ? await previousTransformContext(messages, signal) : messages;
916
+ const forced = this._runSystemPromptOptions?.forceSystemPrompt;
917
+ if (forced === undefined)
918
+ return transformed;
919
+ const current = getCurrentSystemMessage(transformed);
920
+ const head = {
921
+ role: "system",
922
+ content: forced,
923
+ ...(current?.toolsAdded ? { toolsAdded: current.toolsAdded } : {}),
924
+ timestamp: current?.timestamp ?? Date.now(),
925
+ };
926
+ return [head, ...transformed.filter((message) => message.role !== "system")];
839
927
  };
840
- return buildSystemPrompt(this._baseSystemPromptOptions);
928
+ }
929
+ /** Restore the active tool loadout declared by the session transcript, if it declares one. */
930
+ _restoreToolsFromTranscript() {
931
+ const current = getCurrentSystemMessage(this.sessionManager.buildSessionContext().messages);
932
+ if (!current)
933
+ return;
934
+ const toolNames = (current.toolsAdded ?? [])
935
+ .map((tool) => tool.name)
936
+ .filter((name) => this._toolRegistry.has(name));
937
+ this.agent.state.tools = toolNames.flatMap((name) => {
938
+ const registered = this._toolRegistry.get(name);
939
+ return registered ? [registered] : [];
940
+ });
941
+ this._rebuildSystemPrompt(toolNames);
841
942
  }
842
943
  // =========================================================================
843
944
  // Prompting
844
945
  // =========================================================================
845
946
  async _runAgentPrompt(messages) {
947
+ this._agentRunAbortRequested = false;
846
948
  this._isAgentRunActive = true;
847
949
  try {
848
950
  await this.agent.prompt(messages);
849
951
  while (await this._handlePostAgentRun()) {
952
+ if (this._agentRunAbortRequested)
953
+ break;
850
954
  await this.agent.continue();
851
955
  }
852
956
  }
853
957
  finally {
854
- this._systemPromptOverride = undefined;
958
+ if (this._agentRunAbortRequested)
959
+ this._finishCancelledRetry();
960
+ this._runSystemPromptOptions = undefined;
855
961
  this._flushPendingBashMessages();
856
962
  this._flushPendingCustomMessages();
857
963
  await this._emitAgentSettled();
@@ -860,11 +966,21 @@ export class AgentSession {
860
966
  async _handlePostAgentRun() {
861
967
  const msg = this._lastAssistantMessage;
862
968
  this._lastAssistantMessage = undefined;
969
+ if (this._agentRunAbortRequested) {
970
+ this._finishCancelledRetry();
971
+ return false;
972
+ }
863
973
  if (!msg) {
864
974
  return false;
865
975
  }
866
976
  if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
867
- return true;
977
+ if (this._agentRunAbortRequested)
978
+ this._finishCancelledRetry();
979
+ return !this._agentRunAbortRequested;
980
+ }
981
+ if (this._agentRunAbortRequested) {
982
+ this._finishCancelledRetry();
983
+ return false;
868
984
  }
869
985
  if (msg.stopReason === "error" && this._retryAttempt > 0) {
870
986
  this._emit({
@@ -876,7 +992,7 @@ export class AgentSession {
876
992
  this._retryAttempt = 0;
877
993
  }
878
994
  if (await this._checkCompaction(msg)) {
879
- return true;
995
+ return !this._agentRunAbortRequested;
880
996
  }
881
997
  // A length stop means the model hit the output token limit mid-response.
882
998
  // The agent loop already re-prompts when truncated tool calls are present;
@@ -892,7 +1008,20 @@ export class AgentSession {
892
1008
  }
893
1009
  // The agent loop drains both queues before emitting agent_end. Any messages
894
1010
  // here were queued by agent_end extension handlers and need a continuation.
895
- return this.agent.hasQueuedMessages();
1011
+ return !this._agentRunAbortRequested && this.agent.hasQueuedMessages();
1012
+ }
1013
+ async _runInputHandlers(text, images, source, streamingBehavior) {
1014
+ if (!this._extensionRunner.hasHandlers("input")) {
1015
+ return { text, images };
1016
+ }
1017
+ const inputResult = await this._extensionRunner.emitInput(text, images, source, streamingBehavior);
1018
+ if (inputResult.action === "handled") {
1019
+ return undefined;
1020
+ }
1021
+ if (inputResult.action === "transform") {
1022
+ return { text: inputResult.text, images: inputResult.images ?? images };
1023
+ }
1024
+ return { text, images };
896
1025
  }
897
1026
  /**
898
1027
  * Send a prompt to the agent.
@@ -918,20 +1047,16 @@ export class AgentSession {
918
1047
  return;
919
1048
  }
920
1049
  }
1050
+ if (this._compactionAbortController !== undefined) {
1051
+ throw new Error("Cannot submit a prompt while compaction is in progress. Wait for compaction to finish and retry.");
1052
+ }
921
1053
  // Emit input event for extension interception (before skill/template expansion)
922
- let currentText = text;
923
- let currentImages = options?.images;
924
- if (this._extensionRunner.hasHandlers("input")) {
925
- const inputResult = await this._extensionRunner.emitInput(currentText, currentImages, options?.source ?? "interactive", this.isStreaming ? options?.streamingBehavior : undefined);
926
- if (inputResult.action === "handled") {
927
- preflightResult?.(true);
928
- return;
929
- }
930
- if (inputResult.action === "transform") {
931
- currentText = inputResult.text;
932
- currentImages = inputResult.images ?? currentImages;
933
- }
1054
+ const processedInput = await this._runInputHandlers(text, options?.images, options?.source ?? "interactive", this.isStreaming ? options?.streamingBehavior : undefined);
1055
+ if (!processedInput) {
1056
+ preflightResult?.(true);
1057
+ return;
934
1058
  }
1059
+ const { text: currentText, images: currentImages } = processedInput;
935
1060
  // Expand skill commands (/skill:name args) and prompt templates (/template args)
936
1061
  let expandedText = currentText;
937
1062
  if (expandPromptTemplates) {
@@ -1003,30 +1128,25 @@ export class AgentSession {
1003
1128
  }
1004
1129
  this._pendingNextTurnMessages = [];
1005
1130
  // Emit before_agent_start extension event
1006
- const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPrompt, this._baseSystemPromptOptions);
1007
- // Add all custom messages from extensions
1008
- if (result?.messages) {
1009
- for (const msg of result.messages) {
1010
- messages.push({
1011
- role: "custom",
1012
- customType: msg.customType,
1013
- // Untyped extensions can pass null/missing content; normalize at ingestion.
1014
- content: msg.content ?? [],
1015
- display: msg.display,
1016
- details: msg.details,
1017
- timestamp: Date.now(),
1018
- });
1019
- }
1020
- }
1021
- // Apply extension-modified system prompt, or reset to base
1022
- if (result?.systemPrompt !== undefined) {
1023
- this._systemPromptOverride = result.systemPrompt;
1024
- this.agent.state.systemPrompt = result.systemPrompt;
1025
- }
1026
- else {
1027
- // Ensure we're using the base prompt (in case previous turn had modifications)
1028
- this._systemPromptOverride = undefined;
1029
- this.agent.state.systemPrompt = this._baseSystemPrompt;
1131
+ const selectedToolsBefore = this._baseSystemPromptOptions.selectedTools;
1132
+ const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPromptOptions);
1133
+ // Handlers may edit event.systemPromptOptions.selectedTools or call setActiveTools(),
1134
+ // which updates the live loadout instead. An explicit edit wins; otherwise the live
1135
+ // loadout is authoritative, so a setActiveTools() call is not undone here.
1136
+ const handlerEditedTools = result.systemPromptOptions.selectedTools.length !== selectedToolsBefore.length ||
1137
+ result.systemPromptOptions.selectedTools.some((name, index) => name !== selectedToolsBefore[index]);
1138
+ if (!handlerEditedTools)
1139
+ result.systemPromptOptions.selectedTools = this.getActiveToolNames();
1140
+ for (const msg of result.messages) {
1141
+ messages.push({
1142
+ role: "custom",
1143
+ customType: msg.customType,
1144
+ // Untyped extensions can pass null/missing content; normalize at ingestion.
1145
+ content: msg.content ?? [],
1146
+ display: msg.display,
1147
+ details: msg.details,
1148
+ timestamp: Date.now(),
1149
+ });
1030
1150
  }
1031
1151
  // Another prompt may have started while this one was preflighting (auth
1032
1152
  // checks, compaction, and extension events can take seconds). The agent is
@@ -1043,6 +1163,10 @@ export class AgentSession {
1043
1163
  preflightResult?.(true);
1044
1164
  return;
1045
1165
  }
1166
+ const updateMessage = this._preparePromptAndToolLoadout(result.systemPromptOptions);
1167
+ this._runSystemPromptOptions = result.systemPromptOptions;
1168
+ if (updateMessage)
1169
+ messages.unshift(updateMessage);
1046
1170
  }
1047
1171
  catch (error) {
1048
1172
  preflightResult?.(false);
@@ -1111,40 +1235,44 @@ export class AgentSession {
1111
1235
  return text; // Return original on error
1112
1236
  }
1113
1237
  }
1238
+ async _queueUserInput(text, images, behavior, source) {
1239
+ if (text.startsWith("/")) {
1240
+ this._throwIfExtensionCommand(text);
1241
+ }
1242
+ const processedInput = await this._runInputHandlers(text, images, source, this.isStreaming ? behavior : undefined);
1243
+ if (!processedInput)
1244
+ return;
1245
+ let expandedText = this._expandSkillCommand(processedInput.text);
1246
+ expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1247
+ if (behavior === "steer") {
1248
+ await this._queueSteer(expandedText, processedInput.images);
1249
+ }
1250
+ else {
1251
+ await this._queueFollowUp(expandedText, processedInput.images);
1252
+ }
1253
+ }
1114
1254
  /**
1115
1255
  * Queue a steering message while the agent is running.
1116
1256
  * Delivered after the current assistant turn finishes executing its tool calls,
1117
1257
  * before the next LLM call.
1118
1258
  * Expands skill commands and prompt templates. Errors on extension commands.
1119
1259
  * @param images Optional image attachments to include with the message
1260
+ * @param options Input source; defaults to interactive
1120
1261
  * @throws Error if text is an extension command
1121
1262
  */
1122
- async steer(text, images) {
1123
- // Check for extension commands (cannot be queued)
1124
- if (text.startsWith("/")) {
1125
- this._throwIfExtensionCommand(text);
1126
- }
1127
- // Expand skill commands and prompt templates
1128
- let expandedText = this._expandSkillCommand(text);
1129
- expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1130
- await this._queueSteer(expandedText, images);
1263
+ async steer(text, images, options) {
1264
+ await this._queueUserInput(text, images, "steer", options?.source ?? "interactive");
1131
1265
  }
1132
1266
  /**
1133
1267
  * Queue a follow-up message to be processed after the agent finishes.
1134
1268
  * Delivered only when agent has no more tool calls or steering messages.
1135
1269
  * Expands skill commands and prompt templates. Errors on extension commands.
1136
1270
  * @param images Optional image attachments to include with the message
1271
+ * @param options Input source; defaults to interactive
1137
1272
  * @throws Error if text is an extension command
1138
1273
  */
1139
- async followUp(text, images) {
1140
- // Check for extension commands (cannot be queued)
1141
- if (text.startsWith("/")) {
1142
- this._throwIfExtensionCommand(text);
1143
- }
1144
- // Expand skill commands and prompt templates
1145
- let expandedText = this._expandSkillCommand(text);
1146
- expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1147
- await this._queueFollowUp(expandedText, images);
1274
+ async followUp(text, images, options) {
1275
+ await this._queueUserInput(text, images, "followUp", options?.source ?? "interactive");
1148
1276
  }
1149
1277
  /**
1150
1278
  * Internal: Queue a steering message (already expanded, no extension command check).
@@ -1488,6 +1616,9 @@ export class AgentSession {
1488
1616
  * Abort current operation and wait for agent to become idle.
1489
1617
  */
1490
1618
  async abort() {
1619
+ if (this._isAgentRunActive) {
1620
+ this._agentRunAbortRequested = true;
1621
+ }
1491
1622
  this.abortRetry();
1492
1623
  this.abortCompaction();
1493
1624
  this.abortBranchSummary();
@@ -1710,6 +1841,10 @@ export class AgentSession {
1710
1841
  // =========================================================================
1711
1842
  // Compaction
1712
1843
  // =========================================================================
1844
+ /** Generate the built-in compaction summary for manual and automatic compaction. */
1845
+ async _runDefaultCompaction(preparation, requestModel, apiKey, headers, customInstructions, signal, env, reason) {
1846
+ return compact(preparation, requestModel, apiKey, headers, customInstructions, signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }), undefined);
1847
+ }
1713
1848
  _clearManualCompactionState() {
1714
1849
  this._compactionAbortController = undefined;
1715
1850
  this._resolveIdleWaitIfIdle();
@@ -1724,13 +1859,16 @@ export class AgentSession {
1724
1859
  await this.abort();
1725
1860
  this._compactionAbortController = new AbortController();
1726
1861
  this._emit({ type: "compaction_start", reason: "manual" });
1862
+ let fromExtension = false;
1863
+ let cancelledByExtension = false;
1727
1864
  try {
1728
- if (!this.model) {
1865
+ const model = this.model;
1866
+ if (!model) {
1729
1867
  throw new Error(formatNoModelSelectedMessage());
1730
1868
  }
1731
- const { apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model);
1869
+ const settings = this.settingsManager.getCompactionSettings(model);
1870
+ const { model: requestModel, apiKey, headers, env, } = await this._getSummarizationRequestAuth(model, this._compactionAbortController.signal);
1732
1871
  const pathEntries = this.sessionManager.getBranch();
1733
- const settings = this.settingsManager.getCompactionSettings();
1734
1872
  const preparation = prepareCompaction(pathEntries, settings);
1735
1873
  if (!preparation) {
1736
1874
  // Check why we can't compact
@@ -1753,6 +1891,7 @@ export class AgentSession {
1753
1891
  signal: this._compactionAbortController.signal,
1754
1892
  }));
1755
1893
  if (result?.cancel) {
1894
+ cancelledByExtension = true;
1756
1895
  throw new Error("Compaction cancelled");
1757
1896
  }
1758
1897
  if (result?.compaction) {
@@ -1774,8 +1913,8 @@ export class AgentSession {
1774
1913
  details = extensionCompaction.details;
1775
1914
  }
1776
1915
  else {
1777
- // Generate compaction result
1778
- const result = await compact(preparation, this.model, apiKey, headers, customInstructions, this._compactionAbortController.signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason: "manual" }));
1916
+ // Shared default summary generator, also used by automatic compaction.
1917
+ const result = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, customInstructions, this._compactionAbortController.signal, env, "manual");
1779
1918
  summary = result.summary;
1780
1919
  firstKeptEntryId = result.firstKeptEntryId;
1781
1920
  tokensBefore = result.tokensBefore;
@@ -1822,7 +1961,7 @@ export class AgentSession {
1822
1961
  }
1823
1962
  catch (error) {
1824
1963
  const message = error instanceof Error ? error.message : String(error);
1825
- const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
1964
+ const aborted = this._compactionAbortController.signal.aborted || cancelledByExtension;
1826
1965
  const errorMessage = aborted ? undefined : `Compaction failed: ${message}`;
1827
1966
  this._clearManualCompactionState();
1828
1967
  this._emit({
@@ -1872,7 +2011,7 @@ export class AgentSession {
1872
2011
  * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
1873
2012
  */
1874
2013
  async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
1875
- const settings = this.settingsManager.getCompactionSettings();
2014
+ const settings = this.settingsManager.getCompactionSettings(this.model);
1876
2015
  if (!settings.enabled)
1877
2016
  return false;
1878
2017
  // Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
@@ -1892,11 +2031,12 @@ export class AgentSession {
1892
2031
  if (assistantIsFromBeforeCompaction) {
1893
2032
  return false;
1894
2033
  }
1895
- // Case 1: Overflow - LLM returned context overflow error, or reported usage exceeded
1896
- // the configured window. A successful response over the configured window should compact
1897
- // but must not retry: the assistant answer already completed and agent.continue() cannot
1898
- // continue from an assistant message.
1899
- if (sameModel && isContextOverflow(assistantMessage, contextWindow)) {
2034
+ // Automatic cases 1 and 2: context overflow.
2035
+ // A length stop is recoverable when output ended below the model's original desired limit,
2036
+ // independent of the configured context size or any context-clamped provider request limit.
2037
+ const contextOverflow = sameModel && isContextOverflow(assistantMessage, contextWindow);
2038
+ const recoverableLength = sameModel && isRecoverableLength(assistantMessage, this.model?.maxTokens ?? 0);
2039
+ if (contextOverflow || recoverableLength) {
1900
2040
  const willRetry = assistantMessage.stopReason !== "stop";
1901
2041
  if (!willRetry) {
1902
2042
  return await this._runAutoCompaction("overflow", false);
@@ -1908,7 +2048,9 @@ export class AgentSession {
1908
2048
  result: undefined,
1909
2049
  aborted: false,
1910
2050
  willRetry: false,
1911
- errorMessage: "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
2051
+ errorMessage: contextOverflow
2052
+ ? "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model."
2053
+ : "Truncated response recovery failed after one compact-and-retry attempt.",
1912
2054
  });
1913
2055
  return false;
1914
2056
  }
@@ -1955,30 +2097,28 @@ export class AgentSession {
1955
2097
  * Internal: Run auto-compaction with events.
1956
2098
  */
1957
2099
  async _runAutoCompaction(reason, willRetry) {
1958
- const settings = this.settingsManager.getCompactionSettings();
2100
+ const model = this.model;
2101
+ const settings = this.settingsManager.getCompactionSettings(model);
2102
+ let abortController;
1959
2103
  let started = false;
1960
2104
  let fromExtension = false;
2105
+ let cancelledByExtension = false;
1961
2106
  try {
1962
- if (!this.model) {
2107
+ if (!model) {
1963
2108
  return false;
1964
2109
  }
1965
- let apiKey;
1966
- let headers;
1967
- let env;
1968
- if (this.agent.streamFunction === streamSimple) {
1969
- ({ apiKey, headers, env } = await this._getRequiredRequestAuth(this.model));
1970
- }
1971
- else {
1972
- ({ apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model));
1973
- }
1974
2110
  const pathEntries = this.sessionManager.getBranch();
1975
2111
  const preparation = prepareCompaction(pathEntries, settings);
1976
2112
  if (!preparation) {
1977
2113
  return false;
1978
2114
  }
1979
- this._emit({ type: "compaction_start", reason });
1980
- this._autoCompactionAbortController = new AbortController();
2115
+ abortController = new AbortController();
2116
+ this._autoCompactionAbortController = abortController;
1981
2117
  started = true;
2118
+ this._emit({ type: "compaction_start", reason });
2119
+ abortController.signal.throwIfAborted();
2120
+ const { model: requestModel, apiKey, headers, env, } = await this._getSummarizationRequestAuth(model, abortController.signal);
2121
+ abortController.signal.throwIfAborted();
1982
2122
  let extensionCompaction;
1983
2123
  if (this._extensionRunner.hasHandlers("session_before_compact")) {
1984
2124
  const extensionResult = (await this._extensionRunner.emit({
@@ -1988,24 +2128,18 @@ export class AgentSession {
1988
2128
  customInstructions: undefined,
1989
2129
  reason,
1990
2130
  willRetry,
1991
- signal: this._autoCompactionAbortController.signal,
2131
+ signal: abortController.signal,
1992
2132
  }));
1993
2133
  if (extensionResult?.cancel) {
1994
- this._emit({
1995
- type: "compaction_end",
1996
- reason,
1997
- result: undefined,
1998
- aborted: true,
1999
- willRetry: false,
2000
- });
2001
- await this._emitSessionCompactFailed({ reason, aborted: true, willRetry: false, fromExtension: false });
2002
- return false;
2134
+ cancelledByExtension = true;
2135
+ throw new Error("Compaction cancelled");
2003
2136
  }
2004
2137
  if (extensionResult?.compaction) {
2005
2138
  extensionCompaction = extensionResult.compaction;
2006
2139
  fromExtension = true;
2007
2140
  }
2008
2141
  }
2142
+ abortController.signal.throwIfAborted();
2009
2143
  let summary;
2010
2144
  let firstKeptEntryId;
2011
2145
  let tokensBefore;
@@ -2020,25 +2154,15 @@ export class AgentSession {
2020
2154
  details = extensionCompaction.details;
2021
2155
  }
2022
2156
  else {
2023
- // Generate compaction result
2024
- const compactResult = await compact(preparation, this.model, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }));
2157
+ // Shared default summary generator, also used by manual compaction.
2158
+ const compactResult = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, undefined, abortController.signal, env, reason);
2025
2159
  summary = compactResult.summary;
2026
2160
  firstKeptEntryId = compactResult.firstKeptEntryId;
2027
2161
  tokensBefore = compactResult.tokensBefore;
2028
2162
  usage = compactResult.usage;
2029
2163
  details = compactResult.details;
2030
2164
  }
2031
- if (this._autoCompactionAbortController.signal.aborted) {
2032
- this._emit({
2033
- type: "compaction_end",
2034
- reason,
2035
- result: undefined,
2036
- aborted: true,
2037
- willRetry: false,
2038
- });
2039
- await this._emitSessionCompactFailed({ reason, aborted: true, willRetry: false, fromExtension });
2040
- return false;
2041
- }
2165
+ abortController.signal.throwIfAborted();
2042
2166
  this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
2043
2167
  const newEntries = this.sessionManager.getEntries();
2044
2168
  const sessionContext = this.sessionManager.buildSessionContext();
@@ -2067,7 +2191,11 @@ export class AgentSession {
2067
2191
  if (willRetry) {
2068
2192
  const messages = this.agent.state.messages;
2069
2193
  const lastMsg = messages[messages.length - 1];
2070
- if (lastMsg?.role === "assistant" && lastMsg.stopReason === "error") {
2194
+ // The overflow response was persisted on message_end before _checkCompaction() removed it
2195
+ // from agent state. Rebuilding state from the new compaction can restore that kept entry,
2196
+ // leaving an assistant as the final message. agent.continue() rejects that state, so remove
2197
+ // the retriable error or truncated-length response again before continuing the interrupted turn.
2198
+ if (lastMsg?.role === "assistant" && (lastMsg.stopReason === "error" || lastMsg.stopReason === "length")) {
2071
2199
  this.agent.state.messages = messages.slice(0, -1);
2072
2200
  }
2073
2201
  return true;
@@ -2077,23 +2205,26 @@ export class AgentSession {
2077
2205
  return this.agent.hasQueuedMessages();
2078
2206
  }
2079
2207
  catch (error) {
2080
- const errorMessage = error instanceof Error ? error.message : "compaction failed";
2208
+ const message = error instanceof Error ? error.message : "compaction failed";
2209
+ const aborted = abortController?.signal.aborted === true || cancelledByExtension;
2081
2210
  if (started) {
2082
- const formattedErrorMessage = reason === "overflow"
2083
- ? `Context overflow recovery failed: ${errorMessage}`
2084
- : `Auto-compaction failed: ${errorMessage}`;
2211
+ const errorMessage = aborted
2212
+ ? undefined
2213
+ : reason === "overflow"
2214
+ ? `Context overflow recovery failed: ${message}`
2215
+ : `Auto-compaction failed: ${message}`;
2085
2216
  this._emit({
2086
2217
  type: "compaction_end",
2087
2218
  reason,
2088
2219
  result: undefined,
2089
- aborted: false,
2220
+ aborted,
2090
2221
  willRetry: false,
2091
- errorMessage: formattedErrorMessage,
2222
+ errorMessage,
2092
2223
  });
2093
2224
  await this._emitSessionCompactFailed({
2094
2225
  reason,
2095
- errorMessage: formattedErrorMessage,
2096
- aborted: false,
2226
+ errorMessage,
2227
+ aborted,
2097
2228
  willRetry: false,
2098
2229
  fromExtension,
2099
2230
  });
@@ -2101,7 +2232,9 @@ export class AgentSession {
2101
2232
  return false;
2102
2233
  }
2103
2234
  finally {
2104
- this._autoCompactionAbortController = undefined;
2235
+ if (this._autoCompactionAbortController === abortController) {
2236
+ this._autoCompactionAbortController = undefined;
2237
+ }
2105
2238
  this._resolveIdleWaitIfIdle();
2106
2239
  }
2107
2240
  }
@@ -2158,8 +2291,7 @@ export class AgentSession {
2158
2291
  themePaths: this.buildExtensionResourcePaths(themePaths),
2159
2292
  };
2160
2293
  this._resourceLoader.extendResources(extensionPaths);
2161
- this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());
2162
- this.agent.state.systemPrompt = this._baseSystemPrompt;
2294
+ this._rebuildSystemPrompt(this.getActiveToolNames());
2163
2295
  }
2164
2296
  buildExtensionResourcePaths(entries) {
2165
2297
  return entries.map((entry) => {
@@ -2276,8 +2408,7 @@ export class AgentSession {
2276
2408
  }),
2277
2409
  setSkillsIndexFilter: (filter) => {
2278
2410
  this._resourceLoader.setSkillsIndexFilter(filter);
2279
- this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());
2280
- this.agent.state.systemPrompt = this._systemPromptOverride ?? this._baseSystemPrompt;
2411
+ this._rebuildSystemPrompt(this.getActiveToolNames());
2281
2412
  },
2282
2413
  refreshTools: () => this._refreshToolRegistry(),
2283
2414
  getCommands,
@@ -2511,6 +2642,18 @@ export class AgentSession {
2511
2642
  },
2512
2643
  };
2513
2644
  }
2645
+ _finishCancelledRetry() {
2646
+ if (this._retryAttempt === 0)
2647
+ return;
2648
+ const attempt = this._retryAttempt;
2649
+ this._retryAttempt = 0;
2650
+ this._emit({
2651
+ type: "auto_retry_end",
2652
+ success: false,
2653
+ attempt,
2654
+ finalError: "Retry cancelled",
2655
+ });
2656
+ }
2514
2657
  /**
2515
2658
  * Prepare a retryable error for continuation with exponential backoff.
2516
2659
  * @returns true if the caller should continue the agent, false otherwise
@@ -2526,7 +2669,7 @@ export class AgentSession {
2526
2669
  this._retryAttempt--;
2527
2670
  return false;
2528
2671
  }
2529
- const delayMs = settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
2672
+ const delayMs = retryDelayMs(settings, this._retryAttempt);
2530
2673
  this._emit({
2531
2674
  type: "auto_retry_start",
2532
2675
  attempt: this._retryAttempt,
@@ -2546,14 +2689,7 @@ export class AgentSession {
2546
2689
  }
2547
2690
  catch {
2548
2691
  // Aborted during sleep - emit end event so UI can clean up
2549
- const attempt = this._retryAttempt;
2550
- this._retryAttempt = 0;
2551
- this._emit({
2552
- type: "auto_retry_end",
2553
- success: false,
2554
- attempt,
2555
- finalError: "Retry cancelled",
2556
- });
2692
+ this._finishCancelledRetry();
2557
2693
  return false;
2558
2694
  }
2559
2695
  finally {
@@ -2704,6 +2840,9 @@ export class AgentSession {
2704
2840
  if (this.isStreaming) {
2705
2841
  throw new Error("Wait for the current response to finish before navigating the session tree.");
2706
2842
  }
2843
+ if (this.isCompacting) {
2844
+ throw new Error("Wait for the current compaction or tree navigation to finish before navigating the session tree.");
2845
+ }
2707
2846
  const oldLeafId = this.sessionManager.getLeafId();
2708
2847
  // No-op if already at target
2709
2848
  if (targetId === oldLeafId) {
@@ -2846,6 +2985,7 @@ export class AgentSession {
2846
2985
  // Update agent state
2847
2986
  const sessionContext = this.sessionManager.buildSessionContext();
2848
2987
  this.agent.state.messages = sessionContext.messages;
2988
+ this._restoreToolsFromTranscript();
2849
2989
  // Emit session_tree event (extension runners + RPC stdout subscribers)
2850
2990
  const treeEvent = {
2851
2991
  type: "session_tree",
@@ -2895,7 +3035,10 @@ export class AgentSession {
2895
3035
  let toolCalls = 0;
2896
3036
  const usageTotals = createUsageTotals();
2897
3037
  for (const entry of this.sessionManager.getEntries()) {
2898
- if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
3038
+ if (entry.type === "usage") {
3039
+ addUsageToTotals(usageTotals, entry.usage);
3040
+ }
3041
+ else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
2899
3042
  addUsageToTotals(usageTotals, entry.usage);
2900
3043
  }
2901
3044
  if (entry.type !== "message")
@@ -3030,6 +3173,30 @@ export class AgentSession {
3030
3173
  writeFileSync(filePath, `${lines.join("\n")}\n`);
3031
3174
  return filePath;
3032
3175
  }
3176
+ /**
3177
+ * Ask the current model to describe what went wrong in this session for a bug report.
3178
+ * Used when the user declines to share the transcript itself.
3179
+ */
3180
+ async summarizeForBugReport(options) {
3181
+ const model = this.model;
3182
+ if (!model) {
3183
+ throw new Error("No model selected");
3184
+ }
3185
+ const { model: requestModel, apiKey, headers, env } = await this._getSummarizationRequestAuth(model);
3186
+ return generateBugReportSummary({
3187
+ messages: this.messages,
3188
+ hint: options.hint,
3189
+ model: requestModel,
3190
+ apiKey,
3191
+ headers,
3192
+ env,
3193
+ signal: options.signal,
3194
+ thinkingLevel: this.thinkingLevel,
3195
+ streamFn: this.agent.streamFunction,
3196
+ retry: this.settingsManager.getRetrySettings(),
3197
+ sessionId: this.sessionId,
3198
+ });
3199
+ }
3033
3200
  // =========================================================================
3034
3201
  // Utilities
3035
3202
  // =========================================================================