@astralyn/pi 0.87.0 → 0.87.2

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 (301) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/bundle/chunks/{anthropic-messages-R2LYJUDD.js → anthropic-messages-XWFTZP2N.js} +1 -1
  3. package/dist/bundle/chunks/{chunk-INDBRWNK.js → chunk-NBIUJDV6.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-RKLIHRPH.js → chunk-RFLBIA5Z.js} +82 -82
  5. package/dist/bundle/chunks/github-copilot.js +1 -1
  6. package/dist/bundle/chunks/{openai-completions-JZCEEJ43.js → openai-completions-SHS3CAEL.js} +1 -1
  7. package/dist/bundle/chunks/{virtual-modules-CG7NJ5WO.js → virtual-modules-GOHLE5JL.js} +1 -1
  8. package/dist/bundle/cli.js +1 -1
  9. package/dist/bundle/index.js +1 -1
  10. package/dist/bundle/rpc-entry.js +1 -1
  11. package/dist/cli/args.d.ts.map +1 -1
  12. package/dist/cli/args.js +14 -4
  13. package/dist/cli/args.js.map +1 -1
  14. package/dist/config.d.ts +1 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js.map +1 -1
  17. package/dist/core/agent-session-runtime.d.ts +9 -0
  18. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  19. package/dist/core/agent-session-runtime.js +158 -157
  20. package/dist/core/agent-session-runtime.js.map +1 -1
  21. package/dist/core/agent-session.d.ts +30 -0
  22. package/dist/core/agent-session.d.ts.map +1 -1
  23. package/dist/core/agent-session.js +514 -443
  24. package/dist/core/agent-session.js.map +1 -1
  25. package/dist/core/background/service.d.ts +9 -0
  26. package/dist/core/background/service.d.ts.map +1 -1
  27. package/dist/core/background/service.js +13 -1
  28. package/dist/core/background/service.js.map +1 -1
  29. package/dist/core/background/session.d.ts +5 -0
  30. package/dist/core/background/session.d.ts.map +1 -1
  31. package/dist/core/background/session.js +2 -1
  32. package/dist/core/background/session.js.map +1 -1
  33. package/dist/core/background/types.d.ts +2 -0
  34. package/dist/core/background/types.d.ts.map +1 -1
  35. package/dist/core/background/types.js.map +1 -1
  36. package/dist/core/compaction/compaction.js +9 -9
  37. package/dist/core/compaction/compaction.js.map +1 -1
  38. package/dist/core/extensions/distribution-api.d.ts +41 -0
  39. package/dist/core/extensions/distribution-api.d.ts.map +1 -0
  40. package/dist/core/extensions/distribution-api.js +12 -0
  41. package/dist/core/extensions/distribution-api.js.map +1 -0
  42. package/dist/core/extensions/index.d.ts +1 -1
  43. package/dist/core/extensions/index.d.ts.map +1 -1
  44. package/dist/core/extensions/index.js.map +1 -1
  45. package/dist/core/extensions/loader.d.ts.map +1 -1
  46. package/dist/core/extensions/loader.js +1 -0
  47. package/dist/core/extensions/loader.js.map +1 -1
  48. package/dist/core/extensions/runner.d.ts.map +1 -1
  49. package/dist/core/extensions/runner.js +0 -2
  50. package/dist/core/extensions/runner.js.map +1 -1
  51. package/dist/core/extensions/types.d.ts +15 -45
  52. package/dist/core/extensions/types.d.ts.map +1 -1
  53. package/dist/core/extensions/types.js +1 -11
  54. package/dist/core/extensions/types.js.map +1 -1
  55. package/dist/core/extensions/virtual-modules.d.ts.map +1 -1
  56. package/dist/core/extensions/virtual-modules.js +2 -1
  57. package/dist/core/extensions/virtual-modules.js.map +1 -1
  58. package/dist/core/keybinding-registry.d.ts +11 -0
  59. package/dist/core/keybinding-registry.d.ts.map +1 -0
  60. package/dist/core/keybinding-registry.js +24 -0
  61. package/dist/core/keybinding-registry.js.map +1 -0
  62. package/dist/core/keybindings.d.ts +4 -64
  63. package/dist/core/keybindings.d.ts.map +1 -1
  64. package/dist/core/keybindings.js +2 -41
  65. package/dist/core/keybindings.js.map +1 -1
  66. package/dist/core/model-config.d.ts +70 -2
  67. package/dist/core/model-config.d.ts.map +1 -1
  68. package/dist/core/model-config.js +2 -26
  69. package/dist/core/model-config.js.map +1 -1
  70. package/dist/core/model-resolver.js +1 -1
  71. package/dist/core/model-resolver.js.map +1 -1
  72. package/dist/core/resource-loader.d.ts.map +1 -1
  73. package/dist/core/resource-loader.js +5 -11
  74. package/dist/core/resource-loader.js.map +1 -1
  75. package/dist/core/tools/bash.d.ts.map +1 -1
  76. package/dist/core/tools/bash.js +2 -2
  77. package/dist/core/tools/bash.js.map +1 -1
  78. package/dist/core/tools/shell-execution.d.ts +1 -0
  79. package/dist/core/tools/shell-execution.d.ts.map +1 -1
  80. package/dist/core/tools/shell-execution.js +21 -0
  81. package/dist/core/tools/shell-execution.js.map +1 -1
  82. package/dist/core/trust-manager.d.ts.map +1 -1
  83. package/dist/core/trust-manager.js +11 -48
  84. package/dist/core/trust-manager.js.map +1 -1
  85. package/dist/extensions/background/index.d.ts +2 -0
  86. package/dist/extensions/background/index.d.ts.map +1 -1
  87. package/dist/extensions/background/index.js +17 -0
  88. package/dist/extensions/background/index.js.map +1 -1
  89. package/dist/extensions/background/keybindings.d.ts +10 -0
  90. package/dist/extensions/background/keybindings.d.ts.map +1 -0
  91. package/dist/extensions/background/keybindings.js +14 -0
  92. package/dist/extensions/background/keybindings.js.map +1 -0
  93. package/dist/extensions/background/manager.d.ts +2 -0
  94. package/dist/extensions/background/manager.d.ts.map +1 -1
  95. package/dist/extensions/background/manager.js +1 -0
  96. package/dist/extensions/background/manager.js.map +1 -1
  97. package/dist/extensions/btw/controller.d.ts +1 -0
  98. package/dist/extensions/btw/controller.d.ts.map +1 -1
  99. package/dist/extensions/btw/controller.js +5 -5
  100. package/dist/extensions/btw/controller.js.map +1 -1
  101. package/dist/extensions/btw/index.js +1 -2
  102. package/dist/extensions/btw/index.js.map +1 -1
  103. package/dist/extensions/btw/keybindings.d.ts +10 -0
  104. package/dist/extensions/btw/keybindings.d.ts.map +1 -0
  105. package/dist/extensions/btw/keybindings.js +8 -0
  106. package/dist/extensions/btw/keybindings.js.map +1 -0
  107. package/dist/extensions/btw/panel.d.ts +1 -0
  108. package/dist/extensions/btw/panel.d.ts.map +1 -1
  109. package/dist/extensions/btw/panel.js +1 -0
  110. package/dist/extensions/btw/panel.js.map +1 -1
  111. package/dist/extensions/list-keybindings.d.ts +8 -0
  112. package/dist/extensions/list-keybindings.d.ts.map +1 -0
  113. package/dist/extensions/list-keybindings.js +8 -0
  114. package/dist/extensions/list-keybindings.js.map +1 -0
  115. package/dist/extensions/provider/compat-fields.d.ts.map +1 -1
  116. package/dist/extensions/provider/compat-fields.js +23 -3
  117. package/dist/extensions/provider/compat-fields.js.map +1 -1
  118. package/dist/extensions/provider/connection.d.ts.map +1 -1
  119. package/dist/extensions/provider/connection.js +1 -1
  120. package/dist/extensions/provider/connection.js.map +1 -1
  121. package/dist/extensions/provider/constants.d.ts +2 -0
  122. package/dist/extensions/provider/constants.d.ts.map +1 -1
  123. package/dist/extensions/provider/constants.js +2 -0
  124. package/dist/extensions/provider/constants.js.map +1 -1
  125. package/dist/extensions/provider/keybindings.d.ts +9 -0
  126. package/dist/extensions/provider/keybindings.d.ts.map +1 -0
  127. package/dist/extensions/provider/keybindings.js +12 -0
  128. package/dist/extensions/provider/keybindings.js.map +1 -0
  129. package/dist/extensions/provider/probe.d.ts +8 -4
  130. package/dist/extensions/provider/probe.d.ts.map +1 -1
  131. package/dist/extensions/provider/probe.js +169 -11
  132. package/dist/extensions/provider/probe.js.map +1 -1
  133. package/dist/extensions/provider/ui/builtin-data.d.ts +1 -0
  134. package/dist/extensions/provider/ui/builtin-data.d.ts.map +1 -1
  135. package/dist/extensions/provider/ui/builtin-data.js +1 -0
  136. package/dist/extensions/provider/ui/builtin-data.js.map +1 -1
  137. package/dist/extensions/provider/ui/compat.d.ts +1 -0
  138. package/dist/extensions/provider/ui/compat.d.ts.map +1 -1
  139. package/dist/extensions/provider/ui/compat.js +1 -0
  140. package/dist/extensions/provider/ui/compat.js.map +1 -1
  141. package/dist/extensions/provider/ui/dictionary.d.ts +1 -0
  142. package/dist/extensions/provider/ui/dictionary.d.ts.map +1 -1
  143. package/dist/extensions/provider/ui/dictionary.js +1 -0
  144. package/dist/extensions/provider/ui/dictionary.js.map +1 -1
  145. package/dist/extensions/provider/ui/editor.d.ts +1 -0
  146. package/dist/extensions/provider/ui/editor.d.ts.map +1 -1
  147. package/dist/extensions/provider/ui/editor.js +1 -0
  148. package/dist/extensions/provider/ui/editor.js.map +1 -1
  149. package/dist/extensions/provider/ui/fetch-models.d.ts +5 -3
  150. package/dist/extensions/provider/ui/fetch-models.d.ts.map +1 -1
  151. package/dist/extensions/provider/ui/fetch-models.js +28 -3
  152. package/dist/extensions/provider/ui/fetch-models.js.map +1 -1
  153. package/dist/extensions/provider/ui/model-fields.d.ts +1 -0
  154. package/dist/extensions/provider/ui/model-fields.d.ts.map +1 -1
  155. package/dist/extensions/provider/ui/model-fields.js +1 -0
  156. package/dist/extensions/provider/ui/model-fields.js.map +1 -1
  157. package/dist/extensions/provider/ui/model-options.d.ts +1 -0
  158. package/dist/extensions/provider/ui/model-options.d.ts.map +1 -1
  159. package/dist/extensions/provider/ui/model-options.js +1 -0
  160. package/dist/extensions/provider/ui/model-options.js.map +1 -1
  161. package/dist/extensions/provider/ui/provider-fields.d.ts +1 -0
  162. package/dist/extensions/provider/ui/provider-fields.d.ts.map +1 -1
  163. package/dist/extensions/provider/ui/provider-fields.js +1 -0
  164. package/dist/extensions/provider/ui/provider-fields.js.map +1 -1
  165. package/dist/extensions/question/dialog.d.ts +1 -0
  166. package/dist/extensions/question/dialog.d.ts.map +1 -1
  167. package/dist/extensions/question/dialog.js +1 -0
  168. package/dist/extensions/question/dialog.js.map +1 -1
  169. package/dist/extensions/question/keybindings.d.ts +2 -0
  170. package/dist/extensions/question/keybindings.d.ts.map +1 -0
  171. package/dist/extensions/question/keybindings.js +3 -0
  172. package/dist/extensions/question/keybindings.js.map +1 -0
  173. package/dist/index.d.ts +1 -1
  174. package/dist/index.d.ts.map +1 -1
  175. package/dist/index.js.map +1 -1
  176. package/dist/modes/interactive/editor-host.d.ts +43 -0
  177. package/dist/modes/interactive/editor-host.d.ts.map +1 -0
  178. package/dist/modes/interactive/editor-host.js +83 -0
  179. package/dist/modes/interactive/editor-host.js.map +1 -0
  180. package/dist/modes/interactive/interactive-mode.d.ts +1 -11
  181. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  182. package/dist/modes/interactive/interactive-mode.js +300 -360
  183. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  184. package/dist/modes/interactive/tool-chat.d.ts +21 -0
  185. package/dist/modes/interactive/tool-chat.d.ts.map +1 -0
  186. package/dist/modes/interactive/tool-chat.js +58 -0
  187. package/dist/modes/interactive/tool-chat.js.map +1 -0
  188. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  189. package/dist/modes/rpc/rpc-mode.js +0 -2
  190. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  191. package/dist/utils/path-identity.d.ts +12 -0
  192. package/dist/utils/path-identity.d.ts.map +1 -0
  193. package/dist/utils/path-identity.js +63 -0
  194. package/dist/utils/path-identity.js.map +1 -0
  195. package/dist/utils/shell.d.ts +0 -1
  196. package/dist/utils/shell.d.ts.map +1 -1
  197. package/dist/utils/shell.js +0 -21
  198. package/dist/utils/shell.js.map +1 -1
  199. package/dist/utils/syntax-highlight.d.ts +2 -0
  200. package/dist/utils/syntax-highlight.d.ts.map +1 -1
  201. package/dist/utils/syntax-highlight.js +2 -0
  202. package/dist/utils/syntax-highlight.js.map +1 -1
  203. package/docs/bundled/extensions/background.md +3 -3
  204. package/docs/bundled/extensions/provider.md +7 -3
  205. package/docs/bundled/extensions/subagent.md +1 -1
  206. package/docs/compaction.md +1 -1
  207. package/docs/extensions.md +15 -5
  208. package/docs/sdk.md +1 -1
  209. package/docs/usage.md +3 -0
  210. package/examples/README.md +11 -1
  211. package/examples/extensions/README.md +34 -14
  212. package/examples/extensions/auto-commit-on-exit.ts +1 -1
  213. package/examples/extensions/bash-spawn-hook.ts +2 -2
  214. package/examples/extensions/bookmark.ts +1 -1
  215. package/examples/extensions/border-status-editor.ts +6 -1
  216. package/examples/extensions/built-in-tool-renderer.ts +2 -2
  217. package/examples/extensions/claude-rules.ts +1 -1
  218. package/examples/extensions/commands.ts +1 -1
  219. package/examples/extensions/confirm-destructive.ts +1 -1
  220. package/examples/extensions/custom-compaction.ts +2 -2
  221. package/examples/extensions/custom-footer.ts +1 -1
  222. package/examples/extensions/custom-header.ts +2 -2
  223. package/examples/extensions/custom-provider-anthropic/index.ts +6 -6
  224. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  225. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  226. package/examples/extensions/custom-provider-gitlab-duo/index.ts +3 -3
  227. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  228. package/examples/extensions/dirty-repo-guard.ts +1 -1
  229. package/examples/extensions/doom-overlay/index.ts +1 -1
  230. package/examples/extensions/dynamic-resources/dynamic.json +3 -1
  231. package/examples/extensions/dynamic-resources/index.ts +1 -1
  232. package/examples/extensions/dynamic-tools.ts +1 -1
  233. package/examples/extensions/entry-renderer.ts +1 -1
  234. package/examples/extensions/event-bus.ts +1 -1
  235. package/examples/extensions/file-trigger.ts +1 -1
  236. package/examples/extensions/git-checkpoint.ts +1 -1
  237. package/examples/extensions/git-merge-and-resolve.ts +1 -1
  238. package/examples/extensions/github-issue-autocomplete.ts +1 -1
  239. package/examples/extensions/gondolin/index.ts +9 -7
  240. package/examples/extensions/gondolin/package-lock.json +2 -2
  241. package/examples/extensions/gondolin/package.json +1 -1
  242. package/examples/extensions/handoff.ts +2 -2
  243. package/examples/extensions/hello.ts +1 -1
  244. package/examples/extensions/hidden-thinking-label.ts +1 -1
  245. package/examples/extensions/inline-bash.ts +1 -1
  246. package/examples/extensions/input-transform-streaming.ts +1 -1
  247. package/examples/extensions/input-transform.ts +1 -1
  248. package/examples/extensions/interactive-shell.ts +1 -1
  249. package/examples/extensions/mac-system-theme.ts +1 -1
  250. package/examples/extensions/message-renderer.ts +1 -1
  251. package/examples/extensions/minimal-mode.ts +2 -2
  252. package/examples/extensions/modal-editor.ts +1 -1
  253. package/examples/extensions/model-status.ts +1 -1
  254. package/examples/extensions/notify.ts +1 -1
  255. package/examples/extensions/overlay-qa-tests.ts +3 -3
  256. package/examples/extensions/overlay-test.ts +1 -1
  257. package/examples/extensions/permission-gate.ts +1 -1
  258. package/examples/extensions/pirate.ts +1 -1
  259. package/examples/extensions/plan-mode/index.ts +1 -1
  260. package/examples/extensions/preset.ts +2 -2
  261. package/examples/extensions/project-trust.ts +3 -3
  262. package/examples/extensions/prompt-customizer.ts +1 -1
  263. package/examples/extensions/protected-paths.ts +1 -1
  264. package/examples/extensions/provider-payload.ts +1 -1
  265. package/examples/extensions/qna.ts +2 -2
  266. package/examples/extensions/question.ts +1 -1
  267. package/examples/extensions/questionnaire.ts +1 -1
  268. package/examples/extensions/rainbow-editor.ts +1 -1
  269. package/examples/extensions/reload-runtime.ts +1 -1
  270. package/examples/extensions/rpc-demo.ts +1 -1
  271. package/examples/extensions/sandbox/index.ts +2 -2
  272. package/examples/extensions/sandbox/package-lock.json +2 -2
  273. package/examples/extensions/sandbox/package.json +1 -1
  274. package/examples/extensions/send-user-message.ts +1 -1
  275. package/examples/extensions/session-name.ts +1 -1
  276. package/examples/extensions/shutdown-command.ts +1 -1
  277. package/examples/extensions/snake.ts +1 -1
  278. package/examples/extensions/space-invaders.ts +1 -1
  279. package/examples/extensions/ssh.ts +2 -2
  280. package/examples/extensions/status-line.ts +1 -1
  281. package/examples/extensions/structured-output.ts +1 -1
  282. package/examples/extensions/summarize.ts +2 -3
  283. package/examples/extensions/system-prompt-header.ts +1 -1
  284. package/examples/extensions/tic-tac-toe.ts +1 -1
  285. package/examples/extensions/timed-confirm.ts +1 -1
  286. package/examples/extensions/titlebar-spinner.ts +1 -1
  287. package/examples/extensions/todo.ts +1 -1
  288. package/examples/extensions/tool-override.ts +1 -1
  289. package/examples/extensions/tools.ts +2 -2
  290. package/examples/extensions/trigger-compact.ts +1 -1
  291. package/examples/extensions/truncated-tool.ts +4 -4
  292. package/examples/extensions/widget-placement.ts +1 -1
  293. package/examples/extensions/with-deps/index.ts +1 -1
  294. package/examples/extensions/with-deps/package-lock.json +2 -2
  295. package/examples/extensions/with-deps/package.json +1 -1
  296. package/examples/extensions/working-indicator.ts +1 -1
  297. package/examples/extensions/working-message-test.ts +1 -1
  298. package/examples/rpc-client.ts +35 -0
  299. package/examples/rpc-extension-ui.ts +25 -5
  300. package/npm-shrinkwrap.json +43 -43
  301. package/package.json +8 -8
@@ -58,9 +58,6 @@ export function parseSkillBlock(text) {
58
58
  userMessage: match[4]?.trim() || undefined,
59
59
  };
60
60
  }
61
- // ============================================================================
62
- // Types
63
- // ============================================================================
64
61
  function withoutDeletedHeaders(headers) {
65
62
  return headers
66
63
  ? Object.fromEntries(Object.entries(headers).filter((entry) => entry[1] !== null))
@@ -89,6 +86,8 @@ export class AgentSession {
89
86
  this._pendingNextTurnMessages = [];
90
87
  /** Context-only custom messages queued during a run, flushed once the current turn's tool results are in. */
91
88
  this._pendingCustomMessages = [];
89
+ /** Background completions queued for steering, awaiting the persistence that confirms delivery. */
90
+ this._queuedBackgroundCompletions = new Map();
92
91
  // Compaction state
93
92
  this._compactionAbortController = undefined;
94
93
  this._autoCompactionAbortController = undefined;
@@ -122,6 +121,12 @@ export class AgentSession {
122
121
  // Preserve host delivery identity before extension message_end transforms.
123
122
  // Content/details may be replaced, but a persisted replacement is still an ack.
124
123
  const acknowledgeBackground = event.type === "message_end" ? this._backgroundHost.messageAcknowledgement(event.message) : undefined;
124
+ // A queued completion is tracked by identity too, for the same reason.
125
+ const queuedBackgroundCompletion = event.type === "message_start" || event.type === "message_end"
126
+ ? this._queuedBackgroundCompletions.get(event.message)
127
+ : undefined;
128
+ if (event.type === "message_start" && queuedBackgroundCompletion)
129
+ queuedBackgroundCompletion.drained = true;
125
130
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
126
131
  // This ensures the UI sees the updated queue state
127
132
  if (event.type === "message_start" && event.message.role === "user") {
@@ -149,8 +154,10 @@ export class AgentSession {
149
154
  this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
150
155
  // Handle session persistence
151
156
  if (event.type === "message_end") {
152
- if (this._persistMessage(event.message))
157
+ if (this._persistMessage(event.message)) {
153
158
  acknowledgeBackground?.();
159
+ queuedBackgroundCompletion?.resolve();
160
+ }
154
161
  if (event.message.role === "assistant") {
155
162
  const assistantMsg = event.message;
156
163
  this._lastAssistantMessage = assistantMsg;
@@ -178,6 +185,8 @@ export class AgentSession {
178
185
  this._lastAssistantToolResults = event.toolResults;
179
186
  this._flushPendingCustomMessages();
180
187
  }
188
+ if (event.type === "agent_end")
189
+ this._failDrainedBackgroundCompletions();
181
190
  };
182
191
  this.agent = config.agent;
183
192
  this.sessionManager = config.sessionManager;
@@ -185,16 +194,15 @@ export class AgentSession {
185
194
  this._backgroundHost = new BackgroundSession({
186
195
  manager: this.sessionManager,
187
196
  role: config.executionRole ?? "main",
188
- canDeliver: () => this.isIdle &&
189
- !this.agent.state.isStreaming &&
190
- this.pendingMessageCount === 0 &&
191
- !this.agent.hasQueuedMessages() &&
192
- this.model !== undefined,
193
- deliver: (message) => {
194
- // Completion turns skip before_agent_start, but consume nextTurn context.
195
- const asides = this._pendingNextTurnMessages.slice();
196
- return this._runAgentPrompt(asides.length ? [message, ...asides] : message);
197
+ canDeliver: () => {
198
+ if (this.model === undefined || this.isCompacting)
199
+ return false;
200
+ // A completion steered into an active run follows the interactive steering queue, which
201
+ // already preserves the order of queued user input. An idle delivery starts its own
202
+ // turn, so it still waits for the queues to drain.
203
+ return this.isStreaming || (this.pendingMessageCount === 0 && !this.agent.hasQueuedMessages());
197
204
  },
205
+ deliver: (message) => this._deliverBackgroundCompletion(message),
198
206
  onEntry: (entry) => this._emit({ type: "entry_appended", entry }),
199
207
  onError: (event, error) => this._extensionRunner?.emitError({ extensionPath: "<background>", event, error }),
200
208
  });
@@ -243,6 +251,19 @@ export class AgentSession {
243
251
  retryBackgroundNotifications() {
244
252
  this._backgroundHost.retry();
245
253
  }
254
+ /**
255
+ * Run a lifecycle operation with delivery paused across its asynchronous gaps.
256
+ * Lifecycle methods are thin shells over upstream bodies so those keep upstream's shape.
257
+ */
258
+ async _withBackgroundPaused(operation) {
259
+ const resumeBackground = this.pauseBackgroundNotifications();
260
+ try {
261
+ return await operation();
262
+ }
263
+ finally {
264
+ resumeBackground();
265
+ }
266
+ }
246
267
  get modelRuntime() {
247
268
  return this._modelRuntime;
248
269
  }
@@ -635,28 +656,36 @@ export class AgentSession {
635
656
  this._resolveIdleWait = undefined;
636
657
  resolve();
637
658
  }
659
+ /**
660
+ * Background completions stay paused until runs requested by settled handlers have started,
661
+ * so a deferred prompt never races a delivery for the same idle gap.
662
+ */
638
663
  async _emitAgentSettled() {
664
+ return this._withBackgroundPaused(() => this._emitAgentSettledBody());
665
+ }
666
+ async _emitAgentSettledBody() {
639
667
  this._cacheWarmer?.onAgentSettled();
640
- // Background completions stay paused until runs requested by settled handlers have started,
641
- // so a deferred prompt never races a delivery for the same idle gap.
642
- const resumeBackground = this.pauseBackgroundNotifications();
643
668
  this._isAgentRunActive = false;
669
+ this._isEmittingAgentSettled = true;
644
670
  try {
645
- this._isEmittingAgentSettled = true;
671
+ await this._extensionRunner.emit({ type: "agent_settled" });
672
+ this._emit({ type: "agent_settled" });
673
+ }
674
+ finally {
675
+ this._isEmittingAgentSettled = false;
676
+ }
677
+ const deferred = this._deferredSettledActions.splice(0);
678
+ if (deferred.length > 0) {
646
679
  try {
647
- await this._extensionRunner.emit({ type: "agent_settled" });
648
- this._emit({ type: "agent_settled" });
680
+ for (const action of deferred)
681
+ await action();
649
682
  }
650
683
  finally {
651
- this._isEmittingAgentSettled = false;
684
+ this._resolveIdleWaitIfIdle();
652
685
  }
653
- for (const action of this._deferredSettledActions.splice(0))
654
- await action();
655
- }
656
- finally {
657
- this._resolveIdleWaitIfIdle();
658
- resumeBackground();
686
+ return;
659
687
  }
688
+ this._resolveIdleWaitIfIdle();
660
689
  }
661
690
  /**
662
691
  * Persist model/custom messages and consume queued context only after the append succeeds.
@@ -881,6 +910,9 @@ export class AgentSession {
881
910
  */
882
911
  dispose() {
883
912
  this._backgroundHost.dispose();
913
+ for (const queued of [...this._queuedBackgroundCompletions.values()]) {
914
+ queued.reject(new Error("the session was disposed"));
915
+ }
884
916
  try {
885
917
  this.abortRetry();
886
918
  this.abortCompaction();
@@ -1164,6 +1196,55 @@ export class AgentSession {
1164
1196
  await this._emitAgentSettled();
1165
1197
  }
1166
1198
  }
1199
+ /**
1200
+ * Deliver a background completion through the same queueing path as interactive input:
1201
+ * steering into an active run lands the result right after the current tool batch, and an
1202
+ * idle session starts its own turn. The promise settles only once the message is persisted,
1203
+ * or once it can no longer be persisted, so the caller never releases its claim for a
1204
+ * message the transcript does not contain.
1205
+ */
1206
+ _deliverBackgroundCompletion(message) {
1207
+ if (!this.isStreaming) {
1208
+ // Completion turns skip before_agent_start, but consume nextTurn context.
1209
+ const asides = this._pendingNextTurnMessages.slice();
1210
+ return this._runAgentPrompt(asides.length ? [message, ...asides] : message);
1211
+ }
1212
+ return new Promise((resolve, reject) => {
1213
+ const queued = {
1214
+ drained: false,
1215
+ resolve: () => {
1216
+ if (this._queuedBackgroundCompletions.get(message) !== queued)
1217
+ return;
1218
+ this._queuedBackgroundCompletions.delete(message);
1219
+ resolve();
1220
+ },
1221
+ reject: (reason) => {
1222
+ if (this._queuedBackgroundCompletions.get(message) !== queued)
1223
+ return;
1224
+ this._queuedBackgroundCompletions.delete(message);
1225
+ reject(reason);
1226
+ },
1227
+ };
1228
+ this._queuedBackgroundCompletions.set(message, queued);
1229
+ try {
1230
+ this.agent.steer(message);
1231
+ }
1232
+ catch (error) {
1233
+ queued.reject(error);
1234
+ }
1235
+ });
1236
+ }
1237
+ /**
1238
+ * Fail deliveries the owning run emitted but never persisted. A completion still sitting in
1239
+ * the steering queue when its run ends stays claimed: the next run delivers it, so failing
1240
+ * it here would persist the same message twice.
1241
+ */
1242
+ _failDrainedBackgroundCompletions() {
1243
+ for (const queued of [...this._queuedBackgroundCompletions.values()]) {
1244
+ if (queued.drained)
1245
+ queued.reject(new Error("the completion message was not persisted"));
1246
+ }
1247
+ }
1167
1248
  async _handlePostAgentRun() {
1168
1249
  const message = this._lastAssistantMessage;
1169
1250
  const toolResults = this._lastAssistantToolResults;
@@ -1274,134 +1355,132 @@ export class AgentSession {
1274
1355
  this.retryBackgroundNotifications();
1275
1356
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
1276
1357
  const preflightResult = options?.preflightResult;
1277
- // Observer commands may stay open indefinitely. Their lifecycle operations
1278
- // pause independently; only real model input owns the preflight pause.
1279
- if (expandPromptTemplates && text.startsWith("/")) {
1280
- try {
1281
- if (await this._tryExecuteExtensionCommand(text)) {
1358
+ let messages;
1359
+ let resumeBackground;
1360
+ try {
1361
+ // Handle extension commands first (execute immediately, even during streaming)
1362
+ // Extension commands manage their own LLM interaction via pi.sendMessage()
1363
+ if (expandPromptTemplates && text.startsWith("/")) {
1364
+ const handled = await this._tryExecuteExtensionCommand(text);
1365
+ if (handled) {
1366
+ // Extension command executed, no prompt to send
1282
1367
  preflightResult?.(true);
1283
1368
  return;
1284
1369
  }
1285
1370
  }
1286
- catch (error) {
1287
- preflightResult?.(false);
1288
- throw error;
1371
+ // Observer commands above may stay open indefinitely; only real model input owns
1372
+ // the preflight pause, released once the user message is established.
1373
+ resumeBackground = this.pauseBackgroundNotifications();
1374
+ if (this._compactionAbortController !== undefined) {
1375
+ throw new Error("Cannot submit a prompt while compaction is in progress. Wait for compaction to finish and retry.");
1289
1376
  }
1290
- }
1291
- const resumeBackground = this.pauseBackgroundNotifications();
1292
- try {
1293
- let messages;
1294
- try {
1295
- if (this._compactionAbortController !== undefined) {
1296
- throw new Error("Cannot submit a prompt while compaction is in progress. Wait for compaction to finish and retry.");
1297
- }
1298
- // Emit input event for extension interception (before skill/template expansion)
1299
- const processedInput = await this._runInputHandlers(text, options?.images, options?.source ?? "interactive", this.isStreaming ? options?.streamingBehavior : undefined);
1300
- if (!processedInput) {
1301
- preflightResult?.(true);
1302
- return;
1303
- }
1304
- const { text: currentText, images: currentImages } = processedInput;
1305
- // Expand skill commands (/skill:name args) and prompt templates (/template args)
1306
- let expandedText = currentText;
1307
- if (expandPromptTemplates) {
1308
- expandedText = this._expandSkillCommand(expandedText);
1309
- expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1310
- }
1311
- // If streaming, queue via steer() or followUp() based on option
1312
- if (this.isStreaming) {
1313
- if (!options?.streamingBehavior) {
1314
- throw new Error("Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.");
1315
- }
1316
- if (options.streamingBehavior === "followUp") {
1317
- await this._queueFollowUp(expandedText, currentImages);
1318
- }
1319
- else {
1320
- await this._queueSteer(expandedText, currentImages);
1321
- }
1322
- preflightResult?.(true);
1323
- return;
1377
+ // Emit input event for extension interception (before skill/template expansion)
1378
+ const processedInput = await this._runInputHandlers(text, options?.images, options?.source ?? "interactive", this.isStreaming ? options?.streamingBehavior : undefined);
1379
+ if (!processedInput) {
1380
+ preflightResult?.(true);
1381
+ return;
1382
+ }
1383
+ const { text: currentText, images: currentImages } = processedInput;
1384
+ // Expand skill commands (/skill:name args) and prompt templates (/template args)
1385
+ let expandedText = currentText;
1386
+ if (expandPromptTemplates) {
1387
+ expandedText = this._expandSkillCommand(expandedText);
1388
+ expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1389
+ }
1390
+ // If streaming, queue via steer() or followUp() based on option
1391
+ if (this.isStreaming) {
1392
+ if (!options?.streamingBehavior) {
1393
+ throw new Error("Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.");
1324
1394
  }
1325
- // Flush any pending bash and custom messages before the new prompt
1326
- this._flushPendingBashMessages();
1327
- this._flushPendingCustomMessages();
1328
- // Validate model
1329
- if (!this.model) {
1330
- throw new Error(formatNoModelSelectedMessage());
1395
+ if (options.streamingBehavior === "followUp") {
1396
+ await this._queueFollowUp(expandedText, currentImages);
1331
1397
  }
1332
- const hasConfiguredAuth = this._modelRuntime.hasConfiguredAuth(this.model.provider) ||
1333
- (await this._modelRuntime.checkAuth(this.model.provider)) !== undefined;
1334
- if (!hasConfiguredAuth) {
1335
- const isOAuth = this._modelRuntime.isUsingOAuth(this.model.provider);
1336
- if (isOAuth) {
1337
- throw new Error(`Authentication failed for "${this.model.provider}". ` +
1338
- `Credentials may have expired or network is unavailable. ` +
1339
- `Run '/login ${this.model.provider}' to re-authenticate.`);
1340
- }
1341
- throw new Error(formatNoApiKeyFoundMessage(this.model.provider));
1398
+ else {
1399
+ await this._queueSteer(expandedText, currentImages);
1342
1400
  }
1343
- // Check if we need to compact before sending (catches aborted responses).
1344
- // The user's new prompt is sent below, so do not call agent.continue() here.
1345
- const lastAssistant = this._findLastAssistantMessage();
1346
- if (lastAssistant) {
1347
- await this._checkCompaction(lastAssistant, false);
1401
+ preflightResult?.(true);
1402
+ return;
1403
+ }
1404
+ // Flush any pending bash and custom messages before the new prompt
1405
+ this._flushPendingBashMessages();
1406
+ this._flushPendingCustomMessages();
1407
+ // Validate model
1408
+ if (!this.model) {
1409
+ throw new Error(formatNoModelSelectedMessage());
1410
+ }
1411
+ const hasConfiguredAuth = this._modelRuntime.hasConfiguredAuth(this.model.provider) ||
1412
+ (await this._modelRuntime.checkAuth(this.model.provider)) !== undefined;
1413
+ if (!hasConfiguredAuth) {
1414
+ const isOAuth = this._modelRuntime.isUsingOAuth(this.model.provider);
1415
+ if (isOAuth) {
1416
+ throw new Error(`Authentication failed for "${this.model.provider}". ` +
1417
+ `Credentials may have expired or network is unavailable. ` +
1418
+ `Run '/login ${this.model.provider}' to re-authenticate.`);
1348
1419
  }
1349
- // Emit before_agent_start before normalizing images so extension-driven model
1350
- // selection determines the resize profile used for the request and history.
1351
- const selectedToolsBefore = this._baseSystemPromptOptions.selectedTools;
1352
- const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPromptOptions);
1353
- // Handlers may edit event.systemPromptOptions.selectedTools or call setActiveTools(),
1354
- // which updates the live loadout instead. An explicit edit wins; otherwise the live
1355
- // loadout is authoritative, so a setActiveTools() call is not undone here.
1356
- const handlerEditedTools = result.systemPromptOptions.selectedTools.length !== selectedToolsBefore.length ||
1357
- result.systemPromptOptions.selectedTools.some((name, index) => name !== selectedToolsBefore[index]);
1358
- if (!handlerEditedTools)
1359
- result.systemPromptOptions.selectedTools = this.getActiveToolNames();
1360
- const normalized = await this._normalizePromptImages(currentImages);
1361
- const userText = normalized.hints.length > 0 ? `${expandedText}\n\n${normalized.hints.join("\n")}` : expandedText;
1362
- // Build messages only after hooks and image normalization have completed.
1363
- messages = [];
1364
- const userContent = [{ type: "text", text: userText }];
1365
- userContent.push(...normalized.images);
1420
+ throw new Error(formatNoApiKeyFoundMessage(this.model.provider));
1421
+ }
1422
+ // Check if we need to compact before sending (catches aborted responses).
1423
+ // The user's new prompt is sent below, so do not call agent.continue() here.
1424
+ const lastAssistant = this._findLastAssistantMessage();
1425
+ if (lastAssistant) {
1426
+ await this._checkCompaction(lastAssistant, false);
1427
+ }
1428
+ // Emit before_agent_start before normalizing images so extension-driven model
1429
+ // selection determines the resize profile used for the request and history.
1430
+ const selectedToolsBefore = this._baseSystemPromptOptions.selectedTools;
1431
+ const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPromptOptions);
1432
+ // Handlers may edit event.systemPromptOptions.selectedTools or call setActiveTools(),
1433
+ // which updates the live loadout instead. An explicit edit wins; otherwise the live
1434
+ // loadout is authoritative, so a setActiveTools() call is not undone here.
1435
+ const handlerEditedTools = result.systemPromptOptions.selectedTools.length !== selectedToolsBefore.length ||
1436
+ result.systemPromptOptions.selectedTools.some((name, index) => name !== selectedToolsBefore[index]);
1437
+ if (!handlerEditedTools)
1438
+ result.systemPromptOptions.selectedTools = this.getActiveToolNames();
1439
+ const normalized = await this._normalizePromptImages(currentImages);
1440
+ const userText = normalized.hints.length > 0 ? `${expandedText}\n\n${normalized.hints.join("\n")}` : expandedText;
1441
+ // Build messages only after hooks and image normalization have completed.
1442
+ messages = [];
1443
+ const userContent = [{ type: "text", text: userText }];
1444
+ userContent.push(...normalized.images);
1445
+ messages.push({
1446
+ role: "user",
1447
+ content: userContent,
1448
+ timestamp: Date.now(),
1449
+ });
1450
+ // Inject any pending "nextTurn" messages as context alongside the user message.
1451
+ // They stay queued until _persistMessage() confirms the append.
1452
+ for (const msg of this._pendingNextTurnMessages) {
1453
+ messages.push(msg);
1454
+ }
1455
+ for (const msg of result.messages) {
1366
1456
  messages.push({
1367
- role: "user",
1368
- content: userContent,
1457
+ role: "custom",
1458
+ customType: msg.customType,
1459
+ // Untyped extensions can pass null/missing content; normalize at ingestion.
1460
+ content: msg.content ?? [],
1461
+ display: msg.display,
1462
+ details: msg.details,
1369
1463
  timestamp: Date.now(),
1370
1464
  });
1371
- // Inject any pending "nextTurn" messages as context alongside the user message.
1372
- // They stay queued until _persistMessage() confirms the append.
1373
- for (const msg of this._pendingNextTurnMessages) {
1374
- messages.push(msg);
1375
- }
1376
- for (const msg of result.messages) {
1377
- messages.push({
1378
- role: "custom",
1379
- customType: msg.customType,
1380
- // Untyped extensions can pass null/missing content; normalize at ingestion.
1381
- content: msg.content ?? [],
1382
- display: msg.display,
1383
- details: msg.details,
1384
- timestamp: Date.now(),
1385
- });
1386
- }
1387
- const updateMessage = this._preparePromptAndToolLoadout(result.systemPromptOptions);
1388
- this._runSystemPromptOptions = result.systemPromptOptions;
1389
- if (updateMessage)
1390
- messages.unshift(updateMessage);
1391
- }
1392
- catch (error) {
1393
- preflightResult?.(false);
1394
- throw error;
1395
- }
1396
- if (!messages) {
1397
- return;
1398
1465
  }
1399
- preflightResult?.(true);
1400
- await this._runAgentPrompt(messages);
1466
+ const updateMessage = this._preparePromptAndToolLoadout(result.systemPromptOptions);
1467
+ this._runSystemPromptOptions = result.systemPromptOptions;
1468
+ if (updateMessage)
1469
+ messages.unshift(updateMessage);
1470
+ }
1471
+ catch (error) {
1472
+ preflightResult?.(false);
1473
+ throw error;
1401
1474
  }
1402
1475
  finally {
1403
- resumeBackground();
1476
+ // Delivery is timer-scheduled, so a completion cannot start before the run below does.
1477
+ resumeBackground?.();
1404
1478
  }
1479
+ if (!messages) {
1480
+ return;
1481
+ }
1482
+ preflightResult?.(true);
1483
+ await this._runAgentPrompt(messages);
1405
1484
  }
1406
1485
  /**
1407
1486
  * Try to execute an extension command. Returns true if command was found and executed.
@@ -1658,6 +1737,10 @@ export class AgentSession {
1658
1737
  this._steeringMessages = [];
1659
1738
  this._followUpMessages = [];
1660
1739
  this.agent.clearAllQueues();
1740
+ // Clearing the queues drops queued completions; fail them so their claims retry.
1741
+ for (const queued of [...this._queuedBackgroundCompletions.values()]) {
1742
+ queued.reject(new Error("the completion message was dequeued before delivery"));
1743
+ }
1661
1744
  this._emitQueueUpdate();
1662
1745
  return { steering, followUp };
1663
1746
  }
@@ -1937,137 +2020,134 @@ export class AgentSession {
1937
2020
  * @param customInstructions Optional instructions for the compaction summary
1938
2021
  */
1939
2022
  async compact(customInstructions) {
1940
- const resumeBackground = this.pauseBackgroundNotifications();
2023
+ return this._withBackgroundPaused(() => this._compactBody(customInstructions));
2024
+ }
2025
+ async _compactBody(customInstructions) {
2026
+ await this.abort();
2027
+ this._compactionAbortController = new AbortController();
2028
+ this._emit({ type: "compaction_start", reason: "manual" });
2029
+ let fromExtension = false;
2030
+ let cancelledByExtension = false;
1941
2031
  try {
1942
- await this.abort();
1943
- this._compactionAbortController = new AbortController();
1944
- this._emit({ type: "compaction_start", reason: "manual" });
1945
- let fromExtension = false;
1946
- let cancelledByExtension = false;
1947
- try {
1948
- const model = this.model;
1949
- if (!model) {
1950
- throw new Error(formatNoModelSelectedMessage());
1951
- }
1952
- const settings = this.settingsManager.getCompactionSettings(model);
1953
- const { model: requestModel, apiKey, headers, env, } = await this._getSummarizationRequestAuth(model, this._compactionAbortController.signal);
1954
- const pathEntries = this.sessionManager.getBranch();
1955
- const preparation = prepareCompaction(pathEntries, settings);
1956
- if (!preparation) {
1957
- // Check why we can't compact
1958
- const lastEntry = pathEntries[pathEntries.length - 1];
1959
- if (lastEntry?.type === "compaction") {
1960
- throw new Error("Already compacted");
1961
- }
1962
- throw new Error("Nothing to compact (session too small)");
1963
- }
1964
- let extensionCompaction;
1965
- if (this._extensionRunner.hasHandlers("session_before_compact")) {
1966
- const result = (await this._extensionRunner.emit({
1967
- type: "session_before_compact",
1968
- preparation,
1969
- branchEntries: pathEntries,
1970
- customInstructions,
1971
- reason: "manual",
1972
- willRetry: false,
1973
- signal: this._compactionAbortController.signal,
1974
- }));
1975
- if (result?.cancel) {
1976
- cancelledByExtension = true;
1977
- throw new Error("Compaction cancelled");
1978
- }
1979
- if (result?.compaction) {
1980
- extensionCompaction = result.compaction;
1981
- fromExtension = true;
1982
- }
1983
- }
1984
- let summary;
1985
- let firstKeptEntryId;
1986
- let tokensBefore;
1987
- let usage;
1988
- let details;
1989
- if (extensionCompaction) {
1990
- // Extension provided compaction content
1991
- summary = extensionCompaction.summary;
1992
- firstKeptEntryId = extensionCompaction.firstKeptEntryId;
1993
- tokensBefore = extensionCompaction.tokensBefore;
1994
- usage = extensionCompaction.usage;
1995
- details = extensionCompaction.details;
1996
- }
1997
- else {
1998
- // Shared default summary generator, also used by automatic compaction.
1999
- const result = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, customInstructions, this._compactionAbortController.signal, env, "manual");
2000
- summary = result.summary;
2001
- firstKeptEntryId = result.firstKeptEntryId;
2002
- tokensBefore = result.tokensBefore;
2003
- usage = result.usage;
2004
- details = result.details;
2032
+ const model = this.model;
2033
+ if (!model) {
2034
+ throw new Error(formatNoModelSelectedMessage());
2035
+ }
2036
+ const settings = this.settingsManager.getCompactionSettings(model);
2037
+ const { model: requestModel, apiKey, headers, env, } = await this._getSummarizationRequestAuth(model, this._compactionAbortController.signal);
2038
+ const pathEntries = this.sessionManager.getBranch();
2039
+ const preparation = prepareCompaction(pathEntries, settings);
2040
+ if (!preparation) {
2041
+ // Check why we can't compact
2042
+ const lastEntry = pathEntries[pathEntries.length - 1];
2043
+ if (lastEntry?.type === "compaction") {
2044
+ throw new Error("Already compacted");
2005
2045
  }
2006
- if (this._compactionAbortController.signal.aborted) {
2046
+ throw new Error("Nothing to compact (session too small)");
2047
+ }
2048
+ let extensionCompaction;
2049
+ if (this._extensionRunner.hasHandlers("session_before_compact")) {
2050
+ const result = (await this._extensionRunner.emit({
2051
+ type: "session_before_compact",
2052
+ preparation,
2053
+ branchEntries: pathEntries,
2054
+ customInstructions,
2055
+ reason: "manual",
2056
+ willRetry: false,
2057
+ signal: this._compactionAbortController.signal,
2058
+ }));
2059
+ if (result?.cancel) {
2060
+ cancelledByExtension = true;
2007
2061
  throw new Error("Compaction cancelled");
2008
2062
  }
2009
- this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
2010
- const newEntries = this.sessionManager.getEntries();
2011
- this._refreshFinalizedContext();
2012
- const estimatedTokensAfter = estimateMessagesTokens(this.sessionManager.buildSessionProjection().messages);
2013
- // Get the saved compaction entry for the extension event
2014
- const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
2015
- if (this._extensionRunner && savedCompactionEntry) {
2016
- await this._extensionRunner.emit({
2017
- type: "session_compact",
2018
- compactionEntry: savedCompactionEntry,
2019
- fromExtension,
2020
- reason: "manual",
2021
- willRetry: false,
2022
- });
2063
+ if (result?.compaction) {
2064
+ extensionCompaction = result.compaction;
2065
+ fromExtension = true;
2023
2066
  }
2024
- const compactionResult = {
2025
- summary,
2026
- firstKeptEntryId,
2027
- tokensBefore,
2028
- estimatedTokensAfter,
2029
- usage,
2030
- details,
2031
- };
2032
- // compaction_end listeners may submit queued prompts, so expose idle state before notifying them.
2033
- this._clearManualCompactionState();
2034
- this._emit({
2035
- type: "compaction_end",
2036
- reason: "manual",
2037
- result: compactionResult,
2038
- aborted: false,
2039
- willRetry: false,
2040
- });
2041
- return compactionResult;
2042
2067
  }
2043
- catch (error) {
2044
- const message = error instanceof Error ? error.message : String(error);
2045
- const aborted = this._compactionAbortController?.signal.aborted === true || cancelledByExtension;
2046
- const errorMessage = aborted ? undefined : `Compaction failed: ${message}`;
2047
- this._clearManualCompactionState();
2048
- this._emit({
2049
- type: "compaction_end",
2068
+ let summary;
2069
+ let firstKeptEntryId;
2070
+ let tokensBefore;
2071
+ let usage;
2072
+ let details;
2073
+ if (extensionCompaction) {
2074
+ // Extension provided compaction content
2075
+ summary = extensionCompaction.summary;
2076
+ firstKeptEntryId = extensionCompaction.firstKeptEntryId;
2077
+ tokensBefore = extensionCompaction.tokensBefore;
2078
+ usage = extensionCompaction.usage;
2079
+ details = extensionCompaction.details;
2080
+ }
2081
+ else {
2082
+ // Shared default summary generator, also used by automatic compaction.
2083
+ const result = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, customInstructions, this._compactionAbortController.signal, env, "manual");
2084
+ summary = result.summary;
2085
+ firstKeptEntryId = result.firstKeptEntryId;
2086
+ tokensBefore = result.tokensBefore;
2087
+ usage = result.usage;
2088
+ details = result.details;
2089
+ }
2090
+ if (this._compactionAbortController.signal.aborted) {
2091
+ throw new Error("Compaction cancelled");
2092
+ }
2093
+ this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
2094
+ const newEntries = this.sessionManager.getEntries();
2095
+ this._refreshFinalizedContext();
2096
+ const estimatedTokensAfter = estimateMessagesTokens(this.sessionManager.buildSessionProjection().messages);
2097
+ // Get the saved compaction entry for the extension event
2098
+ const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
2099
+ if (this._extensionRunner && savedCompactionEntry) {
2100
+ await this._extensionRunner.emit({
2101
+ type: "session_compact",
2102
+ compactionEntry: savedCompactionEntry,
2103
+ fromExtension,
2050
2104
  reason: "manual",
2051
- result: undefined,
2052
- aborted,
2053
2105
  willRetry: false,
2054
- errorMessage,
2055
2106
  });
2056
- await this._emitSessionCompactFailed({
2057
- reason: "manual",
2058
- errorMessage,
2059
- aborted,
2060
- willRetry: false,
2061
- fromExtension,
2062
- });
2063
- throw error;
2064
- }
2065
- finally {
2066
- this._clearManualCompactionState();
2067
2107
  }
2108
+ const compactionResult = {
2109
+ summary,
2110
+ firstKeptEntryId,
2111
+ tokensBefore,
2112
+ estimatedTokensAfter,
2113
+ usage,
2114
+ details,
2115
+ };
2116
+ // compaction_end listeners may submit queued prompts, so expose idle state before notifying them.
2117
+ this._clearManualCompactionState();
2118
+ this._emit({
2119
+ type: "compaction_end",
2120
+ reason: "manual",
2121
+ result: compactionResult,
2122
+ aborted: false,
2123
+ willRetry: false,
2124
+ });
2125
+ return compactionResult;
2126
+ }
2127
+ catch (error) {
2128
+ const message = error instanceof Error ? error.message : String(error);
2129
+ const aborted = this._compactionAbortController.signal.aborted || cancelledByExtension;
2130
+ const errorMessage = aborted ? undefined : `Compaction failed: ${message}`;
2131
+ this._clearManualCompactionState();
2132
+ this._emit({
2133
+ type: "compaction_end",
2134
+ reason: "manual",
2135
+ result: undefined,
2136
+ aborted,
2137
+ willRetry: false,
2138
+ errorMessage,
2139
+ });
2140
+ await this._emitSessionCompactFailed({
2141
+ reason: "manual",
2142
+ errorMessage,
2143
+ aborted,
2144
+ willRetry: false,
2145
+ fromExtension,
2146
+ });
2147
+ throw error;
2068
2148
  }
2069
2149
  finally {
2070
- resumeBackground();
2150
+ this._clearManualCompactionState();
2071
2151
  }
2072
2152
  }
2073
2153
  /**
@@ -2230,140 +2310,137 @@ export class AgentSession {
2230
2310
  * @returns Whether the post-run loop should call `agent.continue()`
2231
2311
  */
2232
2312
  async _runAutoCompaction(reason, willRetry) {
2233
- const resumeBackground = this.pauseBackgroundNotifications();
2313
+ return this._withBackgroundPaused(() => this._runAutoCompactionBody(reason, willRetry));
2314
+ }
2315
+ async _runAutoCompactionBody(reason, willRetry) {
2316
+ const model = this.model;
2317
+ const settings = this.settingsManager.getCompactionSettings(model);
2318
+ let abortController;
2319
+ let started = false;
2320
+ let fromExtension = false;
2321
+ let cancelledByExtension = false;
2234
2322
  try {
2235
- const model = this.model;
2236
- const settings = this.settingsManager.getCompactionSettings(model);
2237
- let abortController;
2238
- let started = false;
2239
- let fromExtension = false;
2240
- let cancelledByExtension = false;
2241
- try {
2242
- if (!model) {
2243
- return false;
2244
- }
2245
- const pathEntries = this.sessionManager.getBranch();
2246
- const preparation = prepareCompaction(pathEntries, settings);
2247
- if (!preparation) {
2248
- return false;
2249
- }
2250
- abortController = new AbortController();
2251
- this._autoCompactionAbortController = abortController;
2252
- started = true;
2253
- this._emit({ type: "compaction_start", reason });
2254
- abortController.signal.throwIfAborted();
2255
- const { model: requestModel, apiKey, headers, env, } = await this._getSummarizationRequestAuth(model, abortController.signal);
2256
- abortController.signal.throwIfAborted();
2257
- let extensionCompaction;
2258
- if (this._extensionRunner.hasHandlers("session_before_compact")) {
2259
- const extensionResult = (await this._extensionRunner.emit({
2260
- type: "session_before_compact",
2261
- preparation,
2262
- branchEntries: pathEntries,
2263
- customInstructions: undefined,
2264
- reason,
2265
- willRetry,
2266
- signal: abortController.signal,
2267
- }));
2268
- if (extensionResult?.cancel) {
2269
- cancelledByExtension = true;
2270
- throw new Error("Compaction cancelled");
2271
- }
2272
- if (extensionResult?.compaction) {
2273
- extensionCompaction = extensionResult.compaction;
2274
- fromExtension = true;
2275
- }
2276
- }
2277
- abortController.signal.throwIfAborted();
2278
- let summary;
2279
- let firstKeptEntryId;
2280
- let tokensBefore;
2281
- let usage;
2282
- let details;
2283
- if (extensionCompaction) {
2284
- // Extension provided compaction content
2285
- summary = extensionCompaction.summary;
2286
- firstKeptEntryId = extensionCompaction.firstKeptEntryId;
2287
- tokensBefore = extensionCompaction.tokensBefore;
2288
- usage = extensionCompaction.usage;
2289
- details = extensionCompaction.details;
2290
- }
2291
- else {
2292
- // Shared default summary generator, also used by manual compaction.
2293
- const compactResult = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, undefined, abortController.signal, env, reason);
2294
- summary = compactResult.summary;
2295
- firstKeptEntryId = compactResult.firstKeptEntryId;
2296
- tokensBefore = compactResult.tokensBefore;
2297
- usage = compactResult.usage;
2298
- details = compactResult.details;
2299
- }
2300
- abortController.signal.throwIfAborted();
2301
- this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
2302
- const newEntries = this.sessionManager.getEntries();
2303
- this._refreshFinalizedContext();
2304
- const estimatedTokensAfter = estimateMessagesTokens(this.sessionManager.buildSessionProjection().messages);
2305
- // Get the saved compaction entry for the extension event
2306
- const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
2307
- if (this._extensionRunner && savedCompactionEntry) {
2308
- await this._extensionRunner.emit({
2309
- type: "session_compact",
2310
- compactionEntry: savedCompactionEntry,
2311
- fromExtension,
2312
- reason,
2313
- willRetry,
2314
- });
2315
- }
2316
- const result = {
2317
- summary,
2318
- firstKeptEntryId,
2319
- tokensBefore,
2320
- estimatedTokensAfter,
2321
- usage,
2322
- details,
2323
- };
2324
- this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
2325
- if (willRetry)
2326
- return true;
2327
- // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
2328
- // Continue once so queued messages are delivered.
2329
- return this.agent.hasQueuedMessages();
2323
+ if (!model) {
2324
+ return false;
2330
2325
  }
2331
- catch (error) {
2332
- const message = error instanceof Error ? error.message : "compaction failed";
2333
- const aborted = abortController?.signal.aborted === true || cancelledByExtension;
2334
- if (started) {
2335
- const errorMessage = aborted
2336
- ? undefined
2337
- : reason === "overflow"
2338
- ? `Context overflow recovery failed: ${message}`
2339
- : `Auto-compaction failed: ${message}`;
2340
- this._emit({
2341
- type: "compaction_end",
2342
- reason,
2343
- result: undefined,
2344
- aborted,
2345
- willRetry: false,
2346
- errorMessage,
2347
- });
2348
- await this._emitSessionCompactFailed({
2349
- reason,
2350
- errorMessage,
2351
- aborted,
2352
- willRetry: false,
2353
- fromExtension,
2354
- });
2355
- }
2326
+ const pathEntries = this.sessionManager.getBranch();
2327
+ const preparation = prepareCompaction(pathEntries, settings);
2328
+ if (!preparation) {
2356
2329
  return false;
2357
2330
  }
2358
- finally {
2359
- if (this._autoCompactionAbortController === abortController) {
2360
- this._autoCompactionAbortController = undefined;
2331
+ abortController = new AbortController();
2332
+ this._autoCompactionAbortController = abortController;
2333
+ started = true;
2334
+ this._emit({ type: "compaction_start", reason });
2335
+ abortController.signal.throwIfAborted();
2336
+ const { model: requestModel, apiKey, headers, env, } = await this._getSummarizationRequestAuth(model, abortController.signal);
2337
+ abortController.signal.throwIfAborted();
2338
+ let extensionCompaction;
2339
+ if (this._extensionRunner.hasHandlers("session_before_compact")) {
2340
+ const extensionResult = (await this._extensionRunner.emit({
2341
+ type: "session_before_compact",
2342
+ preparation,
2343
+ branchEntries: pathEntries,
2344
+ customInstructions: undefined,
2345
+ reason,
2346
+ willRetry,
2347
+ signal: abortController.signal,
2348
+ }));
2349
+ if (extensionResult?.cancel) {
2350
+ cancelledByExtension = true;
2351
+ throw new Error("Compaction cancelled");
2361
2352
  }
2362
- this._resolveIdleWaitIfIdle();
2353
+ if (extensionResult?.compaction) {
2354
+ extensionCompaction = extensionResult.compaction;
2355
+ fromExtension = true;
2356
+ }
2357
+ }
2358
+ abortController.signal.throwIfAborted();
2359
+ let summary;
2360
+ let firstKeptEntryId;
2361
+ let tokensBefore;
2362
+ let usage;
2363
+ let details;
2364
+ if (extensionCompaction) {
2365
+ // Extension provided compaction content
2366
+ summary = extensionCompaction.summary;
2367
+ firstKeptEntryId = extensionCompaction.firstKeptEntryId;
2368
+ tokensBefore = extensionCompaction.tokensBefore;
2369
+ usage = extensionCompaction.usage;
2370
+ details = extensionCompaction.details;
2371
+ }
2372
+ else {
2373
+ // Shared default summary generator, also used by manual compaction.
2374
+ const compactResult = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, undefined, abortController.signal, env, reason);
2375
+ summary = compactResult.summary;
2376
+ firstKeptEntryId = compactResult.firstKeptEntryId;
2377
+ tokensBefore = compactResult.tokensBefore;
2378
+ usage = compactResult.usage;
2379
+ details = compactResult.details;
2380
+ }
2381
+ abortController.signal.throwIfAborted();
2382
+ this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
2383
+ const newEntries = this.sessionManager.getEntries();
2384
+ this._refreshFinalizedContext();
2385
+ const estimatedTokensAfter = estimateMessagesTokens(this.sessionManager.buildSessionProjection().messages);
2386
+ // Get the saved compaction entry for the extension event
2387
+ const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
2388
+ if (this._extensionRunner && savedCompactionEntry) {
2389
+ await this._extensionRunner.emit({
2390
+ type: "session_compact",
2391
+ compactionEntry: savedCompactionEntry,
2392
+ fromExtension,
2393
+ reason,
2394
+ willRetry,
2395
+ });
2396
+ }
2397
+ const result = {
2398
+ summary,
2399
+ firstKeptEntryId,
2400
+ tokensBefore,
2401
+ estimatedTokensAfter,
2402
+ usage,
2403
+ details,
2404
+ };
2405
+ this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
2406
+ if (willRetry)
2407
+ return true;
2408
+ // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
2409
+ // Continue once so queued messages are delivered.
2410
+ return this.agent.hasQueuedMessages();
2411
+ }
2412
+ catch (error) {
2413
+ const message = error instanceof Error ? error.message : "compaction failed";
2414
+ const aborted = abortController?.signal.aborted === true || cancelledByExtension;
2415
+ if (started) {
2416
+ const errorMessage = aborted
2417
+ ? undefined
2418
+ : reason === "overflow"
2419
+ ? `Context overflow recovery failed: ${message}`
2420
+ : `Auto-compaction failed: ${message}`;
2421
+ this._emit({
2422
+ type: "compaction_end",
2423
+ reason,
2424
+ result: undefined,
2425
+ aborted,
2426
+ willRetry: false,
2427
+ errorMessage,
2428
+ });
2429
+ await this._emitSessionCompactFailed({
2430
+ reason,
2431
+ errorMessage,
2432
+ aborted,
2433
+ willRetry: false,
2434
+ fromExtension,
2435
+ });
2363
2436
  }
2437
+ return false;
2364
2438
  }
2365
2439
  finally {
2366
- resumeBackground();
2440
+ if (this._autoCompactionAbortController === abortController) {
2441
+ this._autoCompactionAbortController = undefined;
2442
+ }
2443
+ this._resolveIdleWaitIfIdle();
2367
2444
  }
2368
2445
  }
2369
2446
  /**
@@ -2377,36 +2454,33 @@ export class AgentSession {
2377
2454
  return this.settingsManager.getCompactionEnabled();
2378
2455
  }
2379
2456
  async bindExtensions(bindings) {
2380
- const resumeBackground = this.pauseBackgroundNotifications();
2381
- try {
2382
- if (bindings.backgroundEnabled !== undefined) {
2383
- this._backgroundHost.setEnabled(bindings.backgroundEnabled);
2384
- }
2385
- if (bindings.uiContext !== undefined) {
2386
- this._extensionUIContext = bindings.uiContext;
2387
- }
2388
- if (bindings.mode !== undefined) {
2389
- this._extensionMode = bindings.mode;
2390
- }
2391
- if (bindings.commandContextActions !== undefined) {
2392
- this._extensionCommandContextActions = bindings.commandContextActions;
2393
- }
2394
- if (bindings.abortHandler !== undefined) {
2395
- this._extensionAbortHandler = bindings.abortHandler;
2396
- }
2397
- if (bindings.shutdownHandler !== undefined) {
2398
- this._extensionShutdownHandler = bindings.shutdownHandler;
2399
- }
2400
- if (bindings.onError !== undefined) {
2401
- this._extensionErrorListener = bindings.onError;
2402
- }
2403
- this._applyExtensionBindings(this._extensionRunner);
2404
- await this._extensionRunner.emit(this._sessionStartEvent);
2405
- await this.extendResourcesFromExtensions(this._sessionStartEvent.reason === "reload" ? "reload" : "startup");
2457
+ return this._withBackgroundPaused(() => this._bindExtensionsBody(bindings));
2458
+ }
2459
+ async _bindExtensionsBody(bindings) {
2460
+ if (bindings.backgroundEnabled !== undefined) {
2461
+ this._backgroundHost.setEnabled(bindings.backgroundEnabled);
2406
2462
  }
2407
- finally {
2408
- resumeBackground();
2463
+ if (bindings.uiContext !== undefined) {
2464
+ this._extensionUIContext = bindings.uiContext;
2465
+ }
2466
+ if (bindings.mode !== undefined) {
2467
+ this._extensionMode = bindings.mode;
2409
2468
  }
2469
+ if (bindings.commandContextActions !== undefined) {
2470
+ this._extensionCommandContextActions = bindings.commandContextActions;
2471
+ }
2472
+ if (bindings.abortHandler !== undefined) {
2473
+ this._extensionAbortHandler = bindings.abortHandler;
2474
+ }
2475
+ if (bindings.shutdownHandler !== undefined) {
2476
+ this._extensionShutdownHandler = bindings.shutdownHandler;
2477
+ }
2478
+ if (bindings.onError !== undefined) {
2479
+ this._extensionErrorListener = bindings.onError;
2480
+ }
2481
+ this._applyExtensionBindings(this._extensionRunner);
2482
+ await this._extensionRunner.emit(this._sessionStartEvent);
2483
+ await this.extendResourcesFromExtensions(this._sessionStartEvent.reason === "reload" ? "reload" : "startup");
2410
2484
  }
2411
2485
  async extendResourcesFromExtensions(reason) {
2412
2486
  if (!this._extensionRunner.hasHandlers("resources_discover")) {
@@ -2698,37 +2772,34 @@ export class AgentSession {
2698
2772
  });
2699
2773
  }
2700
2774
  async reload(options) {
2701
- const resumeBackground = this.pauseBackgroundNotifications();
2702
- try {
2703
- const oldRunner = this._extensionRunner;
2704
- const previousFlagValues = oldRunner.getFlagValues();
2705
- this.background.close();
2706
- await Promise.all([this.background.shutdown(), this.abort()]);
2707
- await emitSessionShutdownEvent(oldRunner, { type: "session_shutdown", reason: "reload" });
2708
- oldRunner.invalidate();
2709
- await this.settingsManager.reload();
2710
- this.syncQueueModesFromSettings();
2711
- resetApiProviders();
2712
- await this._resourceLoader.reload();
2713
- this._backgroundHost.replaceService();
2714
- this._buildRuntime({
2715
- activeToolNames: this.getActiveToolNames(),
2716
- flagValues: previousFlagValues,
2717
- includeAllExtensionTools: true,
2718
- });
2719
- const hasBindings = this.background.enabled ||
2720
- this._extensionUIContext ||
2721
- this._extensionCommandContextActions ||
2722
- this._extensionShutdownHandler ||
2723
- this._extensionErrorListener;
2724
- if (hasBindings) {
2725
- await options?.beforeSessionStart?.();
2726
- await this._extensionRunner.emit({ type: "session_start", reason: "reload" });
2727
- await this.extendResourcesFromExtensions("reload");
2728
- }
2729
- }
2730
- finally {
2731
- resumeBackground();
2775
+ return this._withBackgroundPaused(() => this._reloadBody(options));
2776
+ }
2777
+ async _reloadBody(options) {
2778
+ const oldRunner = this._extensionRunner;
2779
+ const previousFlagValues = oldRunner.getFlagValues();
2780
+ this.background.close();
2781
+ await Promise.all([this.background.shutdown(), this.abort()]);
2782
+ await emitSessionShutdownEvent(oldRunner, { type: "session_shutdown", reason: "reload" });
2783
+ oldRunner.invalidate();
2784
+ await this.settingsManager.reload();
2785
+ this.syncQueueModesFromSettings();
2786
+ resetApiProviders();
2787
+ await this._resourceLoader.reload();
2788
+ this._backgroundHost.replaceService();
2789
+ this._buildRuntime({
2790
+ activeToolNames: this.getActiveToolNames(),
2791
+ flagValues: previousFlagValues,
2792
+ includeAllExtensionTools: true,
2793
+ });
2794
+ const hasBindings = this.background.enabled ||
2795
+ this._extensionUIContext ||
2796
+ this._extensionCommandContextActions ||
2797
+ this._extensionShutdownHandler ||
2798
+ this._extensionErrorListener;
2799
+ if (hasBindings) {
2800
+ await options?.beforeSessionStart?.();
2801
+ await this._extensionRunner.emit({ type: "session_start", reason: "reload" });
2802
+ await this.extendResourcesFromExtensions("reload");
2732
2803
  }
2733
2804
  }
2734
2805
  // =========================================================================
@@ -3272,7 +3343,7 @@ export class AgentSession {
3272
3343
  if (!model) {
3273
3344
  throw new Error("No model selected");
3274
3345
  }
3275
- const { model: requestModel, apiKey, headers, env, } = await this._getSummarizationRequestAuth(model, options.signal);
3346
+ const { model: requestModel, apiKey, headers, env } = await this._getSummarizationRequestAuth(model);
3276
3347
  return generateBugReportSummary({
3277
3348
  messages: this.messages,
3278
3349
  hint: options.hint,