@oh-my-pi/pi-coding-agent 17.1.7 → 17.2.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 (246) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/dist/{CHANGELOG-5k4dq4g6.md → CHANGELOG-be1f2t8h.md} +127 -0
  3. package/dist/cli.js +9554 -12379
  4. package/dist/template-8vdv6xb4.js +1671 -0
  5. package/dist/template-f8wx9vfn.css +1355 -0
  6. package/dist/template-qat058wr.html +55 -0
  7. package/dist/tool-views.generated-jdfmzwmn.js +35 -0
  8. package/dist/types/advisor/advise-tool.d.ts +0 -7
  9. package/dist/types/advisor/runtime.d.ts +8 -11
  10. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  11. package/dist/types/async/job-manager.d.ts +11 -0
  12. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  13. package/dist/types/cleanse/agent.d.ts +19 -0
  14. package/dist/types/cleanse/balance.d.ts +7 -0
  15. package/dist/types/cleanse/checkers.d.ts +13 -0
  16. package/dist/types/cleanse/index.d.ts +16 -0
  17. package/dist/types/cleanse/loop.d.ts +16 -0
  18. package/dist/types/cleanse/parsers.d.ts +13 -0
  19. package/dist/types/cleanse/progress.d.ts +14 -0
  20. package/dist/types/cleanse/types.d.ts +64 -0
  21. package/dist/types/cli/args.d.ts +2 -0
  22. package/dist/types/cli/session-picker.d.ts +14 -7
  23. package/dist/types/collab/guest.d.ts +21 -2
  24. package/dist/types/commands/cleanse.d.ts +23 -0
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/config/settings-schema.d.ts +63 -8
  27. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  28. package/dist/types/cursor.d.ts +193 -8
  29. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  30. package/dist/types/edit/hashline/diff.d.ts +9 -1
  31. package/dist/types/edit/index.d.ts +8 -1
  32. package/dist/types/edit/streaming.d.ts +8 -1
  33. package/dist/types/export/html/index.d.ts +2 -0
  34. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  35. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
  38. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  39. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  40. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  41. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  42. package/dist/types/lsp/client.d.ts +2 -0
  43. package/dist/types/main.d.ts +2 -0
  44. package/dist/types/mcp/manager.d.ts +33 -4
  45. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  46. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  47. package/dist/types/modes/components/session-selector.d.ts +6 -0
  48. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  49. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  50. package/dist/types/modes/components/usage-row.d.ts +2 -0
  51. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  52. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  53. package/dist/types/modes/interactive-mode.d.ts +8 -5
  54. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  55. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  56. package/dist/types/modes/theme/theme.d.ts +12 -1
  57. package/dist/types/modes/types.d.ts +2 -1
  58. package/dist/types/sdk.d.ts +1 -0
  59. package/dist/types/session/agent-session-events.d.ts +2 -0
  60. package/dist/types/session/agent-session-types.d.ts +36 -1
  61. package/dist/types/session/agent-session.d.ts +4 -2
  62. package/dist/types/session/async-job-delivery.d.ts +8 -0
  63. package/dist/types/session/claude-session-store.d.ts +13 -0
  64. package/dist/types/session/codex-session-store.d.ts +14 -0
  65. package/dist/types/session/credential-pin.d.ts +58 -0
  66. package/dist/types/session/foreign-session-import.d.ts +15 -0
  67. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  68. package/dist/types/session/foreign-session-store.d.ts +23 -0
  69. package/dist/types/session/model-controls.d.ts +3 -3
  70. package/dist/types/session/session-advisors.d.ts +47 -2
  71. package/dist/types/session/session-entries.d.ts +19 -1
  72. package/dist/types/session/session-handoff.d.ts +4 -1
  73. package/dist/types/session/session-loader.d.ts +2 -0
  74. package/dist/types/session/session-maintenance.d.ts +1 -1
  75. package/dist/types/session/session-manager.d.ts +21 -0
  76. package/dist/types/session/session-tools.d.ts +25 -1
  77. package/dist/types/session/turn-recovery.d.ts +10 -4
  78. package/dist/types/system-prompt.d.ts +2 -1
  79. package/dist/types/task/executor.d.ts +11 -2
  80. package/dist/types/task/isolation-ownership.d.ts +34 -0
  81. package/dist/types/task/render.d.ts +7 -0
  82. package/dist/types/thinking.d.ts +18 -6
  83. package/dist/types/tools/acp-bridge.d.ts +28 -5
  84. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
  85. package/dist/types/tools/browser/tab-worker.d.ts +1 -4
  86. package/dist/types/tools/grep.d.ts +17 -1
  87. package/dist/types/tools/hub/launch.d.ts +5 -1
  88. package/dist/types/tools/index.d.ts +4 -1
  89. package/dist/types/tools/path-utils.d.ts +19 -0
  90. package/dist/types/tools/read.d.ts +2 -0
  91. package/dist/types/tools/xdev.d.ts +2 -1
  92. package/dist/types/tui/output-block.d.ts +5 -5
  93. package/dist/types/utils/changelog.d.ts +19 -0
  94. package/dist/types/utils/git.d.ts +10 -0
  95. package/dist/types/utils/jj.d.ts +11 -4
  96. package/dist/types/web/search/types.d.ts +2 -2
  97. package/package.json +16 -12
  98. package/scripts/bundle-dist.ts +3 -1
  99. package/src/advisor/advise-tool.ts +0 -11
  100. package/src/advisor/runtime.ts +138 -24
  101. package/src/advisor/transcript-recorder.ts +56 -0
  102. package/src/async/job-manager.ts +30 -7
  103. package/src/auto-thinking/classifier.ts +48 -10
  104. package/src/cleanse/agent.ts +226 -0
  105. package/src/cleanse/balance.ts +79 -0
  106. package/src/cleanse/checkers.ts +996 -0
  107. package/src/cleanse/index.ts +190 -0
  108. package/src/cleanse/loop.ts +51 -0
  109. package/src/cleanse/parsers.ts +726 -0
  110. package/src/cleanse/progress.ts +50 -0
  111. package/src/cleanse/prompts/assignment.md +47 -0
  112. package/src/cleanse/types.ts +72 -0
  113. package/src/cli/args.ts +6 -0
  114. package/src/cli/flag-tables.ts +2 -0
  115. package/src/cli/gallery-fixtures/fs.ts +25 -0
  116. package/src/cli/session-picker.ts +34 -17
  117. package/src/cli/ttsr-cli.ts +19 -1
  118. package/src/cli/update-cli.ts +3 -0
  119. package/src/cli/usage-cli.ts +29 -4
  120. package/src/cli/worktree-cli.ts +28 -11
  121. package/src/cli-commands.ts +1 -0
  122. package/src/cli.ts +11 -1
  123. package/src/collab/guest.ts +37 -4
  124. package/src/collab/host.ts +1 -0
  125. package/src/commands/cleanse.ts +45 -0
  126. package/src/commands/launch.ts +6 -0
  127. package/src/config/model-registry.ts +103 -25
  128. package/src/config/settings-schema.ts +65 -6
  129. package/src/config/settings.ts +25 -0
  130. package/src/cursor-bridge-tools.ts +81 -0
  131. package/src/cursor.ts +463 -12
  132. package/src/discovery/claude-plugins.ts +144 -34
  133. package/src/edit/edit-clipboard.ts +23 -0
  134. package/src/edit/hashline/diff.ts +49 -10
  135. package/src/edit/hashline/execute.ts +38 -6
  136. package/src/edit/hashline/filesystem.ts +27 -3
  137. package/src/edit/index.ts +12 -2
  138. package/src/edit/renderer.ts +4 -4
  139. package/src/edit/streaming.ts +17 -2
  140. package/src/export/html/index.ts +17 -10
  141. package/src/export/html/template.js +1 -1
  142. package/src/extensibility/extensions/runner.ts +70 -0
  143. package/src/extensibility/extensions/types.ts +30 -0
  144. package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
  145. package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
  146. package/src/internal-urls/local-protocol.ts +3 -2
  147. package/src/internal-urls/mcp-protocol.ts +4 -1
  148. package/src/launch/broker.ts +14 -4
  149. package/src/launch/terminal-output-worker-client.ts +53 -0
  150. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  151. package/src/launch/terminal-output-worker.ts +23 -0
  152. package/src/lsp/client.ts +18 -3
  153. package/src/lsp/index.ts +4 -0
  154. package/src/lsp/render.ts +1 -1
  155. package/src/main.ts +127 -56
  156. package/src/mcp/manager.ts +120 -18
  157. package/src/modes/acp/acp-agent.ts +27 -18
  158. package/src/modes/acp/acp-event-mapper.ts +38 -4
  159. package/src/modes/components/chat-transcript-builder.ts +30 -11
  160. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  161. package/src/modes/components/read-tool-group.ts +159 -17
  162. package/src/modes/components/session-selector.ts +19 -3
  163. package/src/modes/components/settings-selector.ts +9 -1
  164. package/src/modes/components/status-line/component.ts +381 -54
  165. package/src/modes/components/tool-execution.ts +78 -31
  166. package/src/modes/components/usage-row.ts +10 -5
  167. package/src/modes/controllers/event-controller.ts +26 -4
  168. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  169. package/src/modes/controllers/selector-controller.ts +94 -36
  170. package/src/modes/controllers/tan-command-controller.ts +14 -0
  171. package/src/modes/interactive-mode.ts +67 -64
  172. package/src/modes/prompt-action-autocomplete.ts +5 -3
  173. package/src/modes/rpc/rpc-client.ts +18 -0
  174. package/src/modes/rpc/rpc-mode.ts +16 -1
  175. package/src/modes/rpc/rpc-types.ts +11 -0
  176. package/src/modes/theme/theme.ts +151 -6
  177. package/src/modes/types.ts +2 -1
  178. package/src/modes/utils/ui-helpers.ts +38 -21
  179. package/src/prompts/goals/guided-goal-interview.md +41 -6
  180. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  181. package/src/prompts/system/plan-mode-active.md +2 -2
  182. package/src/prompts/system/system-prompt.md +3 -0
  183. package/src/prompts/system/vibe-mode-active.md +4 -1
  184. package/src/prompts/system/xdev-mount-notice.md +1 -1
  185. package/src/prompts/tools/bash.md +15 -17
  186. package/src/prompts/tools/browser.md +1 -0
  187. package/src/prompts/tools/checkpoint.md +1 -1
  188. package/src/prompts/tools/glob.md +7 -6
  189. package/src/prompts/tools/goal.md +1 -1
  190. package/src/prompts/tools/grep.md +7 -6
  191. package/src/sdk.ts +178 -15
  192. package/src/session/agent-session-events.ts +1 -0
  193. package/src/session/agent-session-types.ts +40 -1
  194. package/src/session/agent-session.ts +330 -179
  195. package/src/session/async-job-delivery.ts +8 -0
  196. package/src/session/claude-session-store.ts +426 -0
  197. package/src/session/codex-session-store.ts +673 -0
  198. package/src/session/credential-pin.ts +93 -0
  199. package/src/session/foreign-session-import.ts +52 -0
  200. package/src/session/foreign-session-jsonl.ts +29 -0
  201. package/src/session/foreign-session-store.ts +26 -0
  202. package/src/session/model-controls.ts +15 -7
  203. package/src/session/session-advisors.ts +142 -23
  204. package/src/session/session-entries.ts +21 -1
  205. package/src/session/session-handoff.ts +20 -3
  206. package/src/session/session-history-format.ts +31 -6
  207. package/src/session/session-loader.ts +25 -9
  208. package/src/session/session-maintenance.ts +59 -11
  209. package/src/session/session-manager.ts +61 -0
  210. package/src/session/session-tools.ts +107 -5
  211. package/src/session/settings-stream-fn.ts +1 -0
  212. package/src/session/turn-recovery.ts +97 -51
  213. package/src/slash-commands/builtin-registry.ts +136 -5
  214. package/src/system-prompt.ts +3 -2
  215. package/src/task/executor.ts +17 -4
  216. package/src/task/isolation-ownership.ts +106 -0
  217. package/src/task/render.ts +20 -4
  218. package/src/task/worktree.ts +8 -0
  219. package/src/thinking.ts +29 -10
  220. package/src/tools/acp-bridge.ts +52 -8
  221. package/src/tools/ask.ts +3 -3
  222. package/src/tools/ast-edit.ts +9 -2
  223. package/src/tools/browser/cmux/cmux-tab.ts +9 -14
  224. package/src/tools/browser/tab-worker.ts +12 -35
  225. package/src/tools/browser.ts +7 -2
  226. package/src/tools/checkpoint.ts +0 -13
  227. package/src/tools/gh-renderer.ts +3 -3
  228. package/src/tools/grep.ts +64 -8
  229. package/src/tools/hub/launch.ts +10 -15
  230. package/src/tools/index.ts +38 -5
  231. package/src/tools/path-utils.ts +88 -0
  232. package/src/tools/read.ts +3 -0
  233. package/src/tools/write.ts +44 -3
  234. package/src/tools/xdev.ts +39 -14
  235. package/src/tui/code-cell.ts +4 -4
  236. package/src/tui/output-block.ts +25 -8
  237. package/src/utils/changelog.ts +144 -4
  238. package/src/utils/git.ts +61 -14
  239. package/src/utils/jj.ts +19 -13
  240. package/src/utils/shell-snapshot-fn-env.sh +5 -2
  241. package/src/web/search/providers/codex.ts +34 -0
  242. package/src/web/search/render.ts +2 -2
  243. package/src/web/search/types.ts +1 -1
  244. package/dist/types/goals/guided-setup.d.ts +0 -30
  245. package/src/goals/guided-setup.ts +0 -171
  246. package/src/prompts/goals/guided-goal-system.md +0 -33
@@ -19,7 +19,7 @@ import * as path from "node:path";
19
19
  import { scheduler } from "node:timers/promises";
20
20
  import { isPromise } from "node:util/types";
21
21
 
22
- import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
22
+ import type { Clipboard, InMemorySnapshotStore } from "@oh-my-pi/hashline";
23
23
  import {
24
24
  type AfterToolCallContext,
25
25
  type AfterToolCallResult,
@@ -95,7 +95,7 @@ import {
95
95
  stringProperty,
96
96
  withTimeout,
97
97
  } from "@oh-my-pi/pi-utils";
98
- import type { AdvisorConfig, AdvisorRuntimeStatus } from "../advisor";
98
+ import { type AdvisorConfig, type AdvisorRuntimeStatus, loadAdvisorTranscriptCosts } from "../advisor";
99
99
  import { type AsyncJob, AsyncJobManager } from "../async";
100
100
  import { shouldEnableAppendOnlyContext } from "../config/append-only-context-mode";
101
101
  import type { ModelRegistry } from "../config/model-registry";
@@ -249,6 +249,7 @@ import {
249
249
  shouldEvaluateCodexAutoRedeem,
250
250
  shouldPromptCodexAutoRedeem,
251
251
  } from "./codex-auto-reset";
252
+ import { recordCredentialPin, seedCredentialPins } from "./credential-pin";
252
253
  import { EvalRunner, type EvalRunnerHost } from "./eval-runner";
253
254
  import {
254
255
  collectPendingToolCalls,
@@ -398,6 +399,9 @@ type SetSessionNameWithTrigger = (
398
399
  trigger?: SessionNameTrigger,
399
400
  ) => Promise<boolean>;
400
401
 
402
+ const kPersistedSessionEntryId = Symbol("persistedSessionEntryId");
403
+ type PersistedAssistantMessage = AssistantMessage & { [kPersistedSessionEntryId]?: string };
404
+
401
405
  export class AgentSession {
402
406
  readonly agent: Agent;
403
407
  readonly sessionManager: SessionManager;
@@ -406,6 +410,8 @@ export class AgentSession {
406
410
  getXdevToolEntries: () => Array<{ name: string; summary: string }>;
407
411
  readonly yieldQueue: YieldQueue;
408
412
  fileSnapshotStore?: InMemorySnapshotStore;
413
+ /** Per-session `CUT`/`PASTE` clipboard register shared across edit calls. */
414
+ editClipboard?: Clipboard;
409
415
 
410
416
  #powerAssertion: MacOSPowerAssertion | undefined;
411
417
 
@@ -487,6 +493,13 @@ export class AgentSession {
487
493
  readonly #asyncJobManager: AsyncJobManager | undefined;
488
494
  /** Clears this session's owner delivery sink registration; set when a manager + agent id exist. */
489
495
  #unregisterAsyncDeliverySink: (() => void) | undefined;
496
+ /**
497
+ * Async-delivery generation, bumped on every session transition that evicts
498
+ * this owner's jobs (see {@link AgentSession.#cancelOwnAsyncJobs}). Stamped
499
+ * onto each queued async-result follow-up so a delivery formatted or drained
500
+ * across a `/new` is dropped regardless of job-id reuse.
501
+ */
502
+ #asyncDeliveryEpoch = 0;
490
503
 
491
504
  readonly #irc: IrcBridge;
492
505
  // Agent identity (registry id) used for IRC routing and job ownership.
@@ -949,6 +962,7 @@ export class AgentSession {
949
962
  scheduleAgentContinue: options => this.#scheduleAgentContinue(options),
950
963
  waitForSessionMessagePersistence: message => this.#waitForSessionMessagePersistence(message),
951
964
  appendSessionMessage: message => this.#appendSessionMessage(message),
965
+ persistedAssistantEntryId: message => (message as PersistedAssistantMessage)[kPersistedSessionEntryId],
952
966
  sessionMessageAlreadyPersisted: message => this.#sessionMessageAlreadyPersisted(message),
953
967
  setModelWithProviderSessionReset: model => this.#setModelWithProviderSessionReset(model),
954
968
  resetCurrentResponsesProviderSession: reason => this.#resetCurrentResponsesProviderSession(reason),
@@ -1173,7 +1187,7 @@ export class AgentSession {
1173
1187
  this.#deliverAsyncJobResult(manager, jobId, text, job),
1174
1188
  );
1175
1189
  this.yieldQueue.register<AsyncResultEntry>("async-result", {
1176
- isStale: entry => manager.isDeliverySuppressed(entry.jobId),
1190
+ isStale: entry => entry.epoch !== this.#asyncDeliveryEpoch || manager.isDeliverySuppressed(entry.jobId),
1177
1191
  build: buildAsyncResultBatchMessage,
1178
1192
  });
1179
1193
  }
@@ -1267,8 +1281,8 @@ export class AgentSession {
1267
1281
  hasPendingNextTurnMessages: () => this.#pendingNextTurnMessages.length > 0,
1268
1282
  convertToLlmForSideRequest: messages => this.#convertToLlmForSideRequest(messages),
1269
1283
  effectiveServiceTier: model => this.#models.effectiveServiceTier(model),
1270
- resolveContextPromotionTarget: (model, contextWindow) =>
1271
- this.#maintenance.resolveContextPromotionTarget(model, contextWindow),
1284
+ resolveContextPromotionTarget: (model, contextWindow, signal) =>
1285
+ this.#maintenance.resolveContextPromotionTarget(model, contextWindow, signal),
1272
1286
  resolveCompactionModelCandidates: (model, availableModels) =>
1273
1287
  this.#maintenance.resolveCompactionModelCandidates(model, availableModels),
1274
1288
  resolveRetryFallbackRole: (selector, model) => this.#recovery.resolveRetryFallbackRole(selector, model),
@@ -1283,12 +1297,17 @@ export class AgentSession {
1283
1297
  this.#advisors = new SessionAdvisors(advisorsHost, {
1284
1298
  enabled: this.settings.get("advisor.enabled"),
1285
1299
  tools: config.advisorTools,
1300
+ createGrepTool: config.advisorCreateGrepTool,
1301
+ createEditTool: config.advisorCreateEditTool,
1302
+ getToolContext: config.advisorGetToolContext,
1303
+ mcpResources: config.advisorMcpResources,
1286
1304
  watchdogPrompt: config.advisorWatchdogPrompt,
1287
1305
  sharedInstructions: config.advisorSharedInstructions,
1288
1306
  contextPrompt: config.advisorContextPrompt,
1289
1307
  configs: config.advisorConfigs,
1290
1308
  streamFn: config.advisorStreamFn,
1291
1309
  transformProviderContext: config.transformProviderContext,
1310
+ initialCosts: config.initialAdvisorCosts,
1292
1311
  });
1293
1312
 
1294
1313
  const maintenanceHost: SessionMaintenanceHost = {
@@ -1394,7 +1413,10 @@ export class AgentSession {
1394
1413
  },
1395
1414
  resetTodoCycle: () => this.#todo.resetCycle(),
1396
1415
  buildDisplaySessionContext: () => this.buildDisplaySessionContext(),
1397
- resetAdvisorRuntimes: () => this.#advisors.resetAllRuntimes(),
1416
+ resetAdvisorSessionState: () => this.#advisors.resetSessionState(),
1417
+ drainAndDetachAdvisorRecorders: () => this.#advisors.drainAndDetachRecorders(),
1418
+ reattachAdvisorRecorderFeeds: () => this.#advisors.reattachRecorderFeeds(),
1419
+ clearAdvisorCost: () => this.#advisors.clearCost(),
1398
1420
  syncTodoPhasesFromBranch: () => this.#todo.syncFromBranch(),
1399
1421
  };
1400
1422
  this.#handoff = new SessionHandoff(handoffHost);
@@ -1612,7 +1634,9 @@ export class AgentSession {
1612
1634
  * transitions (newSession, switchSession, handoff, dispose) so a subagent
1613
1635
  * cleans up its own background work without touching its parent's jobs.
1614
1636
  *
1615
- * Cancellation runs against this session's scoped manager. Subagents have
1637
+ * Cleanup runs against this session's scoped manager: running jobs are
1638
+ * cancelled, finished rows are evicted with their pending deliveries, and any
1639
+ * async-result follow-up already queued for injection is dropped. Subagents have
1616
1640
  * unique agent ids and inherit the parent's manager to clean up their own
1617
1641
  * jobs. A secondary in-process top-level session gets no scoped manager,
1618
1642
  * because it defaults to `MAIN_AGENT_ID`; reaching through the global
@@ -1625,6 +1649,12 @@ export class AgentSession {
1625
1649
  if (!this.#agentId) return;
1626
1650
  const manager = this.#asyncJobManager;
1627
1651
  manager?.cancelAll({ ownerId: this.#agentId });
1652
+ manager?.evictCompletedJobs({ ownerId: this.#agentId });
1653
+ // Invalidate this owner's in-flight/drained deliveries against the new
1654
+ // generation, then drop any async-result follow-up already queued, so a
1655
+ // prior session's background result cannot inject into the next transcript.
1656
+ this.#asyncDeliveryEpoch += 1;
1657
+ this.yieldQueue.clear("async-result");
1628
1658
  }
1629
1659
 
1630
1660
  /**
@@ -1690,10 +1720,17 @@ export class AgentSession {
1690
1720
  async #deliverAsyncJobResult(manager: AsyncJobManager, jobId: string, text: string, job?: AsyncJob): Promise<void> {
1691
1721
  if (this.#isDisposed) return;
1692
1722
  if (manager.isDeliverySuppressed(jobId)) return;
1723
+ // Snapshot the generation before the async format step: a `/new` during it
1724
+ // bumps the epoch, so this delivery belongs to the replaced session and
1725
+ // must not enqueue — the suppression marker alone is unreliable because
1726
+ // job-id reuse clears it.
1727
+ const epoch = this.#asyncDeliveryEpoch;
1693
1728
  const formatted = await this.#formatAsyncResultForFollowUp(text);
1729
+ if (this.#isDisposed) return;
1730
+ if (epoch !== this.#asyncDeliveryEpoch) return;
1694
1731
  if (manager.isDeliverySuppressed(jobId)) return;
1695
1732
  const durationMs = job ? Math.max(0, Date.now() - job.startTime) : undefined;
1696
- this.yieldQueue.enqueue<AsyncResultEntry>("async-result", { jobId, result: formatted, job, durationMs });
1733
+ this.yieldQueue.enqueue<AsyncResultEntry>("async-result", { jobId, result: formatted, job, durationMs, epoch });
1697
1734
  }
1698
1735
 
1699
1736
  async #formatAsyncResultForFollowUp(result: string): Promise<string> {
@@ -2039,6 +2076,9 @@ export class AgentSession {
2039
2076
  const cache = this.#persistedMessageKeys;
2040
2077
  const wasFresh = cache !== undefined && cache.anchor === this.#persistedMessageKeysAnchor();
2041
2078
  const entryId = this.sessionManager.appendMessage(message);
2079
+ if (message.role === "assistant") {
2080
+ (message as PersistedAssistantMessage)[kPersistedSessionEntryId] = entryId;
2081
+ }
2042
2082
  const key = sessionMessagePersistenceKey(message);
2043
2083
  if (wasFresh && cache && key) {
2044
2084
  cache.keys.add(key);
@@ -2391,6 +2431,15 @@ export class AgentSession {
2391
2431
  },
2392
2432
  costUsd: assistantMsg.usage.cost.total,
2393
2433
  });
2434
+ // Persist which account served this turn so a resumed process can
2435
+ // re-pin it and keep the provider's account-scoped prompt cache
2436
+ // warm (broker-mode sticky routing is process-local).
2437
+ recordCredentialPin(
2438
+ this.#modelRegistry.authStorage,
2439
+ this.sessionManager,
2440
+ this.sessionId,
2441
+ assistantMsg.provider,
2442
+ );
2394
2443
  }
2395
2444
  if (event.message.role === "toolResult") {
2396
2445
  const { toolName, toolCallId, isError, content } = event.message;
@@ -2574,6 +2623,11 @@ export class AgentSession {
2574
2623
  return;
2575
2624
  }
2576
2625
 
2626
+ // Record quota exhaustion before deciding whether this failed turn may be
2627
+ // replayed. Visible/side-effecting output then remains terminal while its
2628
+ // credential is still blocked or rotated exactly once.
2629
+ await this.#recovery.recordUsageLimitOutcome(msg);
2630
+
2577
2631
  let compactionResult = COMPACTION_CHECK_NONE;
2578
2632
  let checkedCompaction = false;
2579
2633
  if (activeGoal) {
@@ -3358,6 +3412,14 @@ export class AgentSession {
3358
3412
  this.agent.setMetadataResolver((provider: string) =>
3359
3413
  buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage),
3360
3414
  );
3415
+ // Restore the session's recorded provider accounts before the first
3416
+ // request routes: sticky rows are process-local under a remote auth
3417
+ // broker, and losing them re-ranks onto a different account, cold-missing
3418
+ // the account-scoped prompt cache. Skipped for fresh provider sessions —
3419
+ // those explicitly want new routing identity.
3420
+ if (!this.#freshProviderSessionId) {
3421
+ seedCredentialPins(this.#modelRegistry.authStorage, this.sessionManager, sid);
3422
+ }
3361
3423
  // Keep every live advisor's provider identity in lockstep with the primary's
3362
3424
  // across every session-boundary transition — including branch paths that
3363
3425
  // skip conversation restore — so advisors never emit the previous
@@ -4315,6 +4377,7 @@ export class AgentSession {
4315
4377
  this.agent.clearDeferredToolDirectives();
4316
4378
  this.#toolChoiceQueue.clear();
4317
4379
  this.#tools.clearAcpPermissionDecisions();
4380
+ this.#tools.resetAnnouncedMounts();
4318
4381
  }
4319
4382
 
4320
4383
  /**
@@ -4565,7 +4628,9 @@ export class AgentSession {
4565
4628
  return {
4566
4629
  role: "custom",
4567
4630
  customType: "vibe-mode-context",
4568
- content: prompt.render(vibeModeActivePrompt),
4631
+ content: prompt.render(vibeModeActivePrompt, {
4632
+ todoAvailable: this.getActiveToolNames().includes("todo"),
4633
+ }),
4569
4634
  display: false,
4570
4635
  attribution: "agent",
4571
4636
  timestamp: Date.now(),
@@ -5146,6 +5211,7 @@ export class AgentSession {
5146
5211
  void this.dispose().finally(() => process.exit(0));
5147
5212
  },
5148
5213
  getContextUsage: () => this.getContextUsage(),
5214
+ getAsyncJobSnapshot: () => this.getAsyncJobSnapshot(),
5149
5215
  waitForIdle: () => this.waitForIdle(),
5150
5216
  newSession: async options => {
5151
5217
  const success = await this.newSession({ parentSession: options?.parentSession });
@@ -5964,6 +6030,7 @@ export class AgentSession {
5964
6030
  }
5965
6031
 
5966
6032
  this.#disconnectFromAgent();
6033
+ let advisorRecordersDetached = false;
5967
6034
  await this.abort();
5968
6035
  this.#cancelOwnAsyncJobs();
5969
6036
  this.#closeAllProviderSessions("new session");
@@ -5971,69 +6038,73 @@ export class AgentSession {
5971
6038
  const bashTransition = this.#bash.beginSessionTransition({ persistDetached: options?.drop !== true });
5972
6039
  let sessionTransitioned = false;
5973
6040
  try {
5974
- this.agent.reset();
5975
- if (options?.drop && previousSessionFile) {
5976
- // Detach the advisor recorder feed and drain its writer BEFORE deleting the
5977
- // old artifacts dir: `await this.abort()` only stops the primary, so a still-
5978
- // running advisor turn could otherwise finish, emit `message_end`, and recreate
5979
- // `<old>/__advisor.jsonl`. #resetAdvisorSessionState (after newSession) re-primes
5980
- // the advisor and re-attaches the feed at the new session's path.
5981
- await this.#advisors.detachAndCloseRecorders();
5982
- try {
5983
- await this.sessionManager.dropSession(previousSessionFile);
5984
- } catch (err) {
5985
- logger.error("Failed to delete session during /drop", { err });
6041
+ advisorRecordersDetached = true;
6042
+ await this.#advisors.drainAndDetachRecorders();
6043
+ try {
6044
+ this.agent.reset();
6045
+ if (options?.drop && previousSessionFile) {
6046
+ try {
6047
+ await this.sessionManager.dropSession(previousSessionFile);
6048
+ } catch (err) {
6049
+ logger.error("Failed to delete session during /drop", { err });
6050
+ }
6051
+ } else {
6052
+ await this.sessionManager.flush();
5986
6053
  }
5987
- } else {
5988
- await this.sessionManager.flush();
6054
+ await this.sessionManager.newSession({
6055
+ ...options,
6056
+ additionalDirectories: this.settings.get("workspace.additionalDirectories"),
6057
+ });
6058
+ this.#bash.markSessionTransition(bashTransition);
6059
+ // The new session owns the transcript from here, so the previous
6060
+ // conversation's advisor spend is retired with it. Clearing at the commit
6061
+ // point keeps the status line honest even if a later step below throws.
6062
+ this.#advisors.clearCost();
6063
+ sessionTransitioned = true;
6064
+ } finally {
6065
+ this.#bash.finishSessionTransition(bashTransition, sessionTransitioned);
5989
6066
  }
5990
- await this.sessionManager.newSession({
5991
- ...options,
5992
- additionalDirectories: this.settings.get("workspace.additionalDirectories"),
5993
- });
5994
- this.#bash.markSessionTransition(bashTransition);
5995
- // The new session owns the transcript from here, so the previous
5996
- // conversation's advisor spend is retired with it. Clearing at the commit
5997
- // point keeps the status line honest even if a later step below throws.
5998
- this.#advisors.clearCost();
5999
- sessionTransitioned = true;
6000
- } finally {
6001
- this.#bash.finishSessionTransition(bashTransition, sessionTransitioned);
6002
- }
6003
6067
 
6004
- this.#clearSessionScopedToolState();
6005
- this.#clearCheckpointRuntimeState();
6006
- this.setTodoPhases([]);
6007
- this.#freshProviderSessionId = undefined;
6008
- this.#clearInheritedProviderPromptCacheKey();
6009
- this.#syncAgentSessionId();
6010
- this.#memory.rekeyForCurrentSessionId();
6011
- await this.#memory.resetContextForNewTranscript();
6012
- this.#pendingNextTurnMessages = [];
6013
- this.#scheduledHiddenNextTurnGeneration = undefined;
6068
+ this.#clearSessionScopedToolState();
6069
+ this.#clearCheckpointRuntimeState();
6070
+ this.setTodoPhases([]);
6071
+ this.#freshProviderSessionId = undefined;
6072
+ this.#clearInheritedProviderPromptCacheKey();
6073
+ this.#syncAgentSessionId();
6074
+ this.#memory.rekeyForCurrentSessionId();
6075
+ await this.#memory.resetContextForNewTranscript();
6076
+ this.#pendingNextTurnMessages = [];
6077
+ this.#scheduledHiddenNextTurnGeneration = undefined;
6014
6078
 
6015
- this.sessionManager.appendThinkingLevelChange(this.thinkingLevel, this.configuredThinkingLevel());
6016
- this.sessionManager.appendServiceTierChange(this.#models.serviceTierEntry());
6079
+ this.sessionManager.appendThinkingLevelChange(this.thinkingLevel, this.configuredThinkingLevel());
6080
+ this.sessionManager.appendServiceTierChange(this.#models.serviceTierEntry());
6017
6081
 
6018
- this.#todo.resetCycle();
6019
- this.#planReferenceSent = false;
6020
- this.#planReferencePath = "local://PLAN.md";
6021
- this.#advisors.resetSessionState();
6022
- this.#reconnectToAgent();
6023
- // The workspace-roots block must reflect the new session's directory set,
6024
- // not the previous session's refresh before the next turn goes out.
6025
- await this.refreshBaseSystemPrompt();
6082
+ this.#todo.resetCycle();
6083
+ this.#planReferenceSent = false;
6084
+ this.#planReferencePath = "local://PLAN.md";
6085
+ this.#advisors.resetSessionState();
6086
+ advisorRecordersDetached = false;
6087
+ this.#reconnectToAgent();
6088
+ // The workspace-roots block must reflect the new session's directory set,
6089
+ // not the previous session's — refresh before the next turn goes out.
6090
+ await this.refreshBaseSystemPrompt();
6026
6091
 
6027
- // Emit session_switch event with reason "new" to hooks
6028
- if (this.#extensionRunner) {
6029
- await this.#extensionRunner.emit({
6030
- type: "session_switch",
6031
- reason: "new",
6032
- previousSessionFile,
6033
- });
6034
- }
6092
+ // Emit session_switch event with reason "new" to hooks
6093
+ if (this.#extensionRunner) {
6094
+ await this.#extensionRunner.emit({
6095
+ type: "session_switch",
6096
+ reason: "new",
6097
+ previousSessionFile,
6098
+ });
6099
+ }
6035
6100
 
6036
- return true;
6101
+ return true;
6102
+ } finally {
6103
+ if (advisorRecordersDetached) {
6104
+ if (sessionTransitioned) this.#advisors.resetSessionState();
6105
+ else this.#advisors.reattachRecorderFeeds();
6106
+ }
6107
+ }
6037
6108
  }
6038
6109
 
6039
6110
  /**
@@ -6069,59 +6140,70 @@ export class AgentSession {
6069
6140
  await this.#bash.flushPending();
6070
6141
  // Flush current session to ensure all entries are written
6071
6142
  await this.sessionManager.flush();
6072
- const bashTransition = this.#bash.beginSessionTransition();
6073
-
6074
- // Fork the session (creates new session file with same entries)
6075
- let forkResult: { oldSessionFile: string; newSessionFile: string } | undefined;
6143
+ let advisorRecordersDetached = false;
6076
6144
  try {
6077
- forkResult = await this.sessionManager.fork();
6078
- } catch (error) {
6079
- this.#bash.finishSessionTransition(bashTransition, false);
6080
- throw error;
6081
- }
6082
- if (!forkResult) {
6083
- this.#bash.finishSessionTransition(bashTransition, false);
6084
- return false;
6085
- }
6086
- this.#bash.markSessionTransition(bashTransition);
6087
- this.#bash.finishSessionTransition(bashTransition, true);
6145
+ advisorRecordersDetached = true;
6146
+ // Fork keeps the conversation, but still needs a quiet artifact boundary:
6147
+ // stop and settle in-flight advisors before muting their feeds.
6148
+ await this.#advisors.drainAndDetachRecorders();
6149
+ const bashTransition = this.#bash.beginSessionTransition();
6150
+
6151
+ // Fork the session (creates new session file with same entries)
6152
+ let forkResult: { oldSessionFile: string; newSessionFile: string } | undefined;
6153
+ try {
6154
+ forkResult = await this.sessionManager.fork();
6155
+ } catch (error) {
6156
+ this.#bash.finishSessionTransition(bashTransition, false);
6157
+ throw error;
6158
+ }
6159
+ if (!forkResult) {
6160
+ this.#bash.finishSessionTransition(bashTransition, false);
6161
+ return false;
6162
+ }
6163
+ this.#bash.markSessionTransition(bashTransition);
6164
+ this.#bash.finishSessionTransition(bashTransition, true);
6088
6165
 
6089
- // Copy artifacts directory if it exists
6090
- const oldArtifactDir = forkResult.oldSessionFile.slice(0, -6);
6091
- const newArtifactDir = forkResult.newSessionFile.slice(0, -6);
6166
+ // Copy artifacts directory if it exists
6167
+ const oldArtifactDir = forkResult.oldSessionFile.slice(0, -6);
6168
+ const newArtifactDir = forkResult.newSessionFile.slice(0, -6);
6092
6169
 
6093
- try {
6094
- const oldDirStat = await fs.promises.stat(oldArtifactDir);
6095
- if (oldDirStat.isDirectory()) {
6096
- await fs.promises.cp(oldArtifactDir, newArtifactDir, { recursive: true });
6170
+ try {
6171
+ const oldDirStat = await fs.promises.stat(oldArtifactDir);
6172
+ if (oldDirStat.isDirectory()) {
6173
+ await fs.promises.cp(oldArtifactDir, newArtifactDir, { recursive: true });
6174
+ }
6175
+ } catch (err) {
6176
+ if (!isEnoent(err)) {
6177
+ logger.warn("Failed to copy artifacts during fork", {
6178
+ oldArtifactDir,
6179
+ newArtifactDir,
6180
+ error: err instanceof Error ? err.message : String(err),
6181
+ });
6182
+ }
6097
6183
  }
6098
- } catch (err) {
6099
- if (!isEnoent(err)) {
6100
- logger.warn("Failed to copy artifacts during fork", {
6101
- oldArtifactDir,
6102
- newArtifactDir,
6103
- error: err instanceof Error ? err.message : String(err),
6184
+
6185
+ // Update agent session ID
6186
+ this.#freshProviderSessionId = undefined;
6187
+ this.#adoptInheritedProviderPromptCacheKey();
6188
+ this.#syncAgentSessionId();
6189
+ this.#memory.rekeyForCurrentSessionId();
6190
+ this.#advisors.reattachRecorderFeeds();
6191
+ advisorRecordersDetached = false;
6192
+ await this.#memory.resetContextForNewTranscript();
6193
+
6194
+ // Emit session_switch event with reason "fork" to hooks
6195
+ if (this.#extensionRunner) {
6196
+ await this.#extensionRunner.emit({
6197
+ type: "session_switch",
6198
+ reason: "fork",
6199
+ previousSessionFile,
6104
6200
  });
6105
6201
  }
6106
- }
6107
-
6108
- // Update agent session ID
6109
- this.#freshProviderSessionId = undefined;
6110
- this.#adoptInheritedProviderPromptCacheKey();
6111
- this.#syncAgentSessionId();
6112
- this.#memory.rekeyForCurrentSessionId();
6113
- await this.#memory.resetContextForNewTranscript();
6114
6202
 
6115
- // Emit session_switch event with reason "fork" to hooks
6116
- if (this.#extensionRunner) {
6117
- await this.#extensionRunner.emit({
6118
- type: "session_switch",
6119
- reason: "fork",
6120
- previousSessionFile,
6121
- });
6203
+ return true;
6204
+ } finally {
6205
+ if (advisorRecordersDetached) this.#advisors.reattachRecorderFeeds();
6122
6206
  }
6123
-
6124
- return true;
6125
6207
  }
6126
6208
 
6127
6209
  /** Move the active session and artifacts after enforcing mode transition invariants. */
@@ -6495,13 +6577,30 @@ export class AgentSession {
6495
6577
 
6496
6578
  async #setModelWithProviderSessionReset(model: Model): Promise<void> {
6497
6579
  const currentModel = this.model;
6580
+ const isChanging = !currentModel || !modelsAreEqual(currentModel, model);
6498
6581
  if (currentModel) {
6499
6582
  this.#closeProviderSessionsForModelSwitch(currentModel, model);
6500
- if (!modelsAreEqual(currentModel, model)) {
6583
+ if (isChanging) {
6501
6584
  this.#clearInheritedProviderPromptCacheKey();
6502
6585
  }
6503
6586
  }
6504
6587
  this.agent.setModel(model);
6588
+ // Model mutations driven through ModelControls (explicit /model, prewalk
6589
+ // hand-offs, retry-fallback, model cycling) funnel through this method,
6590
+ // so this is the single point that notifies subscribers (ACP config
6591
+ // sync, RPC, TUI status line) — callers that bypass ModelControls never
6592
+ // need to remember to notify separately. `switchSession`'s rollback
6593
+ // restores via `agent.setModel` directly and emits its own corrective
6594
+ // event.
6595
+ //
6596
+ // Fan-out uses the synchronous `#emit`, matching `thinking_level_changed`:
6597
+ // `model_changed` has no extension-facing hook (`#emitExtensionEvent`
6598
+ // never maps it), so routing it through `#emitSessionEvent` would only
6599
+ // add an extension-delivery await inside every model switch — including
6600
+ // retry-fallback on the error path.
6601
+ if (isChanging) {
6602
+ this.#emit({ type: "model_changed" });
6603
+ }
6505
6604
 
6506
6605
  // Re-evaluate append-only context mode — provider or setting may have changed
6507
6606
  this.#syncAppendOnlyContext(model);
@@ -7020,6 +7119,11 @@ export class AgentSession {
7020
7119
  this.#scheduledHiddenNextTurnGeneration = undefined;
7021
7120
 
7022
7121
  try {
7122
+ if (switchingToDifferentSession) {
7123
+ // Stop and settle in-flight advisors while the old-session feeds can
7124
+ // still observe message_end, then mute before swapping files.
7125
+ await this.#advisors.drainAndDetachRecorders();
7126
+ }
7023
7127
  await this.sessionManager.setSessionFile(sessionPath);
7024
7128
  this.#bash.markSessionTransition(bashTransition);
7025
7129
  if (switchingToDifferentSession) {
@@ -7132,7 +7236,7 @@ export class AgentSession {
7132
7236
  if (switchingToDifferentSession) {
7133
7237
  await this.#memory.resetContextForNewTranscript();
7134
7238
  }
7135
- if (switchingToDifferentSession) {
7239
+ if (switchingToDifferentSession || didReloadConversationChange) {
7136
7240
  this.#clearSessionScopedToolState();
7137
7241
  }
7138
7242
  this.#reconnectToAgent();
@@ -7155,9 +7259,14 @@ export class AgentSession {
7155
7259
  error: String(refreshErr),
7156
7260
  });
7157
7261
  }
7158
- // Only a committed switch retires the previous conversation's advisor spend:
7159
- // an earlier clear would be lost work if any step above rolled the switch back.
7160
- if (switchingToDifferentSession) this.#advisors.clearCost();
7262
+ // Hand the ledger over to the session that just took over, and only once the
7263
+ // switch has committed: an earlier swap would be lost work if any step above
7264
+ // rolled it back. The target's own advisor transcripts are the record of what
7265
+ // it already spent, so a session with history resumes with its total instead
7266
+ // of restarting at zero.
7267
+ if (switchingToDifferentSession) {
7268
+ this.#advisors.restoreCost(await loadAdvisorTranscriptCosts(this.sessionFile));
7269
+ }
7161
7270
  this.#bash.finishSessionTransition(bashTransition, true);
7162
7271
  return true;
7163
7272
  } catch (error) {
@@ -7178,13 +7287,32 @@ export class AgentSession {
7178
7287
  this.#pendingRewindReport = previousPendingRewindReport;
7179
7288
  this.#lastCompletedRewind = previousLastCompletedRewind;
7180
7289
  this.#rewoundToolResultIds = previousRewoundToolResultIds;
7290
+ // The try block may have already reached #setModelWithProviderSessionReset
7291
+ // for the target session's model, which emits `model_changed` for it.
7292
+ // Restoring here bypasses that method (it also resets provider-session
7293
+ // state we're already unwinding above), so if the rollback actually
7294
+ // changes the model back, emit the corrective event ourselves —
7295
+ // otherwise ACP/RPC/TUI keep advertising the never-committed target.
7296
+ // Deferred until after restoreThinkingSnapshot below: #emit's listeners
7297
+ // (ACP's #handleLifetimeEvent -> #pushConfigOptionUpdate) read
7298
+ // session state synchronously before their first await, so emitting
7299
+ // here — before the target session's thinking level is unwound —
7300
+ // would push a { previousModel, target-session-thinking } config that
7301
+ // was never a real session state.
7302
+ let modelRolledBack = false;
7181
7303
  if (previousModel) {
7304
+ const rolledBackModel = this.model;
7182
7305
  this.agent.setModel(previousModel);
7306
+ modelRolledBack = !modelsAreEqual(rolledBackModel, previousModel);
7183
7307
  }
7184
7308
  this.#models.restoreThinkingSnapshot(previousThinkingLevel, previousAutoThinking, previousAutoResolvedLevel);
7185
7309
  this.#models.restoreServiceTiers(previousServiceTierByFamily);
7310
+ if (modelRolledBack) {
7311
+ this.#emit({ type: "model_changed" });
7312
+ }
7186
7313
  this.#todo.syncFromBranch();
7187
7314
  this.#advisors.resetAllRuntimes();
7315
+ this.#advisors.reattachRecorderFeeds();
7188
7316
  this.#reconnectToAgent();
7189
7317
  try {
7190
7318
  await this.#sessionSwitchReconciler?.();
@@ -7252,45 +7380,57 @@ export class AgentSession {
7252
7380
  await this.#drainAutolearnCapture();
7253
7381
 
7254
7382
  let sessionTransitioned = false;
7383
+ let advisorRecordersDetached = false;
7255
7384
  try {
7256
- if (!selectedEntry.parentId) {
7257
- await this.sessionManager.newSession({ parentSession: previousSessionFile });
7258
- } else {
7259
- this.sessionManager.createBranchedSession(selectedEntry.parentId);
7385
+ advisorRecordersDetached = true;
7386
+ await this.#advisors.drainAndDetachRecorders();
7387
+ try {
7388
+ if (!selectedEntry.parentId) {
7389
+ await this.sessionManager.newSession({ parentSession: previousSessionFile });
7390
+ } else {
7391
+ this.sessionManager.createBranchedSession(selectedEntry.parentId);
7392
+ }
7393
+ this.#bash.markSessionTransition(bashTransition);
7394
+ this.#advisors.clearCost();
7395
+ sessionTransitioned = true;
7396
+ } finally {
7397
+ this.#bash.finishSessionTransition(bashTransition, sessionTransitioned);
7260
7398
  }
7261
- this.#bash.markSessionTransition(bashTransition);
7262
- this.#advisors.clearCost();
7263
- sessionTransitioned = true;
7264
- } finally {
7265
- this.#bash.finishSessionTransition(bashTransition, sessionTransitioned);
7266
- }
7267
- this.#clearSessionScopedToolState();
7268
- this.#rehydrateCheckpointRewindState();
7269
- this.#todo.syncFromBranch();
7270
- this.#freshProviderSessionId = undefined;
7271
- this.#clearInheritedProviderPromptCacheKey();
7272
- this.#syncAgentSessionId();
7273
- this.#memory.rekeyForCurrentSessionId();
7274
- await this.#memory.resetContextForNewTranscript();
7399
+ this.#clearSessionScopedToolState();
7400
+ this.#rehydrateCheckpointRewindState();
7401
+ this.#todo.syncFromBranch();
7402
+ this.#freshProviderSessionId = undefined;
7403
+ this.#clearInheritedProviderPromptCacheKey();
7404
+ this.#syncAgentSessionId();
7405
+ this.#memory.rekeyForCurrentSessionId();
7406
+ await this.#memory.resetContextForNewTranscript();
7275
7407
 
7276
- // Reload messages from entries (works for both file and in-memory mode)
7277
- const sessionContext = this.buildDisplaySessionContext();
7408
+ // Reload messages from entries (works for both file and in-memory mode)
7409
+ const sessionContext = this.buildDisplaySessionContext();
7278
7410
 
7279
- // Emit session_branch event to hooks (after branch completes)
7280
- if (this.#extensionRunner) {
7281
- await this.#extensionRunner.emit({
7282
- type: "session_branch",
7283
- previousSessionFile,
7284
- });
7285
- }
7411
+ // Emit session_branch event to hooks (after branch completes)
7412
+ if (this.#extensionRunner) {
7413
+ await this.#extensionRunner.emit({
7414
+ type: "session_branch",
7415
+ previousSessionFile,
7416
+ });
7417
+ }
7286
7418
 
7287
- if (!skipConversationRestore) {
7288
- this.agent.replaceMessages(sessionContext.messages);
7289
- this.#advisors.resetSessionState();
7290
- this.#closeCodexProviderSessionsForHistoryRewrite();
7291
- }
7419
+ if (!skipConversationRestore) {
7420
+ this.agent.replaceMessages(sessionContext.messages);
7421
+ this.#advisors.resetSessionState();
7422
+ this.#closeCodexProviderSessionsForHistoryRewrite();
7423
+ }
7292
7424
 
7293
- return { selectedText, selectedImages, cancelled: false };
7425
+ this.#advisors.reattachRecorderFeeds();
7426
+ advisorRecordersDetached = false;
7427
+ return { selectedText, selectedImages, cancelled: false };
7428
+ } finally {
7429
+ if (advisorRecordersDetached) {
7430
+ if (sessionTransitioned) this.#advisors.resetSessionState();
7431
+ else this.#advisors.reattachRecorderFeeds();
7432
+ }
7433
+ }
7294
7434
  }
7295
7435
 
7296
7436
  async branchFromBtw(
@@ -7354,44 +7494,55 @@ export class AgentSession {
7354
7494
  await this.#drainAutolearnCapture();
7355
7495
 
7356
7496
  let sessionTransitioned = false;
7497
+ let advisorRecordersDetached = false;
7357
7498
  try {
7358
- this.sessionManager.createBranchedSession(leafId);
7359
- this.#bash.markSessionTransition(bashTransition);
7360
- this.#advisors.clearCost();
7361
- sessionTransitioned = true;
7362
- } finally {
7363
- this.#bash.finishSessionTransition(bashTransition, sessionTransitioned);
7364
- }
7499
+ advisorRecordersDetached = true;
7500
+ await this.#advisors.drainAndDetachRecorders();
7501
+ try {
7502
+ this.sessionManager.createBranchedSession(leafId);
7503
+ this.#bash.markSessionTransition(bashTransition);
7504
+ this.#advisors.clearCost();
7505
+ sessionTransitioned = true;
7506
+ } finally {
7507
+ this.#bash.finishSessionTransition(bashTransition, sessionTransitioned);
7508
+ }
7365
7509
 
7366
- this.#clearSessionScopedToolState();
7510
+ this.#clearSessionScopedToolState();
7367
7511
 
7368
- this.#rehydrateCheckpointRewindState();
7369
- this.sessionManager.appendMessage({
7370
- role: "user",
7371
- content: [{ type: "text", text: question }],
7372
- timestamp: Date.now(),
7373
- });
7374
- this.sessionManager.appendMessage(sanitizeAssistantForReparentedHistory(assistantMessage));
7375
- this.#todo.syncFromBranch();
7376
- this.#freshProviderSessionId = undefined;
7377
- this.#syncAgentSessionId();
7378
- this.#memory.rekeyForCurrentSessionId();
7379
- await this.#memory.resetContextForNewTranscript();
7512
+ this.#rehydrateCheckpointRewindState();
7513
+ this.sessionManager.appendMessage({
7514
+ role: "user",
7515
+ content: [{ type: "text", text: question }],
7516
+ timestamp: Date.now(),
7517
+ });
7518
+ this.sessionManager.appendMessage(sanitizeAssistantForReparentedHistory(assistantMessage));
7519
+ this.#todo.syncFromBranch();
7520
+ this.#freshProviderSessionId = undefined;
7521
+ this.#syncAgentSessionId();
7522
+ this.#memory.rekeyForCurrentSessionId();
7523
+ await this.#memory.resetContextForNewTranscript();
7380
7524
 
7381
- const sessionContext = this.buildDisplaySessionContext();
7525
+ const sessionContext = this.buildDisplaySessionContext();
7382
7526
 
7383
- if (this.#extensionRunner) {
7384
- await this.#extensionRunner.emit({
7385
- type: "session_branch",
7386
- previousSessionFile,
7387
- });
7388
- }
7527
+ if (this.#extensionRunner) {
7528
+ await this.#extensionRunner.emit({
7529
+ type: "session_branch",
7530
+ previousSessionFile,
7531
+ });
7532
+ }
7389
7533
 
7390
- this.agent.replaceMessages(sessionContext.messages);
7391
- this.#advisors.resetSessionState();
7392
- this.#closeCodexProviderSessionsForHistoryRewrite();
7534
+ this.agent.replaceMessages(sessionContext.messages);
7535
+ this.#advisors.resetSessionState();
7536
+ this.#closeCodexProviderSessionsForHistoryRewrite();
7537
+ advisorRecordersDetached = false;
7393
7538
 
7394
- return { cancelled: false, sessionFile: this.sessionFile };
7539
+ return { cancelled: false, sessionFile: this.sessionFile };
7540
+ } finally {
7541
+ if (advisorRecordersDetached) {
7542
+ if (sessionTransitioned) this.#advisors.resetSessionState();
7543
+ else this.#advisors.reattachRecorderFeeds();
7544
+ }
7545
+ }
7395
7546
  }
7396
7547
 
7397
7548
  // =========================================================================