@code-yeongyu/senpi 2026.7.9 → 2026.7.10

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 (207) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/README.md +1 -1
  3. package/dist/cli/args.js +3 -3
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/cli/config-selector.d.ts +4 -2
  6. package/dist/cli/config-selector.d.ts.map +1 -1
  7. package/dist/cli/config-selector.js +1 -1
  8. package/dist/cli/config-selector.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +18 -3
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +153 -36
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/cache-stats.d.ts +49 -0
  14. package/dist/core/cache-stats.d.ts.map +1 -0
  15. package/dist/core/cache-stats.js +101 -0
  16. package/dist/core/cache-stats.js.map +1 -0
  17. package/dist/core/compaction/compaction.d.ts +1 -2
  18. package/dist/core/compaction/compaction.d.ts.map +1 -1
  19. package/dist/core/compaction/compaction.js +54 -79
  20. package/dist/core/compaction/compaction.js.map +1 -1
  21. package/dist/core/extensions/builtin/goal/continuation.js +1 -1
  22. package/dist/core/extensions/builtin/goal/continuation.js.map +1 -1
  23. package/dist/core/extensions/builtin/goal/index.d.ts.map +1 -1
  24. package/dist/core/extensions/builtin/goal/index.js +1 -0
  25. package/dist/core/extensions/builtin/goal/index.js.map +1 -1
  26. package/dist/core/extensions/builtin/import-repro.d.ts.map +1 -1
  27. package/dist/core/extensions/builtin/import-repro.js +100 -6
  28. package/dist/core/extensions/builtin/import-repro.js.map +1 -1
  29. package/dist/core/extensions/builtin/mcp/connection.d.ts +1 -0
  30. package/dist/core/extensions/builtin/mcp/connection.d.ts.map +1 -1
  31. package/dist/core/extensions/builtin/mcp/connection.js +15 -0
  32. package/dist/core/extensions/builtin/mcp/connection.js.map +1 -1
  33. package/dist/core/extensions/builtin/mcp/service.d.ts.map +1 -1
  34. package/dist/core/extensions/builtin/mcp/service.js +3 -1
  35. package/dist/core/extensions/builtin/mcp/service.js.map +1 -1
  36. package/dist/core/extensions/index.d.ts +1 -1
  37. package/dist/core/extensions/index.d.ts.map +1 -1
  38. package/dist/core/extensions/index.js.map +1 -1
  39. package/dist/core/extensions/runner.d.ts +4 -3
  40. package/dist/core/extensions/runner.d.ts.map +1 -1
  41. package/dist/core/extensions/runner.js +30 -24
  42. package/dist/core/extensions/runner.js.map +1 -1
  43. package/dist/core/extensions/types.d.ts +23 -3
  44. package/dist/core/extensions/types.d.ts.map +1 -1
  45. package/dist/core/extensions/types.js.map +1 -1
  46. package/dist/core/model-registry.d.ts +3 -0
  47. package/dist/core/model-registry.d.ts.map +1 -1
  48. package/dist/core/model-registry.js +20 -3
  49. package/dist/core/model-registry.js.map +1 -1
  50. package/dist/core/package-manager.d.ts +4 -1
  51. package/dist/core/package-manager.d.ts.map +1 -1
  52. package/dist/core/package-manager.js +68 -26
  53. package/dist/core/package-manager.js.map +1 -1
  54. package/dist/core/resource-loader.d.ts +2 -2
  55. package/dist/core/resource-loader.d.ts.map +1 -1
  56. package/dist/core/resource-loader.js +6 -7
  57. package/dist/core/resource-loader.js.map +1 -1
  58. package/dist/core/sdk.d.ts +1 -1
  59. package/dist/core/sdk.d.ts.map +1 -1
  60. package/dist/core/sdk.js +9 -2
  61. package/dist/core/sdk.js.map +1 -1
  62. package/dist/core/session-manager.d.ts.map +1 -1
  63. package/dist/core/session-manager.js +11 -2
  64. package/dist/core/session-manager.js.map +1 -1
  65. package/dist/core/settings-manager.d.ts +5 -0
  66. package/dist/core/settings-manager.d.ts.map +1 -1
  67. package/dist/core/settings-manager.js +8 -0
  68. package/dist/core/settings-manager.js.map +1 -1
  69. package/dist/core/slash-commands.d.ts +1 -0
  70. package/dist/core/slash-commands.d.ts.map +1 -1
  71. package/dist/core/slash-commands.js +3 -3
  72. package/dist/core/slash-commands.js.map +1 -1
  73. package/dist/index.d.ts +1 -1
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js.map +1 -1
  76. package/dist/main.d.ts +2 -2
  77. package/dist/main.d.ts.map +1 -1
  78. package/dist/main.js +1 -0
  79. package/dist/main.js.map +1 -1
  80. package/dist/modes/interactive/components/config-selector.d.ts +34 -3
  81. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  82. package/dist/modes/interactive/components/config-selector.js +272 -24
  83. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  84. package/dist/modes/interactive/components/footer.d.ts +4 -0
  85. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  86. package/dist/modes/interactive/components/footer.js +5 -2
  87. package/dist/modes/interactive/components/footer.js.map +1 -1
  88. package/dist/modes/interactive/components/oauth-selector.d.ts +3 -1
  89. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  90. package/dist/modes/interactive/components/oauth-selector.js +16 -6
  91. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  92. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  93. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  94. package/dist/modes/interactive/components/settings-selector.js +10 -0
  95. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  96. package/dist/modes/interactive/interactive-mode.d.ts +10 -0
  97. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  98. package/dist/modes/interactive/interactive-mode.js +261 -37
  99. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  100. package/dist/modes/print-mode.js +1 -1
  101. package/dist/modes/print-mode.js.map +1 -1
  102. package/dist/modes/rpc/rpc-client.d.ts +6 -6
  103. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  104. package/dist/modes/rpc/rpc-client.js +3 -3
  105. package/dist/modes/rpc/rpc-client.js.map +1 -1
  106. package/dist/package-manager-cli.d.ts +2 -2
  107. package/dist/package-manager-cli.d.ts.map +1 -1
  108. package/dist/package-manager-cli.js +64 -17
  109. package/dist/package-manager-cli.js.map +1 -1
  110. package/dist/utils/clipboard-image.js +1 -1
  111. package/dist/utils/clipboard-image.js.map +1 -1
  112. package/docs/extensions.md +33 -9
  113. package/docs/models.md +4 -4
  114. package/docs/packages.md +7 -10
  115. package/docs/rpc.md +13 -3
  116. package/docs/sdk.md +22 -0
  117. package/docs/settings.md +1 -0
  118. package/docs/usage.md +1 -1
  119. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  120. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  121. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  122. package/examples/extensions/gondolin/package-lock.json +2 -2
  123. package/examples/extensions/gondolin/package.json +1 -1
  124. package/examples/extensions/reload-runtime.ts +2 -2
  125. package/examples/extensions/sandbox/package-lock.json +2 -2
  126. package/examples/extensions/sandbox/package.json +1 -1
  127. package/examples/extensions/with-deps/package-lock.json +2 -2
  128. package/examples/extensions/with-deps/package.json +1 -1
  129. package/examples/sdk/README.md +1 -1
  130. package/node_modules/@code-yeongyu/senpi-codemode/package.json +3 -3
  131. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  132. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +37 -2
  133. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js.map +1 -1
  134. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-repo.d.ts.map +1 -1
  135. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-repo.js +2 -0
  136. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-repo.js.map +1 -1
  137. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-storage.d.ts +1 -0
  138. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-storage.d.ts.map +1 -1
  139. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-storage.js +7 -0
  140. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-storage.js.map +1 -1
  141. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.d.ts +18 -4
  142. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.d.ts.map +1 -1
  143. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js +72 -37
  144. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js.map +1 -1
  145. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.d.ts +2 -0
  146. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.d.ts.map +1 -1
  147. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.js.map +1 -1
  148. package/node_modules/@earendil-works/pi-agent-core/dist/index.d.ts +2 -0
  149. package/node_modules/@earendil-works/pi-agent-core/dist/index.d.ts.map +1 -1
  150. package/node_modules/@earendil-works/pi-agent-core/dist/index.js +2 -0
  151. package/node_modules/@earendil-works/pi-agent-core/dist/index.js.map +1 -1
  152. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  153. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts.map +1 -1
  154. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +2 -1
  155. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
  156. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js +1 -1
  157. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js.map +1 -1
  158. package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.d.ts.map +1 -1
  159. package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js +4 -1
  160. package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js.map +1 -1
  161. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +446 -195
  162. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
  163. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.d.ts +84 -0
  164. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.d.ts.map +1 -1
  165. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.js +72 -0
  166. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.js.map +1 -1
  167. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.js +4 -4
  168. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.js.map +1 -1
  169. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.d.ts +63 -0
  170. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.d.ts.map +1 -1
  171. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.js +54 -0
  172. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.js.map +1 -1
  173. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.d.ts +84 -0
  174. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.d.ts.map +1 -1
  175. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.js +72 -0
  176. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.js.map +1 -1
  177. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts +138 -63
  178. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts.map +1 -1
  179. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js +136 -76
  180. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js.map +1 -1
  181. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.d.ts +77 -0
  182. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.d.ts.map +1 -1
  183. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js +72 -1
  184. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js.map +1 -1
  185. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-ams.models.d.ts +0 -44
  186. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-ams.models.d.ts.map +1 -1
  187. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-ams.models.js +9 -45
  188. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-ams.models.js.map +1 -1
  189. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-cn.models.d.ts +0 -44
  190. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-cn.models.d.ts.map +1 -1
  191. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-cn.models.js +9 -45
  192. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-cn.models.js.map +1 -1
  193. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-sgp.models.d.ts +0 -44
  194. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-sgp.models.d.ts.map +1 -1
  195. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-sgp.models.js +9 -45
  196. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-sgp.models.js.map +1 -1
  197. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts.map +1 -1
  198. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +3 -0
  199. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js.map +1 -1
  200. package/node_modules/@earendil-works/pi-ai/package.json +1 -1
  201. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  202. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts.map +1 -1
  203. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +22 -1
  204. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js.map +1 -1
  205. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  206. package/package.json +7 -8
  207. package/npm-shrinkwrap.json +0 -3479
@@ -89,6 +89,7 @@ export class AgentSession {
89
89
  constructor(config) {
90
90
  this._eventListeners = [];
91
91
  this._agentEventQueue = Promise.resolve();
92
+ this._isAgentRunActive = false;
92
93
  /** Tracks pending steering messages for UI display. Removed when delivered. */
93
94
  this._steeringMessages = [];
94
95
  /** Tracks pending follow-up messages for UI display. Removed when delivered. */
@@ -217,11 +218,11 @@ export class AgentSession {
217
218
  };
218
219
  }
219
220
  async _emitBeforeToolCallHooks(toolCall, args) {
221
+ await this._agentEventQueue;
220
222
  const runner = this._extensionRunner;
221
223
  if (!runner.hasHandlers("tool_call")) {
222
224
  return undefined;
223
225
  }
224
- await this._agentEventQueue;
225
226
  try {
226
227
  return await runner.emitToolCall({
227
228
  type: "tool_call",
@@ -323,6 +324,47 @@ export class AgentSession {
323
324
  this.abortBranchSummary();
324
325
  this._incrementMessageRevision();
325
326
  }
327
+ _getIdleWaitPromise() {
328
+ if (!this._idleWaitPromise) {
329
+ this._idleWaitPromise = new Promise((resolve) => {
330
+ this._resolveIdleWait = resolve;
331
+ });
332
+ }
333
+ return this._idleWaitPromise;
334
+ }
335
+ _resolveIdleWaitIfIdle() {
336
+ if (this._isAgentRunActive || !this._resolveIdleWait) {
337
+ return;
338
+ }
339
+ const resolve = this._resolveIdleWait;
340
+ this._idleWaitPromise = undefined;
341
+ this._resolveIdleWait = undefined;
342
+ resolve();
343
+ }
344
+ async _emitAgentSettled() {
345
+ if (!this._isAgentRunActive) {
346
+ this._resolveIdleWaitIfIdle();
347
+ return;
348
+ }
349
+ this._isAgentRunActive = false;
350
+ try {
351
+ await this._extensionRunner.emit({ type: "agent_settled" });
352
+ this._emit({ type: "agent_settled" });
353
+ }
354
+ finally {
355
+ this._resolveIdleWaitIfIdle();
356
+ }
357
+ }
358
+ async _promptAgent(messages) {
359
+ this._isAgentRunActive = true;
360
+ try {
361
+ await this.agent.prompt(messages);
362
+ }
363
+ catch (error) {
364
+ await this._emitAgentSettled();
365
+ throw error;
366
+ }
367
+ }
326
368
  _createRetryPromiseForAgentEnd(event) {
327
369
  if (event.type !== "agent_end" || this._retryPromise) {
328
370
  return;
@@ -348,6 +390,34 @@ export class AgentSession {
348
390
  }
349
391
  return undefined;
350
392
  }
393
+ _agentEndAllowsQueuedContinuation(messages) {
394
+ let lastAssistantIndex = -1;
395
+ for (let index = messages.length - 1; index >= 0; index--) {
396
+ if (messages[index]?.role === "assistant") {
397
+ lastAssistantIndex = index;
398
+ break;
399
+ }
400
+ }
401
+ if (lastAssistantIndex === -1) {
402
+ return false;
403
+ }
404
+ const lastAssistant = messages[lastAssistantIndex];
405
+ if (lastAssistant?.role !== "assistant") {
406
+ return false;
407
+ }
408
+ if (lastAssistant.stopReason === "aborted" || lastAssistant.stopReason === "error") {
409
+ return false;
410
+ }
411
+ for (let index = lastAssistantIndex + 1; index < messages.length; index++) {
412
+ const message = messages[index];
413
+ if (message?.role === "toolResult" &&
414
+ message.isError &&
415
+ message.content.some((content) => content.type === "text" && /\babort(?:ed)?\b/i.test(content.text))) {
416
+ return false;
417
+ }
418
+ }
419
+ return true;
420
+ }
351
421
  _willRetryAfterAgentEnd(messages) {
352
422
  const settings = this.settingsManager.getRetrySettings();
353
423
  if (!settings.enabled) {
@@ -430,6 +500,8 @@ export class AgentSession {
430
500
  }
431
501
  }
432
502
  // Check auto-retry and auto-compaction after agent completes
503
+ let launchedContinuation = false;
504
+ const allowsQueuedContinuation = event.type === "agent_end" ? this._agentEndAllowsQueuedContinuation(event.messages) : false;
433
505
  if (event.type === "agent_end" && this._lastAssistantMessage) {
434
506
  const msg = this._lastAssistantMessage;
435
507
  this._lastAssistantMessage = undefined;
@@ -440,10 +512,16 @@ export class AgentSession {
440
512
  return; // Retry was initiated, don't proceed to compaction
441
513
  }
442
514
  this._resolveRetry();
443
- await this._checkCompaction(msg);
515
+ launchedContinuation = await this._checkCompaction(msg);
444
516
  }
445
517
  if (event.type === "agent_end") {
446
518
  this._flushPendingBashMessages();
519
+ if (!launchedContinuation && allowsQueuedContinuation && this.agent.hasQueuedMessages()) {
520
+ launchedContinuation = await this._continueAgentAfterCurrentRun();
521
+ }
522
+ if (!launchedContinuation) {
523
+ await this._emitAgentSettled();
524
+ }
447
525
  }
448
526
  }
449
527
  /** Resolve the pending retry promise */
@@ -537,7 +615,16 @@ export class AgentSession {
537
615
  };
538
616
  const replacement = await this._extensionRunner.emitMessageEnd(extensionEvent);
539
617
  if (replacement) {
540
- this._replaceMessageInPlace(event.message, replacement);
618
+ // Untyped extension handlers can return messages with null/missing content;
619
+ // normalize so it never enters agent state or session history.
620
+ const normalized = (replacement.role === "user" ||
621
+ replacement.role === "assistant" ||
622
+ replacement.role === "toolResult" ||
623
+ replacement.role === "custom") &&
624
+ replacement.content == null
625
+ ? { ...replacement, content: [] }
626
+ : replacement;
627
+ this._replaceMessageInPlace(event.message, normalized);
541
628
  }
542
629
  }
543
630
  else if (event.type === "tool_execution_start") {
@@ -644,9 +731,13 @@ export class AgentSession {
644
731
  get serviceTier() {
645
732
  return this._currentServiceTier;
646
733
  }
647
- /** Whether agent is currently streaming a response */
734
+ /** Whether the session is currently processing an agent run or post-run continuation. */
648
735
  get isStreaming() {
649
- return this.agent.state.isStreaming;
736
+ return this._isAgentRunActive;
737
+ }
738
+ /** Whether the session has no active agent run, retry, auto-compaction, or queued continuation. */
739
+ get isIdle() {
740
+ return !this._isAgentRunActive;
650
741
  }
651
742
  /** Current effective system prompt (includes any per-turn extension modifications) */
652
743
  get systemPrompt() {
@@ -987,7 +1078,8 @@ export class AgentSession {
987
1078
  messages.push({
988
1079
  role: "custom",
989
1080
  customType: msg.customType,
990
- content: msg.content,
1081
+ // Untyped extensions can pass null/missing content; normalize at ingestion.
1082
+ content: msg.content ?? [],
991
1083
  display: msg.display,
992
1084
  details: msg.details,
993
1085
  timestamp: Date.now(),
@@ -1013,8 +1105,9 @@ export class AgentSession {
1013
1105
  return;
1014
1106
  }
1015
1107
  preflightResult?.(true);
1016
- await this.agent.prompt(messages);
1108
+ await this._promptAgent(messages);
1017
1109
  await this.waitForRetry();
1110
+ await this.waitForIdle();
1018
1111
  if (shouldWaitForSessionWork) {
1019
1112
  await this._waitForSettledSessionWork();
1020
1113
  }
@@ -1244,7 +1337,8 @@ export class AgentSession {
1244
1337
  const appMessage = {
1245
1338
  role: "custom",
1246
1339
  customType: message.customType,
1247
- content: message.content,
1340
+ // Untyped extensions can pass null/missing content; normalize at ingestion.
1341
+ content: message.content ?? [],
1248
1342
  display: message.display,
1249
1343
  details: message.details,
1250
1344
  timestamp: Date.now(),
@@ -1261,7 +1355,7 @@ export class AgentSession {
1261
1355
  }
1262
1356
  }
1263
1357
  else if (options?.triggerTurn) {
1264
- await this.agent.prompt(appMessage);
1358
+ await this._promptAgent(appMessage);
1265
1359
  }
1266
1360
  else {
1267
1361
  this.agent.state.messages.push(appMessage);
@@ -1352,7 +1446,7 @@ export class AgentSession {
1352
1446
  }
1353
1447
  const abortPromise = (async () => {
1354
1448
  this.agent.abort();
1355
- await this.agent.waitForIdle();
1449
+ await this.waitForIdle();
1356
1450
  })();
1357
1451
  this._userAbortPromise = abortPromise;
1358
1452
  try {
@@ -1364,6 +1458,12 @@ export class AgentSession {
1364
1458
  }
1365
1459
  }
1366
1460
  }
1461
+ async waitForIdle() {
1462
+ if (this.isIdle) {
1463
+ return;
1464
+ }
1465
+ await this._getIdleWaitPromise();
1466
+ }
1367
1467
  // =========================================================================
1368
1468
  // Model Management
1369
1469
  // =========================================================================
@@ -1848,10 +1948,10 @@ export class AgentSession {
1848
1948
  async _checkCompaction(assistantMessage, skipAbortedCheck = true, requestReason) {
1849
1949
  const settings = this.settingsManager.getCompactionSettings();
1850
1950
  if (!settings.enabled)
1851
- return;
1951
+ return false;
1852
1952
  // Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
1853
1953
  if (skipAbortedCheck && assistantMessage.stopReason === "aborted")
1854
- return;
1954
+ return false;
1855
1955
  const contextWindow = this.model?.contextWindow ?? 0;
1856
1956
  // Skip overflow check if the message came from a different model.
1857
1957
  // This handles the case where user switched from a smaller-context model (e.g. opus)
@@ -1865,7 +1965,7 @@ export class AgentSession {
1865
1965
  const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
1866
1966
  const assistantIsFromBeforeCompaction = compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
1867
1967
  if (assistantIsFromBeforeCompaction) {
1868
- return;
1968
+ return false;
1869
1969
  }
1870
1970
  // Case 1: Overflow - LLM returned context overflow error.
1871
1971
  // If the saved assistant provider differs from the currently selected provider alias,
@@ -1877,8 +1977,7 @@ export class AgentSession {
1877
1977
  if (isContextOverflow(assistantMessage, contextWindow) && (sameModel || currentContextNeedsCompaction)) {
1878
1978
  const willRetry = assistantMessage.stopReason !== "stop";
1879
1979
  if (!willRetry) {
1880
- await this._runAutoCompaction("overflow", false);
1881
- return;
1980
+ return await this._runAutoCompaction("overflow", false);
1882
1981
  }
1883
1982
  if (this._overflowRecoveryAttempted) {
1884
1983
  const errorMessage = "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.";
@@ -1893,7 +1992,7 @@ export class AgentSession {
1893
1992
  if (requestReason === "pre_prompt") {
1894
1993
  throw new Error(errorMessage);
1895
1994
  }
1896
- return;
1995
+ return false;
1897
1996
  }
1898
1997
  this._overflowRecoveryAttempted = true;
1899
1998
  // Remove the error message from agent state (it IS saved to session for history,
@@ -1907,9 +2006,9 @@ export class AgentSession {
1907
2006
  await this._runPrePromptCompaction(assistantMessage, skipAbortedCheck, "overflow", willRetry);
1908
2007
  }
1909
2008
  else {
1910
- await this._runAutoCompaction("overflow", willRetry);
2009
+ return await this._runAutoCompaction("overflow", willRetry);
1911
2010
  }
1912
- return;
2011
+ return false;
1913
2012
  }
1914
2013
  // Case 2: Threshold - context is getting large
1915
2014
  // For error messages or all-zero usage messages, estimate from the last valid response.
@@ -1921,7 +2020,7 @@ export class AgentSession {
1921
2020
  const messages = filterContextExcludedMessages(this.agent.state.messages);
1922
2021
  const estimate = estimateContextTokens(messages);
1923
2022
  if (estimate.lastUsageIndex === null)
1924
- return; // No usage data at all
2023
+ return false; // No usage data at all
1925
2024
  // Verify the usage source is post-compaction. Kept pre-compaction messages
1926
2025
  // have stale usage reflecting the old (larger) context and would falsely
1927
2026
  // trigger compaction right after one just finished.
@@ -1929,7 +2028,7 @@ export class AgentSession {
1929
2028
  if (compactionEntry &&
1930
2029
  usageMsg.role === "assistant" &&
1931
2030
  usageMsg.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
1932
- return;
2031
+ return false;
1933
2032
  }
1934
2033
  contextTokens = estimate.tokens;
1935
2034
  }
@@ -1941,9 +2040,10 @@ export class AgentSession {
1941
2040
  await this._runPrePromptCompaction(assistantMessage, skipAbortedCheck);
1942
2041
  }
1943
2042
  else {
1944
- await this._runAutoCompaction("threshold", false);
2043
+ return await this._runAutoCompaction("threshold", false);
1945
2044
  }
1946
2045
  }
2046
+ return false;
1947
2047
  }
1948
2048
  async _runPrePromptCompaction(lastAssistantMessage, skipAbortedCheck, reason = "pre_prompt", willRetry = false) {
1949
2049
  this._emit({ type: "compaction_start", reason });
@@ -2256,7 +2356,7 @@ export class AgentSession {
2256
2356
  }, {
2257
2357
  getModel: () => this.model,
2258
2358
  getServiceTier: () => this.serviceTier,
2259
- isIdle: () => !this.isStreaming,
2359
+ isIdle: () => this.isIdle,
2260
2360
  isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
2261
2361
  getSignal: () => this.agent.signal,
2262
2362
  abort: () => {
@@ -2593,6 +2693,7 @@ export class AgentSession {
2593
2693
  const attempt = this._retryAttempt;
2594
2694
  this._retryAttempt = 0;
2595
2695
  this._retryAbortController = undefined;
2696
+ await this._emitAgentSettled();
2596
2697
  this._emit({
2597
2698
  type: "auto_retry_end",
2598
2699
  success: false,
@@ -2949,28 +3050,44 @@ export class AgentSession {
2949
3050
  return "";
2950
3051
  }
2951
3052
  /**
2952
- * Get session statistics.
3053
+ * Get session statistics. Aggregates over ALL session entries (including
3054
+ * history that was compacted away), so token/cost totals reflect what was
3055
+ * actually billed across the session.
2953
3056
  */
2954
3057
  getSessionStats() {
2955
- const state = this.state;
2956
- const userMessages = state.messages.filter((m) => m.role === "user").length;
2957
- const assistantMessages = state.messages.filter((m) => m.role === "assistant").length;
2958
- const toolResults = state.messages.filter((m) => m.role === "toolResult").length;
3058
+ let userMessages = 0;
3059
+ let assistantMessages = 0;
3060
+ let toolResults = 0;
3061
+ let totalMessages = 0;
2959
3062
  let toolCalls = 0;
2960
3063
  let totalInput = 0;
2961
3064
  let totalOutput = 0;
2962
3065
  let totalCacheRead = 0;
2963
3066
  let totalCacheWrite = 0;
2964
3067
  let totalCost = 0;
2965
- for (const message of state.messages) {
2966
- if (message.role === "assistant") {
3068
+ for (const entry of this.sessionManager.getEntries()) {
3069
+ if (entry.type !== "message")
3070
+ continue;
3071
+ totalMessages++;
3072
+ const message = entry.message;
3073
+ if (message.role === "user") {
3074
+ userMessages++;
3075
+ }
3076
+ else if (message.role === "toolResult") {
3077
+ toolResults++;
3078
+ }
3079
+ else if (message.role === "assistant") {
3080
+ assistantMessages++;
2967
3081
  const assistantMsg = message;
2968
- toolCalls += assistantMsg.content.filter((c) => c.type === "toolCall").length;
2969
- totalInput += assistantMsg.usage.input;
2970
- totalOutput += assistantMsg.usage.output;
2971
- totalCacheRead += assistantMsg.usage.cacheRead;
2972
- totalCacheWrite += assistantMsg.usage.cacheWrite;
2973
- totalCost += assistantMsg.usage.cost.total;
3082
+ if (Array.isArray(assistantMsg.content)) {
3083
+ toolCalls += assistantMsg.content.filter((c) => c.type === "toolCall").length;
3084
+ }
3085
+ const usage = assistantMsg.usage;
3086
+ totalInput += usage.input;
3087
+ totalOutput += usage.output;
3088
+ totalCacheRead += usage.cacheRead;
3089
+ totalCacheWrite += usage.cacheWrite;
3090
+ totalCost += usage.cost.total;
2974
3091
  }
2975
3092
  }
2976
3093
  return {
@@ -2980,7 +3097,7 @@ export class AgentSession {
2980
3097
  assistantMessages,
2981
3098
  toolCalls,
2982
3099
  toolResults,
2983
- totalMessages: state.messages.length,
3100
+ totalMessages,
2984
3101
  tokens: {
2985
3102
  input: totalInput,
2986
3103
  output: totalOutput,