@oh-my-pi/pi-coding-agent 17.0.8 → 17.1.0

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 (340) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +1 -1
  3. package/dist/cli.js +5495 -5021
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
  10. package/dist/types/config/model-registry.d.ts +30 -1
  11. package/dist/types/config/models-config-schema.d.ts +18 -6
  12. package/dist/types/config/models-config.d.ts +14 -5
  13. package/dist/types/config/provider-globals.d.ts +2 -2
  14. package/dist/types/config/service-tier.d.ts +5 -1
  15. package/dist/types/config/settings-schema.d.ts +350 -50
  16. package/dist/types/config/settings.d.ts +7 -0
  17. package/dist/types/dap/client.d.ts +20 -0
  18. package/dist/types/exec/bash-executor.d.ts +38 -0
  19. package/dist/types/exec/direnv.d.ts +33 -0
  20. package/dist/types/export/html/args.d.ts +15 -0
  21. package/dist/types/export/html/index.d.ts +11 -26
  22. package/dist/types/export/html/web-palette.d.ts +55 -116
  23. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  25. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
  26. package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
  27. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +14 -1
  28. package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
  29. package/dist/types/live/controller.d.ts +40 -0
  30. package/dist/types/live/protocol.d.ts +85 -0
  31. package/dist/types/live/protocol.test.d.ts +1 -0
  32. package/dist/types/live/transport.d.ts +48 -0
  33. package/dist/types/live/visualizer.d.ts +33 -0
  34. package/dist/types/lsp/client.d.ts +18 -1
  35. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  36. package/dist/types/mcp/manager.d.ts +6 -2
  37. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  38. package/dist/types/mcp/render.d.ts +2 -2
  39. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  40. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  41. package/dist/types/mcp/tool-bridge.d.ts +7 -3
  42. package/dist/types/mcp/types.d.ts +6 -0
  43. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  44. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  45. package/dist/types/modes/components/oauth-selector.d.ts +8 -0
  46. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  47. package/dist/types/modes/components/settings-defs.d.ts +8 -1
  48. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  49. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  50. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  51. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  52. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  53. package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
  54. package/dist/types/modes/interactive-mode.d.ts +5 -0
  55. package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
  56. package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
  57. package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
  58. package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
  59. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  60. package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
  61. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  62. package/dist/types/modes/theme/theme.d.ts +1 -1
  63. package/dist/types/modes/types.d.ts +3 -1
  64. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  65. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  66. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  67. package/dist/types/registry/agent-registry.d.ts +11 -4
  68. package/dist/types/sdk.d.ts +15 -1
  69. package/dist/types/secrets/index.d.ts +15 -2
  70. package/dist/types/secrets/obfuscator.d.ts +113 -15
  71. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  72. package/dist/types/session/agent-session-events.d.ts +84 -0
  73. package/dist/types/session/agent-session-types.d.ts +314 -0
  74. package/dist/types/session/agent-session.d.ts +120 -750
  75. package/dist/types/session/async-job-delivery.d.ts +28 -0
  76. package/dist/types/session/bash-runner.d.ts +75 -0
  77. package/dist/types/session/blob-store.d.ts +11 -1
  78. package/dist/types/session/blob-store.test.d.ts +1 -0
  79. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  80. package/dist/types/session/eval-runner.d.ts +49 -0
  81. package/dist/types/session/irc-bridge.d.ts +46 -0
  82. package/dist/types/session/messages.d.ts +22 -0
  83. package/dist/types/session/model-controls.d.ts +180 -0
  84. package/dist/types/session/prewalk.d.ts +46 -0
  85. package/dist/types/session/queued-messages.d.ts +22 -0
  86. package/dist/types/session/redis-session-storage.d.ts +1 -0
  87. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  88. package/dist/types/session/role-models.d.ts +15 -0
  89. package/dist/types/session/session-advisors.d.ts +228 -0
  90. package/dist/types/session/session-entries.d.ts +8 -0
  91. package/dist/types/session/session-handoff.d.ts +70 -0
  92. package/dist/types/session/session-history-format.d.ts +6 -1
  93. package/dist/types/session/session-loader.d.ts +6 -4
  94. package/dist/types/session/session-maintenance.d.ts +246 -0
  95. package/dist/types/session/session-manager.d.ts +41 -0
  96. package/dist/types/session/session-memory.d.ts +56 -0
  97. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  98. package/dist/types/session/session-stats.d.ts +48 -0
  99. package/dist/types/session/session-tools.d.ts +143 -0
  100. package/dist/types/session/session-workspace.d.ts +32 -0
  101. package/dist/types/session/stream-guards.d.ts +44 -0
  102. package/dist/types/session/todo-tracker.d.ts +56 -0
  103. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  104. package/dist/types/session/turn-recovery.d.ts +225 -0
  105. package/dist/types/system-prompt.d.ts +2 -0
  106. package/dist/types/task/executor.d.ts +4 -1
  107. package/dist/types/task/output-manager.d.ts +3 -1
  108. package/dist/types/task/types.d.ts +14 -0
  109. package/dist/types/tools/approval.d.ts +1 -0
  110. package/dist/types/tools/browser/launch.d.ts +5 -0
  111. package/dist/types/tools/hub/jobs.d.ts +1 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -4
  113. package/dist/types/tools/image-providers.d.ts +38 -0
  114. package/dist/types/tools/index.d.ts +8 -0
  115. package/dist/types/tools/path-utils.d.ts +8 -0
  116. package/dist/types/tools/report-tool-issue.d.ts +24 -9
  117. package/dist/types/tools/todo.d.ts +10 -4
  118. package/dist/types/tools/xdev.d.ts +5 -1
  119. package/dist/types/tts/streaming-player.d.ts +14 -0
  120. package/dist/types/tts/vocalizer.d.ts +5 -0
  121. package/dist/types/utils/jj.d.ts +29 -0
  122. package/dist/types/utils/lang-from-path.d.ts +1 -0
  123. package/dist/types/utils/title-generator.d.ts +30 -0
  124. package/dist/types/vibe/runtime.d.ts +36 -8
  125. package/dist/types/web/search/index.d.ts +1 -1
  126. package/dist/types/web/search/provider.d.ts +15 -5
  127. package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
  128. package/dist/types/web/search/types.d.ts +95 -1
  129. package/package.json +13 -12
  130. package/scripts/generate-share-viewer.ts +4 -6
  131. package/scripts/legacy-pi-virtual-module.ts +1 -1
  132. package/src/advisor/__tests__/advisor.test.ts +643 -0
  133. package/src/advisor/runtime.ts +144 -43
  134. package/src/async/job-manager.ts +90 -2
  135. package/src/cli/args.ts +2 -0
  136. package/src/cli/auth-gateway-cli.ts +18 -3
  137. package/src/cli/bench-cli.ts +395 -29
  138. package/src/cli/flag-tables.ts +3 -0
  139. package/src/cli/gc-cli.ts +2 -2
  140. package/src/cli/grievances-cli.ts +2 -2
  141. package/src/cli/usage-cli.ts +11 -2
  142. package/src/collab/host.ts +3 -2
  143. package/src/commands/bench.ts +18 -4
  144. package/src/commands/launch.ts +4 -0
  145. package/src/config/__tests__/model-registry.test.ts +147 -0
  146. package/src/config/model-registry.ts +50 -2
  147. package/src/config/model-resolver.ts +0 -1
  148. package/src/config/models-config-schema.ts +15 -4
  149. package/src/config/provider-globals.ts +9 -9
  150. package/src/config/service-tier.ts +26 -1
  151. package/src/config/settings-schema.ts +250 -54
  152. package/src/config/settings.ts +81 -2
  153. package/src/dap/client.ts +37 -4
  154. package/src/edit/index.ts +49 -12
  155. package/src/eval/py/executor.ts +103 -33
  156. package/src/exec/bash-executor.ts +89 -6
  157. package/src/exec/direnv.ts +145 -0
  158. package/src/export/html/args.ts +20 -0
  159. package/src/export/html/index.ts +45 -47
  160. package/src/export/html/template.css +19 -1
  161. package/src/export/html/template.html +6 -0
  162. package/src/export/html/template.js +21 -0
  163. package/src/export/html/tool-views.generated.js +31 -31
  164. package/src/export/html/web-palette.ts +116 -132
  165. package/src/export/share.ts +249 -49
  166. package/src/extensibility/extensions/loader.ts +29 -1
  167. package/src/extensibility/extensions/runner.ts +6 -0
  168. package/src/extensibility/extensions/types.ts +33 -2
  169. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
  170. package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
  171. package/src/extensibility/plugins/legacy-pi-compat.ts +870 -311
  172. package/src/hindsight/state.ts +64 -6
  173. package/src/internal-urls/local-protocol.ts +2 -1
  174. package/src/internal-urls/memory-protocol.ts +2 -2
  175. package/src/internal-urls/registry-helpers.ts +40 -0
  176. package/src/internal-urls/skill-protocol.ts +2 -2
  177. package/src/internal-urls/ssh-protocol.ts +2 -1
  178. package/src/internal-urls/vault-protocol.ts +2 -1
  179. package/src/live/audio-worklet.txt +59 -0
  180. package/src/live/browser-runtime.txt +221 -0
  181. package/src/live/controller.ts +464 -0
  182. package/src/live/prompts/agent-final-message.md +3 -0
  183. package/src/live/prompts/live-instructions.md +23 -0
  184. package/src/live/protocol.test.ts +140 -0
  185. package/src/live/protocol.ts +233 -0
  186. package/src/live/transport.ts +490 -0
  187. package/src/live/visualizer.ts +234 -0
  188. package/src/lsp/client.ts +52 -9
  189. package/src/lsp/clients/biome-client.ts +3 -4
  190. package/src/lsp/index.ts +44 -11
  191. package/src/main.ts +9 -1
  192. package/src/mcp/config-writer.test.ts +43 -0
  193. package/src/mcp/config-writer.ts +109 -82
  194. package/src/mcp/manager.ts +39 -8
  195. package/src/mcp/oauth-discovery.ts +10 -2
  196. package/src/mcp/render.ts +94 -35
  197. package/src/mcp/smithery-auth.test.ts +29 -0
  198. package/src/mcp/smithery-auth.ts +3 -2
  199. package/src/mcp/tool-bridge.ts +107 -11
  200. package/src/mcp/types.ts +7 -0
  201. package/src/memories/index.ts +40 -20
  202. package/src/memory-backend/tool-names.ts +2 -0
  203. package/src/mnemopi/backend.ts +24 -7
  204. package/src/modes/acp/acp-agent.ts +56 -10
  205. package/src/modes/acp/acp-event-mapper.ts +8 -1
  206. package/src/modes/components/agent-hub.ts +27 -10
  207. package/src/modes/components/login-dialog.ts +14 -4
  208. package/src/modes/components/oauth-selector.ts +24 -7
  209. package/src/modes/components/plan-review-overlay.ts +350 -35
  210. package/src/modes/components/settings-defs.ts +28 -3
  211. package/src/modes/components/settings-selector.ts +178 -6
  212. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  213. package/src/modes/components/status-line/component.ts +123 -2
  214. package/src/modes/components/tool-execution.test.ts +63 -2
  215. package/src/modes/components/tool-execution.ts +11 -2
  216. package/src/modes/controllers/command-controller.ts +70 -22
  217. package/src/modes/controllers/event-controller.ts +111 -16
  218. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  219. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  220. package/src/modes/controllers/live-command-controller.ts +178 -0
  221. package/src/modes/controllers/mcp-command-controller.ts +80 -41
  222. package/src/modes/controllers/omfg-controller.ts +44 -40
  223. package/src/modes/controllers/selector-controller.ts +18 -10
  224. package/src/modes/controllers/session-focus-controller.ts +6 -1
  225. package/src/modes/interactive-mode.ts +160 -38
  226. package/src/modes/rpc/rpc-client.ts +94 -3
  227. package/src/modes/rpc/rpc-frame.ts +164 -4
  228. package/src/modes/rpc/rpc-messages.ts +127 -0
  229. package/src/modes/rpc/rpc-mode.ts +79 -7
  230. package/src/modes/rpc/rpc-types.ts +34 -2
  231. package/src/modes/runtime-init.ts +2 -0
  232. package/src/modes/setup-wizard/scenes/model.ts +5 -7
  233. package/src/modes/setup-wizard/scenes/providers.ts +3 -2
  234. package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
  235. package/src/modes/setup-wizard/scenes/theme.ts +13 -3
  236. package/src/modes/setup-wizard/scenes/types.ts +9 -1
  237. package/src/modes/setup-wizard/scenes/web-search.ts +23 -10
  238. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  239. package/src/modes/theme/theme.ts +1 -1
  240. package/src/modes/types.ts +3 -1
  241. package/src/modes/utils/context-usage.ts +23 -7
  242. package/src/modes/utils/ui-helpers.ts +10 -5
  243. package/src/plan-mode/plan-files.ts +40 -0
  244. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  245. package/src/prompts/bench/cache-prefix.md +3 -0
  246. package/src/prompts/bench/cache-suffix.md +1 -0
  247. package/src/prompts/goals/guided-goal-system.md +24 -3
  248. package/src/prompts/system/custom-system-prompt.md +1 -1
  249. package/src/prompts/system/project-prompt.md +8 -1
  250. package/src/prompts/system/subagent-async-pending.md +6 -0
  251. package/src/prompts/system/system-prompt.md +1 -1
  252. package/src/prompts/system/workflow-notice.md +48 -40
  253. package/src/prompts/system/xdev-mount-notice.md +4 -0
  254. package/src/prompts/tools/task.md +12 -2
  255. package/src/prompts/tools/todo.md +3 -1
  256. package/src/registry/agent-lifecycle.ts +116 -46
  257. package/src/registry/agent-registry.ts +35 -8
  258. package/src/registry/persisted-agents.ts +26 -2
  259. package/src/sdk.ts +255 -139
  260. package/src/secrets/index.ts +127 -4
  261. package/src/secrets/obfuscator.ts +2328 -105
  262. package/src/session/acp-permission-gate.ts +165 -0
  263. package/src/session/agent-session-error-log.test.ts +1 -1
  264. package/src/session/agent-session-events.ts +66 -0
  265. package/src/session/agent-session-types.ts +334 -0
  266. package/src/session/agent-session.ts +1910 -12213
  267. package/src/session/artifacts.ts +6 -5
  268. package/src/session/async-job-delivery.ts +74 -0
  269. package/src/session/bash-runner.ts +326 -0
  270. package/src/session/blob-store.test.ts +56 -0
  271. package/src/session/blob-store.ts +18 -2
  272. package/src/session/checkpoint-entries.ts +81 -0
  273. package/src/session/eval-runner.ts +212 -0
  274. package/src/session/indexed-session-storage.ts +9 -2
  275. package/src/session/irc-bridge.ts +203 -0
  276. package/src/session/messages.ts +263 -1
  277. package/src/session/model-controls.ts +714 -0
  278. package/src/session/prewalk.ts +252 -0
  279. package/src/session/queued-messages.ts +93 -0
  280. package/src/session/redis-session-storage.ts +51 -11
  281. package/src/session/retry-fallback-chains.ts +455 -0
  282. package/src/session/role-models.ts +85 -0
  283. package/src/session/session-advisors.ts +1679 -0
  284. package/src/session/session-context.test.ts +224 -1
  285. package/src/session/session-context.ts +47 -5
  286. package/src/session/session-entries.ts +8 -0
  287. package/src/session/session-handoff.ts +308 -0
  288. package/src/session/session-history-format.ts +20 -9
  289. package/src/session/session-loader.ts +10 -51
  290. package/src/session/session-maintenance.ts +2983 -0
  291. package/src/session/session-manager.ts +390 -30
  292. package/src/session/session-memory.ts +222 -0
  293. package/src/session/session-provider-boundary.ts +307 -0
  294. package/src/session/session-stats.ts +293 -0
  295. package/src/session/session-tools.ts +943 -0
  296. package/src/session/session-workspace.ts +53 -0
  297. package/src/session/stream-guards.ts +417 -0
  298. package/src/session/todo-tracker.ts +380 -0
  299. package/src/session/ttsr-coordinator.ts +496 -0
  300. package/src/session/turn-recovery.ts +1629 -0
  301. package/src/slash-commands/builtin-registry.ts +92 -11
  302. package/src/slash-commands/helpers/usage-report.ts +25 -4
  303. package/src/system-prompt.ts +41 -17
  304. package/src/task/executor.ts +288 -70
  305. package/src/task/index.ts +148 -111
  306. package/src/task/output-manager.ts +25 -3
  307. package/src/task/persisted-revive.ts +4 -3
  308. package/src/task/structured-subagent.ts +4 -1
  309. package/src/task/types.ts +16 -0
  310. package/src/tools/approval.ts +55 -10
  311. package/src/tools/bash-interactive.ts +26 -0
  312. package/src/tools/bash-skill-urls.ts +28 -2
  313. package/src/tools/bash.ts +339 -123
  314. package/src/tools/browser/launch.ts +9 -1
  315. package/src/tools/browser/tab-supervisor.ts +8 -5
  316. package/src/tools/browser.ts +94 -47
  317. package/src/tools/hub/index.ts +1 -1
  318. package/src/tools/hub/jobs.ts +67 -8
  319. package/src/tools/image-gen.ts +20 -28
  320. package/src/tools/image-providers.ts +50 -0
  321. package/src/tools/index.ts +12 -0
  322. package/src/tools/path-utils.ts +49 -2
  323. package/src/tools/read.ts +156 -88
  324. package/src/tools/renderers.ts +7 -1
  325. package/src/tools/report-tool-issue.ts +79 -28
  326. package/src/tools/todo.ts +101 -11
  327. package/src/tools/vibe.ts +1 -2
  328. package/src/tools/write.ts +51 -1
  329. package/src/tools/xdev.ts +67 -3
  330. package/src/tts/streaming-player.ts +62 -4
  331. package/src/tts/vocalizer.ts +18 -1
  332. package/src/utils/jj.ts +125 -4
  333. package/src/utils/lang-from-path.ts +7 -0
  334. package/src/utils/title-generator.ts +137 -1
  335. package/src/vibe/runtime.ts +852 -72
  336. package/src/web/search/index.ts +3 -6
  337. package/src/web/search/provider.ts +31 -16
  338. package/src/web/search/providers/firecrawl.ts +46 -13
  339. package/src/web/search/providers/xai.ts +7 -1
  340. package/src/web/search/types.ts +8 -1
@@ -1,7 +1,8 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
3
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
3
4
  import * as snapcompact from "@oh-my-pi/snapcompact";
4
- import type { CompactionSummaryMessage } from "./messages";
5
+ import { type CompactionSummaryMessage, INTERRUPTED_THINKING_MESSAGE_TYPE } from "./messages";
5
6
  import { buildSessionContext, type StrippedToolCallsMarker } from "./session-context";
6
7
  import type { SessionEntry } from "./session-entries";
7
8
 
@@ -159,3 +160,225 @@ describe("buildSessionContext dangling toolCalls", () => {
159
160
  expect(context.messages.some(message => message.role === "assistant")).toBe(false);
160
161
  });
161
162
  });
163
+
164
+ const assistantUsage: AssistantMessage["usage"] = {
165
+ input: 0,
166
+ output: 0,
167
+ cacheRead: 0,
168
+ cacheWrite: 0,
169
+ totalTokens: 0,
170
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
171
+ };
172
+
173
+ function userEntry(id: string, parentId: string | null, content: string, messageTimestamp: number): SessionEntry {
174
+ return {
175
+ type: "message",
176
+ id,
177
+ parentId,
178
+ timestamp,
179
+ message: { role: "user", content, timestamp: messageTimestamp } as AgentMessage,
180
+ };
181
+ }
182
+
183
+ function assistantEntry(
184
+ id: string,
185
+ parentId: string | null,
186
+ stopReason: AssistantMessage["stopReason"],
187
+ text: string,
188
+ messageTimestamp: number,
189
+ ): SessionEntry {
190
+ return {
191
+ type: "message",
192
+ id,
193
+ parentId,
194
+ timestamp,
195
+ message: {
196
+ role: "assistant",
197
+ content: [{ type: "text", text }],
198
+ api: "anthropic-messages",
199
+ provider: "anthropic",
200
+ model: "claude-sonnet-4-5",
201
+ usage: assistantUsage,
202
+ stopReason,
203
+ timestamp: messageTimestamp,
204
+ } satisfies AssistantMessage,
205
+ };
206
+ }
207
+
208
+ function toolCallAssistantEntry(
209
+ id: string,
210
+ parentId: string | null,
211
+ stopReason: AssistantMessage["stopReason"],
212
+ toolCallId: string,
213
+ messageTimestamp: number,
214
+ ): SessionEntry {
215
+ return {
216
+ type: "message",
217
+ id,
218
+ parentId,
219
+ timestamp,
220
+ message: {
221
+ role: "assistant",
222
+ content: [{ type: "toolCall", id: toolCallId, name: "write", arguments: { path: "plan.md", content: "x" } }],
223
+ api: "anthropic-messages",
224
+ provider: "anthropic",
225
+ model: "claude-sonnet-4-5",
226
+ usage: assistantUsage,
227
+ stopReason,
228
+ timestamp: messageTimestamp,
229
+ } satisfies AssistantMessage,
230
+ };
231
+ }
232
+
233
+ function syntheticToolResultEntry(
234
+ id: string,
235
+ parentId: string | null,
236
+ toolCallId: string,
237
+ messageTimestamp: number,
238
+ ): SessionEntry {
239
+ return {
240
+ type: "message",
241
+ id,
242
+ parentId,
243
+ timestamp,
244
+ message: {
245
+ role: "toolResult",
246
+ toolCallId,
247
+ toolName: "write",
248
+ content: [
249
+ { type: "text", text: "Tool call was not executed because the provider stream ended with an error." },
250
+ ],
251
+ details: { __synthetic: true, source: "assistant_stop_error", executed: false },
252
+ isError: true,
253
+ timestamp: messageTimestamp,
254
+ } as AgentMessage,
255
+ };
256
+ }
257
+
258
+ function hiddenContinuityEntry(id: string, parentId: string | null): SessionEntry {
259
+ return {
260
+ type: "custom_message",
261
+ id,
262
+ parentId,
263
+ timestamp,
264
+ customType: INTERRUPTED_THINKING_MESSAGE_TYPE,
265
+ content: "preserved interrupted thinking",
266
+ display: false,
267
+ attribution: "agent",
268
+ };
269
+ }
270
+
271
+ function expectUserTail(messages: AgentMessage[], content: string): void {
272
+ const tail = messages.at(-1);
273
+ expect(tail?.role).toBe("user");
274
+ if (tail?.role !== "user") {
275
+ throw new Error(`Expected user tail, received ${tail?.role ?? "none"}`);
276
+ }
277
+ expect(tail.content).toBe(content);
278
+ }
279
+
280
+ describe("buildSessionContext failed replay tails", () => {
281
+ it("terminates on cyclic parent links and includes each reachable message once", () => {
282
+ const entries = [userEntry("A", "B", "from A", 1), userEntry("B", "A", "from B", 2)];
283
+
284
+ const context = buildSessionContext(entries, "A");
285
+
286
+ expect(context.messages.map(message => (message.role === "user" ? message.content : message.role))).toEqual([
287
+ "from B",
288
+ "from A",
289
+ ]);
290
+ });
291
+
292
+ it("omits a terminal aborted assistant from normal context", () => {
293
+ const context = buildSessionContext([
294
+ userEntry("user", null, "continue", 1),
295
+ assistantEntry("assistant", "user", "aborted", "partial unsafe replay", 2),
296
+ ]);
297
+
298
+ expect(context.messages.some(message => message.role === "assistant")).toBe(false);
299
+ expectUserTail(context.messages, "continue");
300
+ });
301
+
302
+ it("omits an earlier aborted assistant before a later user from normal context", () => {
303
+ const context = buildSessionContext([
304
+ userEntry("user-1", null, "first prompt", 1),
305
+ assistantEntry("assistant", "user-1", "aborted", "partial unsafe replay", 2),
306
+ userEntry("user-2", "assistant", "retry", 3),
307
+ ]);
308
+
309
+ expect(context.messages.some(message => message.role === "assistant")).toBe(false);
310
+ expectUserTail(context.messages, "retry");
311
+ });
312
+
313
+ it("preserves a terminal aborted assistant in transcript mode", () => {
314
+ const context = buildSessionContext(
315
+ [
316
+ userEntry("user", null, "continue", 1),
317
+ assistantEntry("assistant", "user", "aborted", "visible transcript error", 2),
318
+ ],
319
+ undefined,
320
+ undefined,
321
+ { transcript: true },
322
+ );
323
+
324
+ const assistant = context.messages.find(message => message.role === "assistant");
325
+ expect(assistant?.role).toBe("assistant");
326
+ if (assistant?.role !== "assistant") {
327
+ throw new Error(`Expected transcript assistant, received ${assistant?.role ?? "none"}`);
328
+ }
329
+ expect(assistant.stopReason).toBe("aborted");
330
+ expect(assistant.content).toEqual([{ type: "text", text: "visible transcript error" }]);
331
+ });
332
+
333
+ it("omits a terminal error assistant from normal context", () => {
334
+ const context = buildSessionContext([
335
+ userEntry("user", null, "retry with smaller input", 1),
336
+ assistantEntry("assistant", "user", "error", "provider rejected the request", 2),
337
+ ]);
338
+
339
+ expect(context.messages.some(message => message.role === "assistant")).toBe(false);
340
+ expectUserTail(context.messages, "retry with smaller input");
341
+ });
342
+
343
+ it("keeps an aborted assistant when hidden interrupted-thinking continuity follows it", () => {
344
+ const context = buildSessionContext([
345
+ userEntry("user", null, "keep reasoning continuity", 1),
346
+ assistantEntry("assistant", "user", "aborted", "partial answer before interrupt", 2),
347
+ hiddenContinuityEntry("continuity", "assistant"),
348
+ ]);
349
+
350
+ const assistant = context.messages.find(message => message.role === "assistant");
351
+ expect(assistant?.role).toBe("assistant");
352
+ if (assistant?.role !== "assistant") {
353
+ throw new Error(`Expected assistant before continuity, received ${assistant?.role ?? "none"}`);
354
+ }
355
+ expect(assistant.stopReason).toBe("aborted");
356
+ expect(context.messages.at(-1)?.role).toBe("custom");
357
+ });
358
+
359
+ it("drops synthetic tool results paired with a dropped failed tool-call turn", () => {
360
+ const context = buildSessionContext([
361
+ userEntry("user", null, "write the plan", 1),
362
+ toolCallAssistantEntry("assistant", "user", "error", "call-1", 2),
363
+ syntheticToolResultEntry("result", "assistant", "call-1", 3),
364
+ ]);
365
+
366
+ expect(context.messages.map(message => message.role)).toEqual(["user"]);
367
+ expectUserTail(context.messages, "write the plan");
368
+ });
369
+
370
+ it("keeps the failed tool-call turn and its result in transcript mode", () => {
371
+ const context = buildSessionContext(
372
+ [
373
+ userEntry("user", null, "write the plan", 1),
374
+ toolCallAssistantEntry("assistant", "user", "error", "call-1", 2),
375
+ syntheticToolResultEntry("result", "assistant", "call-1", 3),
376
+ ],
377
+ undefined,
378
+ undefined,
379
+ { transcript: true, keepDanglingToolCalls: true },
380
+ );
381
+
382
+ expect(context.messages.map(message => message.role)).toEqual(["user", "assistant", "toolResult"]);
383
+ });
384
+ });
@@ -5,6 +5,7 @@ import {
5
5
  createBranchSummaryMessage,
6
6
  createCompactionSummaryMessage,
7
7
  createCustomMessage,
8
+ INTERRUPTED_THINKING_MESSAGE_TYPE,
8
9
  isCustomMessageContent,
9
10
  normalizeCustomMessagePayload,
10
11
  } from "./messages";
@@ -17,6 +18,8 @@ import { type CompactionEntry, EPHEMERAL_MODEL_CHANGE_ROLE, type SessionEntry }
17
18
  const LEGACY_SNAPCOMPACT_FRAME_COUNT_GUARD = 16;
18
19
  const LEGACY_SNAPCOMPACT_ARCHIVE_TEXT_GUARD = 250_000;
19
20
  const LEGACY_SNAPCOMPACT_TRUNCATED_CHARS_GUARD = 1_000_000;
21
+ const SUPERSEDED_COMPACTION_SUMMARY = "[Superseded compaction summary elided after a newer compaction]";
22
+ const SUPERSEDED_COMPACTION_SHORT_SUMMARY = "Superseded compaction elided";
20
23
 
21
24
  function hasLegacySnapcompactFrames(archive: snapcompact.Archive): boolean {
22
25
  return archive.frames.some(frame => frame.font === undefined && frame.variant === undefined);
@@ -197,10 +200,13 @@ export function buildSessionContext(
197
200
  };
198
201
  }
199
202
 
200
- // Walk from leaf to root, collecting path
203
+ // Walk from leaf to root, collecting path. Corrupt/pre-fix files can contain
204
+ // parent cycles; stop at the first repeat so session load is bounded.
201
205
  const path: SessionEntry[] = [];
206
+ const seenPathIds = new Set<string>();
202
207
  let current: SessionEntry | undefined = leaf;
203
- while (current) {
208
+ while (current && !seenPathIds.has(current.id)) {
209
+ seenPathIds.add(current.id);
204
210
  path.push(current);
205
211
  current = current.parentId ? byId.get(current.parentId) : undefined;
206
212
  }
@@ -340,13 +346,14 @@ export function buildSessionContext(
340
346
  for (const entry of path) {
341
347
  handleEntryResetTracking(entry);
342
348
  if (entry.type === "compaction") {
343
- const snapcompactArchive = snapcompact.getPreservedArchive(entry.preserveData);
349
+ const active = entry.id === compaction?.id;
350
+ const snapcompactArchive = active ? snapcompact.getPreservedArchive(entry.preserveData) : undefined;
344
351
  pushMessage(
345
352
  createCompactionSummaryMessage(
346
- entry.summary,
353
+ active ? entry.summary : SUPERSEDED_COMPACTION_SUMMARY,
347
354
  entry.tokensBefore,
348
355
  entry.timestamp,
349
- entry.shortSummary,
356
+ active ? entry.shortSummary : SUPERSEDED_COMPACTION_SHORT_SUMMARY,
350
357
  undefined,
351
358
  undefined,
352
359
  snapcompactHistoryBlocksForContext(snapcompactArchive, options),
@@ -498,6 +505,41 @@ export function buildSessionContext(
498
505
  }
499
506
  }
500
507
 
508
+ // Error/abort assistant turns are transcript events, not safe assistant
509
+ // turns to replay into the next provider request. Drop them even when a
510
+ // later user message follows through non-context entries (`session_exit`,
511
+ // labels, etc.); otherwise a resumed session replays a dead partial turn
512
+ // and can spend minutes reprocessing old context before the new prompt.
513
+ // Keep the interrupted-thinking continuity pair: convertToLlm strips the
514
+ // unsafe trailing thinking from that assistant and sends the hidden
515
+ // continuity note instead.
516
+ if (!options?.transcript) {
517
+ for (let i = messages.length - 1; i >= 0; i--) {
518
+ const message = messages[i];
519
+ if (message?.role !== "assistant") continue;
520
+ if (message.stopReason !== "aborted" && message.stopReason !== "error") continue;
521
+ const next = messages[i + 1];
522
+ if (next?.role === "custom" && next.customType === INTERRUPTED_THINKING_MESSAGE_TYPE) continue;
523
+ // A failed turn that emitted tool calls persists paired synthetic
524
+ // tool_result placeholders after it. Dropping only the assistant would
525
+ // strand those results with no preceding tool_use — a shape providers
526
+ // reject — so remove the paired results alongside the turn.
527
+ const droppedToolCallIds = new Set<string>();
528
+ for (const block of message.content) {
529
+ if (block.type === "toolCall") droppedToolCallIds.add(block.id);
530
+ }
531
+ messages.splice(i, 1);
532
+ if (droppedToolCallIds.size > 0) {
533
+ for (let j = messages.length - 1; j >= i; j--) {
534
+ const candidate = messages[j];
535
+ if (candidate?.role === "toolResult" && droppedToolCallIds.has(candidate.toolCallId)) {
536
+ messages.splice(j, 1);
537
+ }
538
+ }
539
+ }
540
+ }
541
+ }
542
+
501
543
  return {
502
544
  messages,
503
545
  cacheMissExplainedAt: options?.transcript ? cacheMissExplainedAt : undefined,
@@ -32,6 +32,12 @@ export interface SessionHeader {
32
32
  titleSource?: SessionTitleSource;
33
33
  timestamp: string;
34
34
  cwd: string;
35
+ /**
36
+ * Additional workspace directories beyond `cwd` (multi-root workspace).
37
+ * Absolute, normalized, deduplicated. Absent on legacy single-cwd sessions.
38
+ * See {@link SessionWorkspace} in `./session-workspace`.
39
+ */
40
+ additionalDirectories?: string[];
35
41
  parentSession?: string;
36
42
  /** Provider prompt-cache identity inherited by exact-route full forks. */
37
43
  providerPromptCacheKey?: string;
@@ -43,6 +49,8 @@ export interface NewSessionOptions {
43
49
  providerPromptCacheKey?: string;
44
50
  /** Skip flushing the current session and delete it instead of saving. */
45
51
  drop?: boolean;
52
+ /** Additional workspace directories to seed on the new session. */
53
+ additionalDirectories?: string[];
46
54
  }
47
55
 
48
56
  export interface SessionEntryBase {
@@ -0,0 +1,308 @@
1
+ /** Handoff generation and session transition orchestration. */
2
+
3
+ import * as path from "node:path";
4
+ import {
5
+ type Agent,
6
+ type AgentMessage,
7
+ resolveTelemetry,
8
+ type StreamFn,
9
+ type ThinkingLevel,
10
+ } from "@oh-my-pi/pi-agent-core";
11
+ import { generateHandoffFromContext, renderHandoffPrompt } from "@oh-my-pi/pi-agent-core/compaction";
12
+ import type { Message, Model, ServiceTier, SimpleStreamOptions } from "@oh-my-pi/pi-ai";
13
+ import { logger, Snowflake } from "@oh-my-pi/pi-utils";
14
+ import type { ModelRegistry } from "../config/model-registry";
15
+ import type { Settings } from "../config/settings";
16
+ import type { ExtensionRunner, SessionBeforeSwitchResult } from "../extensibility/extensions";
17
+ import { obfuscateProviderContext, type SecretObfuscator } from "../secrets/obfuscator";
18
+ import type { HandoffResult, SessionHandoffOptions } from "./agent-session-types";
19
+ import type { BashSessionTransition } from "./bash-runner";
20
+ import type { SessionContext } from "./session-context";
21
+ import type { SessionManager } from "./session-manager";
22
+
23
+ function createHandoffContext(document: string): string {
24
+ return `<handoff-context>\n${document}\n</handoff-context>\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.`;
25
+ }
26
+
27
+ function createHandoffFileName(date = new Date()): string {
28
+ const fileTimestamp = date.toISOString().replace(/[:.]/g, "-");
29
+ return `handoff-${fileTimestamp}.md`;
30
+ }
31
+
32
+ /** Capabilities borrowed from the owning AgentSession. */
33
+ export interface SessionHandoffHost {
34
+ agent: Agent;
35
+ sessionManager: SessionManager;
36
+ settings: Settings;
37
+ modelRegistry: ModelRegistry;
38
+ extensionRunner: ExtensionRunner | undefined;
39
+ sideStreamFn: StreamFn;
40
+ obfuscator: SecretObfuscator | undefined;
41
+ model(): Model | undefined;
42
+ thinkingLevel(): ThinkingLevel | undefined;
43
+ sessionId(): string;
44
+ sessionFile(): string | undefined;
45
+ baseSystemPrompt(): string[];
46
+ assertVibeSessionTransitionAllowed(action: string): void;
47
+ setSkipPostTurnMaintenance(timestamp: number | undefined): void;
48
+ obfuscateTextForProvider(text: string | undefined): string | undefined;
49
+ deobfuscateFromProvider(text: string): string;
50
+ convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise<Message[]>;
51
+ prepareSimpleStreamOptions(options: SimpleStreamOptions, provider?: string): SimpleStreamOptions;
52
+ effectiveServiceTier(model: Model | undefined): ServiceTier | undefined;
53
+ flushPendingBash(): Promise<void>;
54
+ beginBashSessionTransition(): BashSessionTransition;
55
+ markBashSessionTransition(transition: BashSessionTransition): void;
56
+ finishBashSessionTransition(transition: BashSessionTransition, success: boolean): void;
57
+ cancelOwnAsyncJobs(): void;
58
+ clearCheckpointRuntimeState(): void;
59
+ clearSessionScopedToolState(): void;
60
+ clearFreshProviderSessionId(): void;
61
+ syncAgentSessionId(): void;
62
+ rekeyMemoryForCurrentSessionId(): void;
63
+ resetMemoryContextForNewTranscript(): Promise<void>;
64
+ clearPendingNextTurnMessages(): void;
65
+ resetTodoCycle(): void;
66
+ buildDisplaySessionContext(): SessionContext;
67
+ resetAdvisorRuntimes(): void;
68
+ syncTodoPhasesFromBranch(): void;
69
+ }
70
+
71
+ /** Generates handoff documents and owns the handoff session transition. */
72
+ export class SessionHandoff {
73
+ #handoffAbortController: AbortController | undefined;
74
+ readonly #host: SessionHandoffHost;
75
+
76
+ constructor(host: SessionHandoffHost) {
77
+ this.#host = host;
78
+ }
79
+ /**
80
+ * Cancel in-progress handoff generation.
81
+ */
82
+ abortHandoff(): void {
83
+ this.#handoffAbortController?.abort();
84
+ }
85
+
86
+ /**
87
+ * Check if handoff generation is in progress.
88
+ */
89
+ get isGeneratingHandoff(): boolean {
90
+ return this.#handoffAbortController !== undefined;
91
+ }
92
+
93
+ /**
94
+ * Generate a handoff document with a oneshot LLM call, then start a new session with it.
95
+ *
96
+ * @param customInstructions Optional focus for the handoff document
97
+ * @param options Handoff execution options
98
+ * @returns The handoff document text, or undefined if cancelled/failed
99
+ */
100
+ async handoff(customInstructions?: string, options?: SessionHandoffOptions): Promise<HandoffResult | undefined> {
101
+ this.#host.assertVibeSessionTransitionAllowed("handoff to a new session");
102
+ const entries = this.#host.sessionManager.getBranch();
103
+ const messageCount = entries.filter(e => e.type === "message").length;
104
+
105
+ if (messageCount < 2) {
106
+ throw new Error("Nothing to hand off (no messages yet)");
107
+ }
108
+
109
+ this.#host.setSkipPostTurnMaintenance(undefined);
110
+
111
+ this.#handoffAbortController = new AbortController();
112
+ const handoffAbortController = this.#handoffAbortController;
113
+ const handoffSignal = handoffAbortController.signal;
114
+ const sourceSignal = options?.signal;
115
+ const onSourceAbort = () => {
116
+ if (!handoffSignal.aborted) {
117
+ handoffAbortController.abort();
118
+ }
119
+ };
120
+ if (sourceSignal) {
121
+ sourceSignal.addEventListener("abort", onSourceAbort, { once: true });
122
+ if (sourceSignal.aborted) {
123
+ onSourceAbort();
124
+ }
125
+ }
126
+
127
+ try {
128
+ if (handoffSignal.aborted) {
129
+ throw new Error("Handoff cancelled");
130
+ }
131
+
132
+ const model = this.#host.model();
133
+ if (!model) {
134
+ throw new Error("No model selected for handoff");
135
+ }
136
+ const apiKey = await this.#host.modelRegistry.getApiKey(model, this.#host.sessionId());
137
+ if (!apiKey) {
138
+ throw new Error(`No API key for ${model.provider}`);
139
+ }
140
+
141
+ // Build the handoff request through the SAME pipeline a live turn uses
142
+ // (`runEphemeralTurn` / `/btw` share it) so the oneshot reads the
143
+ // provider prompt cache the main turn populated instead of cold-missing
144
+ // the whole prefix: identical system prompt, normalized tools, and
145
+ // transform-/obfuscation-matched message history via
146
+ // `convertMessagesToLlm` + `buildSideRequestContext`, plus the live turn's
147
+ // effective provider cache key with a unique side `sessionId` so
148
+ // OpenAI/Codex append-only state never mixes with the live turn.
149
+ const cacheSessionId = this.#host.sessionId();
150
+ // The loop sends `promptCacheKey` (providerPromptCacheKey) and falls back to
151
+ // the provider session id; providers route on `promptCacheKey ?? sessionId`.
152
+ // Both can diverge from this.#host.sessionId() (tan/subagent/shared sessions), so
153
+ // mirror exactly what the live turn populated the cache under.
154
+ const handoffPromptCacheKey = this.#host.agent.promptCacheKey ?? this.#host.agent.sessionId;
155
+ const handoffPromptText = renderHandoffPrompt(this.#host.obfuscateTextForProvider(customInstructions));
156
+ const handoffSnapshot: AgentMessage[] = [
157
+ ...this.#host.agent.state.messages,
158
+ {
159
+ role: "user",
160
+ content: [{ type: "text", text: handoffPromptText }],
161
+ attribution: "agent",
162
+ timestamp: Date.now(),
163
+ },
164
+ ];
165
+ const handoffLlmMessages = await this.#host.convertMessagesToLlm(handoffSnapshot, handoffSignal);
166
+ // Base system prompt, not a per-turn `before_agent_start` hook override —
167
+ // the handoff seeds a fresh session and must not carry prompt-specific
168
+ // hook state. Matches the prompt the old handoff path sent.
169
+ const handoffContext = await this.#host.agent.buildSideRequestContext(
170
+ handoffLlmMessages,
171
+ this.#host.baseSystemPrompt(),
172
+ );
173
+ const handoffStreamOptions = this.#host.prepareSimpleStreamOptions(
174
+ {
175
+ apiKey: this.#host.modelRegistry.resolver(model, cacheSessionId),
176
+ sessionId: `${cacheSessionId}:side:${Snowflake.next()}`,
177
+ promptCacheKey: handoffPromptCacheKey,
178
+ preferWebsockets: false,
179
+ serviceTier: this.#host.effectiveServiceTier(model),
180
+ hideThinkingSummary: this.#host.agent.hideThinkingSummary,
181
+ initiatorOverride: "agent",
182
+ signal: handoffSignal,
183
+ },
184
+ model.provider,
185
+ );
186
+ const rawHandoffText = await generateHandoffFromContext(
187
+ obfuscateProviderContext(this.#host.obfuscator, handoffContext),
188
+ model,
189
+ {
190
+ streamOptions: handoffStreamOptions,
191
+ completeImpl: async (requestModel, requestContext, requestOptions) => {
192
+ const stream = await this.#host.sideStreamFn(requestModel, requestContext, requestOptions);
193
+ return stream.result();
194
+ },
195
+ telemetry: resolveTelemetry(this.#host.agent.telemetry, this.#host.sessionId()),
196
+ // Honor the user's /model thinking selection on the handoff path.
197
+ // Clamped per-model inside generateHandoffFromContext via
198
+ // resolveCompactionEffort so unsupported-effort models don't trip
199
+ // requireSupportedEffort.
200
+ thinkingLevel: this.#host.thinkingLevel(),
201
+ },
202
+ );
203
+ const handoffText = this.#host.deobfuscateFromProvider(rawHandoffText);
204
+
205
+ if (handoffSignal.aborted) {
206
+ throw new Error("Handoff cancelled");
207
+ }
208
+ if (!handoffText) {
209
+ return undefined;
210
+ }
211
+
212
+ // Start a new session
213
+ const previousSessionFile = this.#host.sessionFile();
214
+ if (this.#host.extensionRunner?.hasHandlers("session_before_switch")) {
215
+ const result = (await this.#host.extensionRunner.emit({
216
+ type: "session_before_switch",
217
+ reason: "handoff",
218
+ })) as SessionBeforeSwitchResult | undefined;
219
+
220
+ if (result?.cancel) {
221
+ options?.onSwitchCancelled?.();
222
+ return undefined;
223
+ }
224
+ }
225
+ await this.#host.flushPendingBash();
226
+ await this.#host.sessionManager.flush();
227
+ const bashTransition = this.#host.beginBashSessionTransition();
228
+ this.#host.cancelOwnAsyncJobs();
229
+ let sessionTransitioned = false;
230
+ try {
231
+ await this.#host.sessionManager.newSession(
232
+ previousSessionFile ? { parentSession: previousSessionFile } : undefined,
233
+ );
234
+ this.#host.markBashSessionTransition(bashTransition);
235
+ sessionTransitioned = true;
236
+ } finally {
237
+ this.#host.finishBashSessionTransition(bashTransition, sessionTransitioned);
238
+ }
239
+
240
+ this.#host.clearSessionScopedToolState();
241
+
242
+ this.#host.clearCheckpointRuntimeState();
243
+ // agent.reset() clears the core steering/follow-up queues. Preserve any queued
244
+ // steers/follow-ups (RPC/SDK steer()/followUp() issued during the handoff, or a
245
+ // pre-loader TUI steer) so they survive into the post-handoff session instead of
246
+ // being silently dropped. Capture is synchronous immediately before reset and
247
+ // restore is synchronous immediately after — no await gap — so a steer arriving
248
+ // later (during ensureOnDisk/Bun.write below) appends to the restored queue
249
+ // rather than being clobbered.
250
+ const preservedSteering = this.#host.agent.peekSteeringQueue().slice();
251
+ const preservedFollowUp = this.#host.agent.peekFollowUpQueue().slice();
252
+ this.#host.agent.reset();
253
+ this.#host.agent.replaceQueues(preservedSteering, preservedFollowUp);
254
+ this.#host.clearFreshProviderSessionId();
255
+ this.#host.syncAgentSessionId();
256
+ this.#host.rekeyMemoryForCurrentSessionId();
257
+ await this.#host.resetMemoryContextForNewTranscript();
258
+ this.#host.clearPendingNextTurnMessages();
259
+ this.#host.resetTodoCycle();
260
+
261
+ // Inject the handoff document as a custom message
262
+ const handoffContent = createHandoffContext(handoffText);
263
+ this.#host.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true, undefined, "agent");
264
+ await this.#host.sessionManager.ensureOnDisk();
265
+ let savedPath: string | undefined;
266
+ if (options?.autoTriggered && this.#host.settings.get("compaction.handoffSaveToDisk")) {
267
+ const artifactsDir = this.#host.sessionManager.getArtifactsDir();
268
+ if (artifactsDir) {
269
+ const handoffFilePath = path.join(artifactsDir, createHandoffFileName());
270
+ try {
271
+ await Bun.write(handoffFilePath, `${handoffText}\n`);
272
+ savedPath = handoffFilePath;
273
+ } catch (error) {
274
+ logger.warn("Failed to save handoff document to disk", {
275
+ path: handoffFilePath,
276
+ error: error instanceof Error ? error.message : String(error),
277
+ });
278
+ }
279
+ } else {
280
+ logger.debug("Skipping handoff document save because session is not persisted");
281
+ }
282
+ }
283
+
284
+ // Rebuild agent messages from session
285
+ const sessionContext = this.#host.buildDisplaySessionContext();
286
+ this.#host.agent.replaceMessages(sessionContext.messages);
287
+ this.#host.resetAdvisorRuntimes();
288
+ this.#host.syncTodoPhasesFromBranch();
289
+ if (this.#host.extensionRunner) {
290
+ await this.#host.extensionRunner.emit({
291
+ type: "session_switch",
292
+ reason: "handoff",
293
+ previousSessionFile,
294
+ });
295
+ }
296
+
297
+ return { document: handoffText, savedPath };
298
+ } catch (error) {
299
+ if (handoffSignal.aborted || (error instanceof Error && error.name === "AbortError")) {
300
+ throw new Error("Handoff cancelled");
301
+ }
302
+ throw error;
303
+ } finally {
304
+ sourceSignal?.removeEventListener("abort", onSourceAbort);
305
+ this.#handoffAbortController = undefined;
306
+ }
307
+ }
308
+ }