@oh-my-pi/pi-coding-agent 15.9.67 → 15.10.1

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 (266) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  4. package/dist/types/cli/gallery-cli.d.ts +43 -0
  5. package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
  6. package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
  7. package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
  8. package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
  9. package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
  10. package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
  11. package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
  12. package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
  13. package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
  14. package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
  15. package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
  16. package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
  17. package/dist/types/cli/gallery-screenshot.d.ts +35 -0
  18. package/dist/types/commands/gallery.d.ts +47 -0
  19. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  20. package/dist/types/commit/analysis/summary.d.ts +2 -2
  21. package/dist/types/commit/changelog/generate.d.ts +2 -2
  22. package/dist/types/commit/changelog/index.d.ts +2 -2
  23. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  24. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  25. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  26. package/dist/types/commit/model-selection.d.ts +10 -4
  27. package/dist/types/config/api-key-resolver.d.ts +34 -0
  28. package/dist/types/config/keybindings.d.ts +6 -1
  29. package/dist/types/config/model-id-affixes.d.ts +2 -0
  30. package/dist/types/config/model-registry.d.ts +25 -2
  31. package/dist/types/config/settings-schema.d.ts +41 -6
  32. package/dist/types/dap/config.d.ts +14 -1
  33. package/dist/types/dap/types.d.ts +10 -0
  34. package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
  35. package/dist/types/lsp/types.d.ts +10 -0
  36. package/dist/types/lsp/utils.d.ts +3 -2
  37. package/dist/types/main.d.ts +3 -2
  38. package/dist/types/memory-backend/index.d.ts +2 -1
  39. package/dist/types/memory-backend/resolve.d.ts +1 -1
  40. package/dist/types/memory-backend/types.d.ts +1 -1
  41. package/dist/types/modes/components/chat-block.d.ts +64 -0
  42. package/dist/types/modes/components/custom-editor.d.ts +5 -1
  43. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  44. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  45. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  46. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  47. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  48. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  49. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  50. package/dist/types/modes/controllers/event-controller.d.ts +0 -1
  51. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  52. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  53. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  54. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  55. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  56. package/dist/types/modes/index.d.ts +5 -4
  57. package/dist/types/modes/interactive-mode.d.ts +16 -6
  58. package/dist/types/modes/setup-version.d.ts +11 -0
  59. package/dist/types/modes/setup-wizard/index.d.ts +2 -1
  60. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
  61. package/dist/types/modes/theme/theme.d.ts +1 -1
  62. package/dist/types/modes/types.d.ts +19 -6
  63. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  64. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  65. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  66. package/dist/types/sdk.d.ts +3 -1
  67. package/dist/types/session/agent-session.d.ts +21 -0
  68. package/dist/types/session/messages.d.ts +12 -0
  69. package/dist/types/session/session-manager.d.ts +3 -1
  70. package/dist/types/slash-commands/types.d.ts +4 -6
  71. package/dist/types/task/executor.d.ts +14 -0
  72. package/dist/types/task/index.d.ts +1 -0
  73. package/dist/types/task/render.d.ts +3 -2
  74. package/dist/types/telemetry-export.d.ts +1 -1
  75. package/dist/types/tools/archive-reader.d.ts +5 -0
  76. package/dist/types/tools/ast-edit.d.ts +3 -0
  77. package/dist/types/tools/ast-grep.d.ts +3 -0
  78. package/dist/types/tools/bash.d.ts +1 -0
  79. package/dist/types/tools/eval-render.d.ts +1 -8
  80. package/dist/types/tools/fetch.d.ts +15 -7
  81. package/dist/types/tools/find.d.ts +8 -4
  82. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  83. package/dist/types/tools/memory-render.d.ts +4 -1
  84. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  85. package/dist/types/tools/render-utils.d.ts +13 -9
  86. package/dist/types/tools/renderers.d.ts +16 -2
  87. package/dist/types/tools/search.d.ts +5 -1
  88. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  89. package/dist/types/tools/todo.d.ts +3 -2
  90. package/dist/types/tools/write.d.ts +5 -0
  91. package/dist/types/tui/output-block.d.ts +16 -4
  92. package/dist/types/tui/status-line.d.ts +3 -0
  93. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  94. package/dist/types/web/scrapers/github.d.ts +22 -0
  95. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  96. package/dist/types/web/search/providers/perplexity.d.ts +8 -1
  97. package/dist/types/web/search/types.d.ts +1 -1
  98. package/package.json +9 -9
  99. package/scripts/dev-launch +42 -0
  100. package/scripts/dev-launch-preload.ts +19 -0
  101. package/src/auto-thinking/classifier.ts +5 -1
  102. package/src/cli/args.ts +2 -2
  103. package/src/cli/dry-balance-cli.ts +52 -17
  104. package/src/cli/gallery-cli.ts +226 -0
  105. package/src/cli/gallery-fixtures/agentic.ts +292 -0
  106. package/src/cli/gallery-fixtures/codeintel.ts +188 -0
  107. package/src/cli/gallery-fixtures/edit.ts +194 -0
  108. package/src/cli/gallery-fixtures/fs.ts +153 -0
  109. package/src/cli/gallery-fixtures/index.ts +40 -0
  110. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  111. package/src/cli/gallery-fixtures/memory.ts +81 -0
  112. package/src/cli/gallery-fixtures/misc.ts +250 -0
  113. package/src/cli/gallery-fixtures/search.ts +213 -0
  114. package/src/cli/gallery-fixtures/shell.ts +167 -0
  115. package/src/cli/gallery-fixtures/types.ts +41 -0
  116. package/src/cli/gallery-fixtures/web.ts +158 -0
  117. package/src/cli/gallery-screenshot.ts +279 -0
  118. package/src/cli-commands.ts +1 -0
  119. package/src/commands/gallery.ts +52 -0
  120. package/src/commands/launch.ts +1 -1
  121. package/src/commit/analysis/conventional.ts +2 -2
  122. package/src/commit/analysis/summary.ts +2 -2
  123. package/src/commit/changelog/generate.ts +2 -2
  124. package/src/commit/changelog/index.ts +2 -2
  125. package/src/commit/map-reduce/index.ts +3 -3
  126. package/src/commit/map-reduce/map-phase.ts +2 -2
  127. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  128. package/src/commit/model-selection.ts +33 -9
  129. package/src/commit/pipeline.ts +4 -4
  130. package/src/config/api-key-resolver.ts +58 -0
  131. package/src/config/keybindings.ts +15 -6
  132. package/src/config/model-equivalence.ts +35 -12
  133. package/src/config/model-id-affixes.ts +39 -22
  134. package/src/config/model-registry.ts +41 -18
  135. package/src/config/settings-schema.ts +28 -5
  136. package/src/config/settings.ts +31 -2
  137. package/src/dap/client.ts +14 -16
  138. package/src/dap/config.ts +41 -2
  139. package/src/dap/defaults.json +1 -0
  140. package/src/dap/session.ts +1 -0
  141. package/src/dap/types.ts +10 -0
  142. package/src/debug/index.ts +40 -54
  143. package/src/edit/renderer.ts +111 -119
  144. package/src/eval/__tests__/agent-bridge.test.ts +75 -32
  145. package/src/eval/__tests__/llm-bridge.test.ts +90 -31
  146. package/src/eval/agent-bridge.ts +34 -7
  147. package/src/eval/llm-bridge.ts +8 -3
  148. package/src/extensibility/extensions/runner.ts +1 -0
  149. package/src/extensibility/plugins/doctor.ts +0 -1
  150. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  151. package/src/goals/tools/goal-tool.ts +37 -27
  152. package/src/internal-urls/docs-index.generated.ts +10 -10
  153. package/src/lsp/client.ts +104 -55
  154. package/src/lsp/types.ts +10 -0
  155. package/src/lsp/utils.ts +3 -2
  156. package/src/main.ts +53 -56
  157. package/src/memories/index.ts +12 -5
  158. package/src/memory-backend/index.ts +13 -1
  159. package/src/memory-backend/resolve.ts +3 -5
  160. package/src/memory-backend/types.ts +1 -1
  161. package/src/mnemopi/backend.ts +5 -1
  162. package/src/modes/acp/acp-agent.ts +33 -26
  163. package/src/modes/components/assistant-message.ts +2 -9
  164. package/src/modes/components/chat-block.ts +111 -0
  165. package/src/modes/components/copy-selector.ts +1 -44
  166. package/src/modes/components/custom-editor.ts +33 -1
  167. package/src/modes/components/custom-message.ts +1 -3
  168. package/src/modes/components/execution-shared.ts +1 -2
  169. package/src/modes/components/hook-message.ts +1 -3
  170. package/src/modes/components/overlay-box.ts +108 -0
  171. package/src/modes/components/plan-review-overlay.ts +799 -0
  172. package/src/modes/components/plan-toc.ts +138 -0
  173. package/src/modes/components/read-tool-group.ts +20 -4
  174. package/src/modes/components/skill-message.ts +0 -1
  175. package/src/modes/components/status-line.ts +3 -5
  176. package/src/modes/components/tips.txt +1 -0
  177. package/src/modes/components/todo-reminder.ts +0 -2
  178. package/src/modes/components/tool-execution.ts +115 -90
  179. package/src/modes/components/transcript-container.ts +84 -24
  180. package/src/modes/components/user-message.ts +1 -2
  181. package/src/modes/controllers/command-controller-shared.ts +7 -6
  182. package/src/modes/controllers/command-controller.ts +70 -57
  183. package/src/modes/controllers/event-controller.ts +41 -40
  184. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  185. package/src/modes/controllers/input-controller.ts +135 -122
  186. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  187. package/src/modes/controllers/selector-controller.ts +25 -27
  188. package/src/modes/controllers/streaming-reveal.ts +212 -0
  189. package/src/modes/controllers/tan-command-controller.ts +173 -0
  190. package/src/modes/index.ts +5 -4
  191. package/src/modes/interactive-mode.ts +171 -82
  192. package/src/modes/setup-version.ts +11 -0
  193. package/src/modes/setup-wizard/index.ts +3 -2
  194. package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
  195. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  196. package/src/modes/theme/theme-schema.json +1 -1
  197. package/src/modes/theme/theme.ts +8 -4
  198. package/src/modes/types.ts +19 -8
  199. package/src/modes/utils/context-usage.ts +10 -6
  200. package/src/modes/utils/copy-targets.ts +133 -27
  201. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  202. package/src/modes/utils/ui-helpers.ts +44 -46
  203. package/src/plan-mode/approved-plan.ts +66 -43
  204. package/src/plan-mode/plan-protection.ts +4 -4
  205. package/src/prompts/system/background-tan-dispatch.md +8 -0
  206. package/src/prompts/system/plan-mode-active.md +67 -58
  207. package/src/prompts/system/plan-mode-approved.md +1 -1
  208. package/src/sdk.ts +32 -60
  209. package/src/session/agent-session.ts +89 -13
  210. package/src/session/messages.ts +26 -0
  211. package/src/session/session-manager.ts +13 -5
  212. package/src/slash-commands/builtin-registry.ts +37 -10
  213. package/src/slash-commands/helpers/usage-report.ts +2 -0
  214. package/src/slash-commands/types.ts +4 -6
  215. package/src/task/executor.ts +25 -4
  216. package/src/task/index.ts +4 -0
  217. package/src/task/render.ts +212 -148
  218. package/src/telemetry-export.ts +25 -7
  219. package/src/tools/archive-reader.ts +64 -0
  220. package/src/tools/ask.ts +119 -164
  221. package/src/tools/ast-edit.ts +98 -71
  222. package/src/tools/ast-grep.ts +37 -43
  223. package/src/tools/bash.ts +50 -6
  224. package/src/tools/debug.ts +20 -8
  225. package/src/tools/eval-backends.ts +6 -17
  226. package/src/tools/eval-render.ts +21 -18
  227. package/src/tools/eval.ts +5 -4
  228. package/src/tools/fetch.ts +391 -91
  229. package/src/tools/find.ts +44 -30
  230. package/src/tools/gh-renderer.ts +81 -42
  231. package/src/tools/grouped-file-output.ts +272 -48
  232. package/src/tools/image-gen.ts +150 -103
  233. package/src/tools/inspect-image-renderer.ts +63 -41
  234. package/src/tools/inspect-image.ts +8 -1
  235. package/src/tools/job.ts +3 -4
  236. package/src/tools/memory-render.ts +4 -1
  237. package/src/tools/plan-mode-guard.ts +21 -39
  238. package/src/tools/read.ts +23 -16
  239. package/src/tools/render-utils.ts +38 -40
  240. package/src/tools/renderers.ts +16 -1
  241. package/src/tools/report-tool-issue.ts +1 -1
  242. package/src/tools/resolve.ts +14 -0
  243. package/src/tools/search-tool-bm25.ts +36 -23
  244. package/src/tools/search.ts +189 -95
  245. package/src/tools/sqlite-reader.ts +9 -12
  246. package/src/tools/todo.ts +138 -59
  247. package/src/tools/write.ts +100 -60
  248. package/src/tui/output-block.ts +60 -13
  249. package/src/tui/status-line.ts +5 -1
  250. package/src/utils/commit-message-generator.ts +9 -1
  251. package/src/utils/enhanced-paste.ts +202 -0
  252. package/src/utils/title-generator.ts +2 -1
  253. package/src/web/scrapers/github.ts +255 -3
  254. package/src/web/scrapers/youtube.ts +3 -2
  255. package/src/web/search/providers/anthropic.ts +25 -19
  256. package/src/web/search/providers/exa.ts +11 -3
  257. package/src/web/search/providers/kimi.ts +28 -17
  258. package/src/web/search/providers/parallel.ts +35 -24
  259. package/src/web/search/providers/perplexity.ts +199 -51
  260. package/src/web/search/providers/synthetic.ts +8 -6
  261. package/src/web/search/providers/tavily.ts +9 -8
  262. package/src/web/search/providers/zai.ts +8 -6
  263. package/src/web/search/render.ts +39 -54
  264. package/src/web/search/types.ts +5 -1
  265. package/dist/types/eval/__tests__/shared-executors.test.d.ts +0 -1
  266. package/src/eval/__tests__/shared-executors.test.ts +0 -609
@@ -128,7 +128,6 @@ import {
128
128
  } from "../eval/py/executor";
129
129
  import { defaultEvalSessionId } from "../eval/session-id";
130
130
  import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
131
- import { exportSessionToHtml } from "../export/html";
132
131
  import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
133
132
  import type { LoadedCustomCommand } from "../extensibility/custom-commands";
134
133
  import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
@@ -472,6 +471,12 @@ export interface SessionStats {
472
471
  cost: number;
473
472
  }
474
473
 
474
+ export interface FreshSessionResult {
475
+ previousSessionId: string;
476
+ sessionId: string;
477
+ closedProviderSessions: number;
478
+ }
479
+
475
480
  /** Internal marker for hook messages queued through the agent loop */
476
481
  // ============================================================================
477
482
  // Constants
@@ -923,6 +928,7 @@ export class AgentSession {
923
928
  #agentId: string | undefined;
924
929
  #agentRegistry: AgentRegistry | undefined;
925
930
  #providerSessionId: string | undefined;
931
+ #freshProviderSessionId: string | undefined;
926
932
  #isDisposed = false;
927
933
  // Extension system
928
934
  #extensionRunner: ExtensionRunner | undefined = undefined;
@@ -1276,6 +1282,14 @@ export class AgentSession {
1276
1282
  return this.#modelRegistry;
1277
1283
  }
1278
1284
 
1285
+ get asyncJobManager(): AsyncJobManager | undefined {
1286
+ return this.#asyncJobManager;
1287
+ }
1288
+
1289
+ getAgentId(): string | undefined {
1290
+ return this.#agentId;
1291
+ }
1292
+
1279
1293
  /** Advance the tool-choice queue and return the next directive for the upcoming LLM call. */
1280
1294
  nextToolChoice(): ToolChoice | undefined {
1281
1295
  return this.#toolChoiceQueue.nextToolChoice();
@@ -1682,7 +1696,7 @@ export class AgentSession {
1682
1696
  // Abort the stream immediately — do not gate on extension callbacks
1683
1697
  this.#ttsrAbortPending = true;
1684
1698
  this.#ensureTtsrResumePromise();
1685
- this.agent.abort();
1699
+ this.agent.abort(this.#formatTtsrAbortReason(matches));
1686
1700
  // Notify extensions (fire-and-forget, does not block abort)
1687
1701
  this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
1688
1702
  // Schedule retry after a short delay
@@ -2163,6 +2177,12 @@ export class AgentSession {
2163
2177
  }
2164
2178
  }
2165
2179
 
2180
+ #formatTtsrAbortReason(rules: Rule[]): string {
2181
+ const label = rules.length === 1 ? "rule" : "rules";
2182
+ const ruleNames = rules.map(rule => rule.name).join(", ");
2183
+ return `TTSR matched ${label}: ${ruleNames}`;
2184
+ }
2185
+
2166
2186
  /** Get TTSR injection payload and clear pending injections. */
2167
2187
  #getTtsrInjectionContent(): { content: string; rules: Rule[] } | undefined {
2168
2188
  if (this.#pendingTtsrInjections.length === 0) return undefined;
@@ -2186,13 +2206,20 @@ export class AgentSession {
2186
2206
  * project, `~`-relative when it lives under home, else the raw path.
2187
2207
  */
2188
2208
  #displayRulePath(rulePath: string): string {
2189
- const cwdRel = relativePathWithinRoot(this.sessionManager.getCwd(), rulePath);
2209
+ const cwdRel =
2210
+ relativePathWithinRoot(this.sessionManager.getCwd(), rulePath) ??
2211
+ this.#displayPathWithinRoot(this.sessionManager.getCwd(), rulePath);
2190
2212
  if (cwdRel) return cwdRel;
2191
2213
  const homeRel = relativePathWithinRoot(os.homedir(), rulePath);
2192
2214
  if (homeRel) return `~/${homeRel}`;
2193
2215
  return rulePath;
2194
2216
  }
2195
2217
 
2218
+ #displayPathWithinRoot(root: string, candidate: string): string | null {
2219
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
2220
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : null;
2221
+ }
2222
+
2196
2223
  #addPendingTtsrInjections(rules: Rule[]): void {
2197
2224
  const seen = new Set(this.#pendingTtsrInjections.map(rule => rule.name));
2198
2225
  for (const rule of rules) {
@@ -2947,6 +2974,10 @@ export class AgentSession {
2947
2974
  this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
2948
2975
  }
2949
2976
 
2977
+ #activeProviderSessionId(sessionId?: string): string {
2978
+ return this.#freshProviderSessionId ?? this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
2979
+ }
2980
+
2950
2981
  /**
2951
2982
  * Set agent.sessionId from the session manager and install a dynamic
2952
2983
  * metadata resolver so every Anthropic API request carries
@@ -2959,7 +2990,7 @@ export class AgentSession {
2959
2990
  * `#syncAgentSessionId()` on every such event.
2960
2991
  */
2961
2992
  #syncAgentSessionId(sessionId?: string): void {
2962
- const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
2993
+ const sid = this.#activeProviderSessionId(sessionId);
2963
2994
  this.agent.sessionId = sid;
2964
2995
  this.agent.setMetadataResolver((provider: string) =>
2965
2996
  buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage),
@@ -2967,14 +2998,14 @@ export class AgentSession {
2967
2998
  }
2968
2999
 
2969
3000
  #rekeyHindsightMemoryForCurrentSessionId(): void {
2970
- if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
3001
+ if (this.settings.get("memory.backend") !== "hindsight") return;
2971
3002
  const sid = this.agent.sessionId;
2972
3003
  if (!sid) return;
2973
3004
  this.getHindsightSessionState()?.setSessionId(sid);
2974
3005
  }
2975
3006
 
2976
3007
  #rekeyMnemopiMemoryForCurrentSessionId(): void {
2977
- if (resolveMemoryBackend(this.settings).id !== "mnemopi") return;
3008
+ if (this.settings.get("memory.backend") !== "mnemopi") return;
2978
3009
  const sid = this.agent.sessionId;
2979
3010
  if (!sid) return;
2980
3011
  this.getMnemopiSessionState()?.setSessionId(sid);
@@ -2982,14 +3013,14 @@ export class AgentSession {
2982
3013
 
2983
3014
  /** New session file: reset auto-recall / retain-threshold counters for the new transcript. */
2984
3015
  #resetHindsightConversationTrackingIfHindsight(): void {
2985
- if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
3016
+ if (this.settings.get("memory.backend") !== "hindsight") return;
2986
3017
  const state = this.getHindsightSessionState();
2987
3018
  if (!state || state.aliasOf) return;
2988
3019
  state.resetConversationTracking();
2989
3020
  }
2990
3021
 
2991
3022
  #resetMnemopiConversationTrackingIfMnemopi(): void {
2992
- if (resolveMemoryBackend(this.settings).id !== "mnemopi") return;
3023
+ if (this.settings.get("memory.backend") !== "mnemopi") return;
2993
3024
  const state = this.getMnemopiSessionState();
2994
3025
  if (!state || state.aliasOf) return;
2995
3026
  state.resetConversationTracking();
@@ -3089,6 +3120,23 @@ export class AgentSession {
3089
3120
  this.#providerSessionState.clear();
3090
3121
  }
3091
3122
 
3123
+ freshSession(): FreshSessionResult | undefined {
3124
+ if (this.isStreaming) return undefined;
3125
+ const previousSessionId = this.sessionId;
3126
+ const closedProviderSessions = this.#providerSessionState.size;
3127
+ this.#closeAllProviderSessions("fresh session");
3128
+ this.#freshProviderSessionId = Bun.randomUUIDv7();
3129
+ this.#syncAgentSessionId();
3130
+ this.#rekeyHindsightMemoryForCurrentSessionId();
3131
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
3132
+ this.agent.appendOnlyContext?.invalidateForModelChange();
3133
+ return {
3134
+ previousSessionId,
3135
+ sessionId: this.sessionId,
3136
+ closedProviderSessions,
3137
+ };
3138
+ }
3139
+
3092
3140
  // =========================================================================
3093
3141
  // Read-only State Access
3094
3142
  // =========================================================================
@@ -3670,7 +3718,7 @@ export class AgentSession {
3670
3718
  }
3671
3719
 
3672
3720
  async #buildSystemPromptForAgentStart(promptText: string): Promise<string[]> {
3673
- const backend = resolveMemoryBackend(this.settings);
3721
+ const backend = await resolveMemoryBackend(this.settings);
3674
3722
  if (!backend.beforeAgentStartPrompt) return this.#baseSystemPrompt;
3675
3723
 
3676
3724
  try {
@@ -3993,7 +4041,7 @@ export class AgentSession {
3993
4041
 
3994
4042
  /** Current session ID */
3995
4043
  get sessionId(): string {
3996
- return this.#providerSessionId ?? this.sessionManager.getSessionId();
4044
+ return this.#activeProviderSessionId();
3997
4045
  }
3998
4046
  getEvalSessionId(): string | null {
3999
4047
  if (this.#parentEvalSessionId !== undefined) return this.#parentEvalSessionId;
@@ -5092,8 +5140,13 @@ export class AgentSession {
5092
5140
 
5093
5141
  /**
5094
5142
  * Abort current operation and wait for agent to become idle.
5143
+ *
5144
+ * `reason` (e.g. `USER_INTERRUPT_LABEL`) rides the agent's `AbortController`
5145
+ * and surfaces verbatim on the aborted assistant message's `errorMessage`, so
5146
+ * the transcript can distinguish a deliberate user interrupt from an opaque
5147
+ * abort. Omit it for internal/lifecycle aborts.
5095
5148
  */
5096
- async abort(options?: { goalReason?: "interrupted" | "internal" }): Promise<void> {
5149
+ async abort(options?: { goalReason?: "interrupted" | "internal"; reason?: string }): Promise<void> {
5097
5150
  this.abortRetry();
5098
5151
  this.#promptGeneration++;
5099
5152
  this.#scheduledHiddenNextTurnGeneration = undefined;
@@ -5102,7 +5155,7 @@ export class AgentSession {
5102
5155
  this.abortBash();
5103
5156
  this.abortEval();
5104
5157
  const postPromptDrain = this.#cancelPostPromptTasks();
5105
- this.agent.abort();
5158
+ this.agent.abort(options?.reason);
5106
5159
  await postPromptDrain;
5107
5160
  await this.agent.waitForIdle();
5108
5161
  await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
@@ -5119,6 +5172,19 @@ export class AgentSession {
5119
5172
  }
5120
5173
  }
5121
5174
 
5175
+ /**
5176
+ * Abort active work, then immediately resume the agent so queued steer/follow-up
5177
+ * messages drain instead of waiting for another natural turn boundary.
5178
+ */
5179
+ async interruptAndFlushQueuedMessages(options?: { reason?: string }): Promise<void> {
5180
+ if (!this.agent.hasQueuedMessages()) return;
5181
+ await this.abort({ reason: options?.reason });
5182
+ if (!this.agent.hasQueuedMessages()) return;
5183
+ if (this.isCompacting || this.isGeneratingHandoff) return;
5184
+ await this.#maybeRestoreRetryFallbackPrimary();
5185
+ await this.agent.continue();
5186
+ }
5187
+
5122
5188
  /**
5123
5189
  * Start a new session, optionally with initial messages and parent tracking.
5124
5190
  * Clears all messages and starts a new session.
@@ -5163,6 +5229,7 @@ export class AgentSession {
5163
5229
  }
5164
5230
  await this.sessionManager.newSession(options);
5165
5231
  this.setTodoPhases([]);
5232
+ this.#freshProviderSessionId = undefined;
5166
5233
  this.#syncAgentSessionId();
5167
5234
  this.#rekeyHindsightMemoryForCurrentSessionId();
5168
5235
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -5260,6 +5327,7 @@ export class AgentSession {
5260
5327
  }
5261
5328
 
5262
5329
  // Update agent session ID
5330
+ this.#freshProviderSessionId = undefined;
5263
5331
  this.#syncAgentSessionId();
5264
5332
  this.#rekeyHindsightMemoryForCurrentSessionId();
5265
5333
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -6096,7 +6164,7 @@ export class AgentSession {
6096
6164
  messagesToSummarize: AgentMessage[];
6097
6165
  turnPrefixMessages: AgentMessage[];
6098
6166
  }): Promise<string | undefined> {
6099
- const backend = resolveMemoryBackend(this.settings);
6167
+ const backend = await resolveMemoryBackend(this.settings);
6100
6168
  if (!backend.preCompactionContext) return undefined;
6101
6169
  const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
6102
6170
  try {
@@ -6227,6 +6295,7 @@ export class AgentSession {
6227
6295
  this.#cancelOwnAsyncJobs();
6228
6296
  await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
6229
6297
  this.agent.reset();
6298
+ this.#freshProviderSessionId = undefined;
6230
6299
  this.#syncAgentSessionId();
6231
6300
  this.#rekeyHindsightMemoryForCurrentSessionId();
6232
6301
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -8942,6 +9011,7 @@ export class AgentSession {
8942
9011
  const previousTools = [...this.agent.state.tools];
8943
9012
  const previousBaseSystemPrompt = this.#baseSystemPrompt;
8944
9013
  const previousSystemPrompt = this.agent.state.systemPrompt;
9014
+ const previousFreshProviderSessionId = this.#freshProviderSessionId;
8945
9015
  const previousFallbackSelectedMCPToolNames = previousSessionFile
8946
9016
  ? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
8947
9017
  : undefined;
@@ -8953,6 +9023,9 @@ export class AgentSession {
8953
9023
 
8954
9024
  try {
8955
9025
  await this.sessionManager.setSessionFile(sessionPath);
9026
+ if (switchingToDifferentSession) {
9027
+ this.#freshProviderSessionId = undefined;
9028
+ }
8956
9029
  this.#syncAgentSessionId();
8957
9030
  this.#rekeyHindsightMemoryForCurrentSessionId();
8958
9031
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -9062,6 +9135,7 @@ export class AgentSession {
9062
9135
  return true;
9063
9136
  } catch (error) {
9064
9137
  this.sessionManager.restoreState(previousSessionState);
9138
+ this.#freshProviderSessionId = previousFreshProviderSessionId;
9065
9139
  this.#syncAgentSessionId(previousSessionState.sessionId);
9066
9140
  this.#rekeyHindsightMemoryForCurrentSessionId();
9067
9141
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -9160,6 +9234,7 @@ export class AgentSession {
9160
9234
  this.sessionManager.createBranchedSession(selectedEntry.parentId);
9161
9235
  }
9162
9236
  this.#syncTodoPhasesFromBranch();
9237
+ this.#freshProviderSessionId = undefined;
9163
9238
  this.#syncAgentSessionId();
9164
9239
  this.#rekeyHindsightMemoryForCurrentSessionId();
9165
9240
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -9588,6 +9663,7 @@ export class AgentSession {
9588
9663
  */
9589
9664
  async exportToHtml(outputPath?: string): Promise<string> {
9590
9665
  const themeName = getCurrentThemeName();
9666
+ const { exportSessionToHtml } = await import("../export/html");
9591
9667
  return exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName });
9592
9668
  }
9593
9669
 
@@ -70,6 +70,32 @@ export function isSilentAbort(errorMessage: string | undefined): boolean {
70
70
  return errorMessage === SILENT_ABORT_MARKER;
71
71
  }
72
72
 
73
+ /** Reason threaded through `AbortController.abort(reason)` when the user aborts
74
+ * the turn with Esc (see `AgentSession.abort`). The agent surfaces it verbatim
75
+ * on the aborted assistant message's `errorMessage`, so the transcript reads as
76
+ * a deliberate user interrupt instead of an opaque failure. */
77
+ export const USER_INTERRUPT_LABEL = "Interrupted by user";
78
+
79
+ /** Sentinel `errorMessage` the agent stamps on any abort that carried no custom
80
+ * reason (bare `abort()`). Renderers treat it as "no specific reason given". */
81
+ const GENERIC_ABORT_SENTINEL = "Request was aborted";
82
+
83
+ /** Resolve the operator-facing label for an aborted assistant turn. A custom
84
+ * abort reason (e.g. `USER_INTERRUPT_LABEL`) threaded onto `errorMessage` is
85
+ * shown verbatim; aborts with no threaded reason fall back to the retry-aware
86
+ * generic label. Centralizes the live-stream (`EventController`), replay
87
+ * (`ui-helpers`), and component (`AssistantMessageComponent`) render paths so
88
+ * they stay in lockstep. */
89
+ export function resolveAbortLabel(errorMessage: string | undefined, retryAttempt = 0): string {
90
+ if (errorMessage && errorMessage !== GENERIC_ABORT_SENTINEL && !isSilentAbort(errorMessage)) {
91
+ return errorMessage;
92
+ }
93
+ if (retryAttempt > 0) {
94
+ return `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`;
95
+ }
96
+ return "Operation aborted";
97
+ }
98
+
73
99
  /** Extract the optional `__pendingDisplayTag` field from a CustomMessage's
74
100
  * `details` blob. Safe over `unknown`; returns undefined when the field is
75
101
  * absent or non-string. */
@@ -1967,6 +1967,7 @@ export class SessionManager {
1967
1967
  #inMemoryArtifacts: Map<string, string> | null = null;
1968
1968
  #inMemoryArtifactCounter = 0;
1969
1969
  readonly #blobStore: BlobStore;
1970
+ #suppressBreadcrumb = false;
1970
1971
 
1971
1972
  private constructor(
1972
1973
  private cwd: string,
@@ -1981,6 +1982,11 @@ export class SessionManager {
1981
1982
  // Note: call _initSession() or _initSessionFile() after construction
1982
1983
  }
1983
1984
 
1985
+ #maybeWriteBreadcrumb(cwd: string, sessionFile: string): void {
1986
+ if (this.#suppressBreadcrumb) return;
1987
+ writeTerminalBreadcrumb(cwd, sessionFile);
1988
+ }
1989
+
1984
1990
  /** Puts a binary blob into the blob store and returns the blob reference */
1985
1991
  async putBlob(data: Buffer, options?: BlobPutOptions): Promise<BlobPutResult> {
1986
1992
  return this.#blobStore.put(data, options);
@@ -2027,7 +2033,7 @@ export class SessionManager {
2027
2033
  this.#adoptedArtifactManager = null;
2028
2034
  this.#buildIndex();
2029
2035
  if (this.#sessionFile) {
2030
- writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
2036
+ this.#maybeWriteBreadcrumb(this.cwd, this.#sessionFile);
2031
2037
  }
2032
2038
  }
2033
2039
 
@@ -2047,7 +2053,7 @@ export class SessionManager {
2047
2053
  this.#persistError = undefined;
2048
2054
  this.#persistErrorReported = false;
2049
2055
  this.#sessionFile = path.resolve(sessionFile);
2050
- writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
2056
+ this.#maybeWriteBreadcrumb(this.cwd, this.#sessionFile);
2051
2057
  this.#fileEntries = await loadEntriesFromFile(this.#sessionFile, this.storage);
2052
2058
  if (this.#fileEntries.length > 0) {
2053
2059
  const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
@@ -2064,7 +2070,7 @@ export class SessionManager {
2064
2070
  if (headerCwd && headerCwd !== this.cwd) {
2065
2071
  this.cwd = headerCwd;
2066
2072
  this.sessionDir = path.resolve(this.#sessionFile, "..");
2067
- writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
2073
+ this.#maybeWriteBreadcrumb(this.cwd, this.#sessionFile);
2068
2074
  }
2069
2075
 
2070
2076
  this.#needsFullRewriteOnNextPersist = migrateToCurrentVersion(this.#fileEntries);
@@ -2245,7 +2251,7 @@ export class SessionManager {
2245
2251
 
2246
2252
  // Update terminal breadcrumb
2247
2253
  if (this.#sessionFile) {
2248
- writeTerminalBreadcrumb(resolvedCwd, this.#sessionFile);
2254
+ this.#maybeWriteBreadcrumb(resolvedCwd, this.#sessionFile);
2249
2255
  }
2250
2256
  }
2251
2257
 
@@ -2280,7 +2286,7 @@ export class SessionManager {
2280
2286
  if (this.persist) {
2281
2287
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
2282
2288
  this.#sessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${this.#sessionId}.jsonl`);
2283
- writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
2289
+ this.#maybeWriteBreadcrumb(this.cwd, this.#sessionFile);
2284
2290
  }
2285
2291
  return this.#sessionFile;
2286
2292
  }
@@ -3429,9 +3435,11 @@ export class SessionManager {
3429
3435
  cwd: string,
3430
3436
  sessionDir?: string,
3431
3437
  storage: SessionStorage = new FileSessionStorage(),
3438
+ options?: { suppressBreadcrumb?: boolean },
3432
3439
  ): Promise<SessionManager> {
3433
3440
  const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
3434
3441
  const manager = new SessionManager(cwd, dir, true, storage);
3442
+ manager.#suppressBreadcrumb = options?.suppressBreadcrumb === true;
3435
3443
  const forkEntries = structuredClone(await loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
3436
3444
  migrateToCurrentVersion(forkEntries);
3437
3445
  await resolveBlobRefsInEntries(forkEntries, manager.#blobStore);
@@ -21,6 +21,7 @@ import {
21
21
  } from "../extensibility/plugins/marketplace";
22
22
  import { resolveMemoryBackend } from "../memory-backend";
23
23
  import type { InteractiveModeContext } from "../modes/types";
24
+ import type { FreshSessionResult } from "../session/agent-session";
24
25
  import { formatShakeSummary, type ShakeMode } from "../session/shake-types";
25
26
  import { getChangelogPath, parseChangelog } from "../utils/changelog";
26
27
  import { buildContextReportText } from "./helpers/context-report";
@@ -52,6 +53,11 @@ function refreshStatusLine(ctx: InteractiveModeContext): void {
52
53
  ctx.ui.requestRender();
53
54
  }
54
55
 
56
+ function formatFreshSessionResult(result: FreshSessionResult): string {
57
+ const stateLabel = result.closedProviderSessions === 1 ? "provider state" : "provider states";
58
+ return `Fresh provider session started (${result.closedProviderSessions} ${stateLabel} pruned).`;
59
+ }
60
+
55
61
  const shutdownHandlerTui = (_command: ParsedSlashCommand, runtime: TuiSlashCommandRuntime): SlashCommandResult => {
56
62
  runtime.ctx.editor.setText("");
57
63
  void runtime.ctx.shutdown();
@@ -770,6 +776,25 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
770
776
  await runtime.ctx.handleClearCommand();
771
777
  },
772
778
  },
779
+ {
780
+ name: "fresh",
781
+ description: "Reset provider stream state without changing the local transcript",
782
+ handle: async (_command, runtime) => {
783
+ const result = runtime.session.freshSession();
784
+ if (!result) {
785
+ await runtime.output(
786
+ "Wait for the current response to finish or abort it before refreshing provider state.",
787
+ );
788
+ return commandConsumed();
789
+ }
790
+ await runtime.output(formatFreshSessionResult(result));
791
+ return commandConsumed();
792
+ },
793
+ handleTui: async (_command, runtime) => {
794
+ runtime.ctx.editor.setText("");
795
+ await runtime.ctx.handleFreshCommand();
796
+ },
797
+ },
773
798
  {
774
799
  name: "drop",
775
800
  description: "Delete the current session and start a new one",
@@ -868,6 +893,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
868
893
  await runtime.ctx.handleBtwCommand(question);
869
894
  },
870
895
  },
896
+ {
897
+ name: "tan",
898
+ description: "Run a full background agent on tangential work",
899
+ inlineHint: "<work>",
900
+ allowArgs: true,
901
+ handleTui: async (command, runtime) => {
902
+ const work = command.text.slice(`/${command.name}`.length).trim();
903
+ runtime.ctx.editor.setText("");
904
+ await runtime.ctx.handleTanCommand(work);
905
+ },
906
+ },
871
907
  {
872
908
  name: "omfg",
873
909
  description: "Forge a TTSR rule from a complaint to stop a recurring behavior",
@@ -890,15 +926,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
890
926
  runtime.ctx.editor.setText("");
891
927
  },
892
928
  },
893
- {
894
- name: "background",
895
- aliases: ["bg"],
896
- description: "Detach UI and continue running in background",
897
- handleTui: (_command, runtime) => {
898
- runtime.ctx.editor.setText("");
899
- runtime.handleBackgroundCommand();
900
- },
901
- },
902
929
  {
903
930
  name: "debug",
904
931
  description: "Open debug tools selector",
@@ -934,7 +961,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
934
961
  allowArgs: true,
935
962
  handle: async (command, runtime) => {
936
963
  const verb = (command.args.trim().split(/\s+/)[0] ?? "").toLowerCase() || "view";
937
- const backend = resolveMemoryBackend(runtime.settings);
964
+ const backend = await resolveMemoryBackend(runtime.settings);
938
965
  switch (verb) {
939
966
  case "view": {
940
967
  const payload = await backend.buildDeveloperInstructions(
@@ -26,6 +26,8 @@ function formatUsageReportAccount(report: UsageReport, limit: UsageLimit, index:
26
26
  if (typeof email === "string" && email) return email;
27
27
  const accountId = report.metadata?.accountId ?? limit.scope.accountId;
28
28
  if (typeof accountId === "string" && accountId) return accountId;
29
+ const projectId = report.metadata?.projectId ?? limit.scope.projectId;
30
+ if (typeof projectId === "string" && projectId) return projectId;
29
31
  return `account ${index + 1}`;
30
32
  }
31
33
 
@@ -71,15 +71,13 @@ export interface SlashCommandRuntime {
71
71
 
72
72
  /**
73
73
  * Runtime visible to TUI-only handlers (`handleTui`). Carries the interactive
74
- * mode context plus the background-detach hook. Intentionally narrower than
75
- * `SlashCommandRuntime` so existing callers can keep building it from just
76
- * `{ ctx, handleBackgroundCommand }`; when the TUI dispatcher needs to invoke
77
- * a `handle` (no `handleTui` override), it synthesizes a `SlashCommandRuntime`
78
- * from `ctx`.
74
+ * mode context. Intentionally narrower than `SlashCommandRuntime` so existing
75
+ * callers can keep building it from just `{ ctx }`; when the TUI dispatcher
76
+ * needs to invoke a `handle` (no `handleTui` override), it synthesizes a
77
+ * `SlashCommandRuntime` from `ctx`.
79
78
  */
80
79
  export interface TuiSlashCommandRuntime {
81
80
  ctx: InteractiveModeContext;
82
- handleBackgroundCommand: () => void;
83
81
  }
84
82
 
85
83
  /** Unified slash-command spec consumed by both TUI and ACP dispatchers. */
@@ -166,6 +166,13 @@ export interface ExecutorOptions {
166
166
  outputSchema?: unknown;
167
167
  /** Parent task recursion depth (0 = top-level, 1 = first child, etc.) */
168
168
  taskDepth?: number;
169
+ /**
170
+ * Override the `task.maxRuntimeMs` wall-clock cap for this run. When provided
171
+ * it wins over the settings value; `0` disables the per-subagent wall-clock
172
+ * limit entirely. Used by the eval `agent()` bridge, whose parent cell
173
+ * watchdog is already suspended for the call's duration.
174
+ */
175
+ maxRuntimeMs?: number;
169
176
  enableLsp?: boolean;
170
177
  signal?: AbortSignal;
171
178
  onProgress?: (progress: AgentProgress) => void;
@@ -481,7 +488,7 @@ function getUsageTokens(usage: unknown): number {
481
488
  /**
482
489
  * Create proxy tools that reuse the parent's MCP connections.
483
490
  */
484
- function createMCPProxyTools(mcpManager: MCPManager): CustomTool[] {
491
+ export function createMCPProxyTools(mcpManager: MCPManager): CustomTool[] {
485
492
  return mcpManager.getTools().map(tool => {
486
493
  const mcpTool = tool as { mcpToolName?: string; mcpServerName?: string };
487
494
  return {
@@ -531,7 +538,10 @@ function createMCPProxyTools(mcpManager: MCPManager): CustomTool[] {
531
538
  });
532
539
  }
533
540
 
534
- function createSubagentSettings(baseSettings: Settings, overrides?: Partial<Record<SettingPath, unknown>>): Settings {
541
+ export function createSubagentSettings(
542
+ baseSettings: Settings,
543
+ overrides?: Partial<Record<SettingPath, unknown>>,
544
+ ): Settings {
535
545
  const snapshot: Partial<Record<SettingPath, unknown>> = {};
536
546
  for (const key of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
537
547
  snapshot[key] = baseSettings.get(key);
@@ -625,7 +635,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
625
635
  agent.readSummarize === false ? { "read.summarize.enabled": false } : undefined,
626
636
  );
627
637
  const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
628
- const maxRuntimeMs = Math.max(0, Math.trunc(Number(settings.get("task.maxRuntimeMs") ?? 0) || 0));
638
+ const maxRuntimeMs = Math.max(
639
+ 0,
640
+ Math.trunc(Number(options.maxRuntimeMs ?? settings.get("task.maxRuntimeMs") ?? 0) || 0),
641
+ );
629
642
  const parentDepth = options.taskDepth ?? 0;
630
643
  const childDepth = parentDepth + 1;
631
644
  const atMaxDepth = maxRecursionDepth >= 0 && childDepth >= maxRecursionDepth;
@@ -1484,7 +1497,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1484
1497
  if (lastAssistant.stopReason === "aborted") {
1485
1498
  aborted = abortReason === "signal" || runtimeLimitExceeded || abortReason === undefined;
1486
1499
  if (aborted) {
1487
- abortReasonText ??= resolveAbortReasonText();
1500
+ // A real caller signal or the wall-clock timer carries a precise
1501
+ // reason (signal.reason / "runtime limit exceeded"). An internal
1502
+ // turn abort (abortReason === undefined) does NOT — prefer the
1503
+ // assistant message's own errorMessage ("Request was aborted" or a
1504
+ // specific stream error) over the misleading "Cancelled by caller".
1505
+ abortReasonText ??=
1506
+ abortReason === "signal" || runtimeLimitExceeded
1507
+ ? resolveAbortReasonText()
1508
+ : lastAssistant.errorMessage?.trim() || resolveAbortReasonText();
1488
1509
  }
1489
1510
  exitCode = 1;
1490
1511
  } else if (lastAssistant.stopReason === "error") {
package/src/task/index.ts CHANGED
@@ -275,6 +275,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
275
275
  readonly strict = true;
276
276
  readonly loadMode = "discoverable";
277
277
  readonly renderResult = renderResult;
278
+ // Suppress the streaming call preview once a (partial or final) result exists
279
+ // so the task renders as ONE block that transitions in place — not a pending
280
+ // call frame stacked above the result frame. Mirrors `taskToolRenderer`.
281
+ readonly mergeCallAndResult = true;
278
282
  readonly #discoveredAgents: AgentDefinition[];
279
283
  readonly #blockedAgent: string | undefined;
280
284