@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
  3. package/dist/cli.js +10009 -9654
  4. package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
  5. package/dist/types/advisor/runtime.d.ts +8 -2
  6. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  7. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  8. package/dist/types/cli/args.d.ts +2 -0
  9. package/dist/types/cli/session-picker.d.ts +14 -7
  10. package/dist/types/collab/guest.d.ts +21 -2
  11. package/dist/types/commands/launch.d.ts +6 -0
  12. package/dist/types/config/model-discovery.d.ts +12 -0
  13. package/dist/types/config/settings-schema.d.ts +73 -8
  14. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  15. package/dist/types/cursor.d.ts +193 -8
  16. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  17. package/dist/types/edit/hashline/diff.d.ts +9 -1
  18. package/dist/types/edit/index.d.ts +8 -1
  19. package/dist/types/edit/streaming.d.ts +8 -1
  20. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  21. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  22. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
  23. package/dist/types/internal-urls/index.d.ts +1 -0
  24. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  25. package/dist/types/internal-urls/security-protocol.d.ts +15 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  28. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  29. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  30. package/dist/types/lsp/client.d.ts +2 -0
  31. package/dist/types/main.d.ts +2 -0
  32. package/dist/types/mcp/manager.d.ts +33 -4
  33. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  35. package/dist/types/modes/components/session-selector.d.ts +6 -0
  36. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  38. package/dist/types/modes/components/usage-row.d.ts +2 -0
  39. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  40. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  41. package/dist/types/modes/interactive-mode.d.ts +4 -2
  42. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  43. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  44. package/dist/types/modes/theme/theme.d.ts +12 -1
  45. package/dist/types/modes/types.d.ts +2 -1
  46. package/dist/types/sdk.d.ts +18 -0
  47. package/dist/types/security/auth.d.ts +30 -0
  48. package/dist/types/security/cloud.d.ts +79 -0
  49. package/dist/types/security/comparison.d.ts +49 -0
  50. package/dist/types/security/contracts/ids.d.ts +15 -0
  51. package/dist/types/security/contracts/index.d.ts +4 -0
  52. package/dist/types/security/contracts/schemas.d.ts +660 -0
  53. package/dist/types/security/contracts/types.d.ts +234 -0
  54. package/dist/types/security/contracts/validation.d.ts +5 -0
  55. package/dist/types/security/coordinator.d.ts +100 -0
  56. package/dist/types/security/importers/codex-security.d.ts +7 -0
  57. package/dist/types/security/importers/index.d.ts +2 -0
  58. package/dist/types/security/importers/sarif.d.ts +9 -0
  59. package/dist/types/security/index.d.ts +13 -0
  60. package/dist/types/security/preflight.d.ts +61 -0
  61. package/dist/types/security/provenance.d.ts +24 -0
  62. package/dist/types/security/publication.d.ts +78 -0
  63. package/dist/types/security/remediation.d.ts +27 -0
  64. package/dist/types/security/resource-output.d.ts +8 -0
  65. package/dist/types/security/sarif.d.ts +2 -0
  66. package/dist/types/security/store.d.ts +40 -0
  67. package/dist/types/session/agent-session-events.d.ts +2 -0
  68. package/dist/types/session/agent-session-types.d.ts +36 -1
  69. package/dist/types/session/agent-session.d.ts +4 -2
  70. package/dist/types/session/claude-session-store.d.ts +13 -0
  71. package/dist/types/session/codex-session-store.d.ts +14 -0
  72. package/dist/types/session/credential-pin.d.ts +58 -0
  73. package/dist/types/session/foreign-session-import.d.ts +15 -0
  74. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  75. package/dist/types/session/foreign-session-store.d.ts +23 -0
  76. package/dist/types/session/session-advisors.d.ts +47 -2
  77. package/dist/types/session/session-entries.d.ts +19 -1
  78. package/dist/types/session/session-handoff.d.ts +4 -1
  79. package/dist/types/session/session-loader.d.ts +2 -0
  80. package/dist/types/session/session-maintenance.d.ts +1 -1
  81. package/dist/types/session/session-manager.d.ts +21 -0
  82. package/dist/types/session/session-tools.d.ts +25 -1
  83. package/dist/types/session/turn-recovery.d.ts +10 -4
  84. package/dist/types/slash-commands/helpers/security.d.ts +2 -0
  85. package/dist/types/system-prompt.d.ts +4 -1
  86. package/dist/types/task/executor.d.ts +14 -2
  87. package/dist/types/task/render.d.ts +7 -0
  88. package/dist/types/thinking.d.ts +18 -6
  89. package/dist/types/tools/acp-bridge.d.ts +28 -5
  90. package/dist/types/tools/builtin-names.d.ts +1 -1
  91. package/dist/types/tools/grep.d.ts +17 -1
  92. package/dist/types/tools/hub/launch.d.ts +5 -1
  93. package/dist/types/tools/index.d.ts +10 -2
  94. package/dist/types/tools/path-utils.d.ts +19 -0
  95. package/dist/types/tools/read.d.ts +2 -0
  96. package/dist/types/tools/security-scan.d.ts +96 -0
  97. package/dist/types/tools/xdev.d.ts +2 -1
  98. package/dist/types/utils/changelog.d.ts +19 -0
  99. package/dist/types/utils/git.d.ts +10 -0
  100. package/dist/types/utils/jj.d.ts +11 -4
  101. package/dist/types/web/search/types.d.ts +2 -2
  102. package/package.json +12 -12
  103. package/scripts/security-compare.ts +40 -0
  104. package/src/advisor/runtime.ts +138 -12
  105. package/src/advisor/transcript-recorder.ts +56 -0
  106. package/src/auto-thinking/classifier.ts +48 -10
  107. package/src/cli/args.ts +6 -0
  108. package/src/cli/flag-tables.ts +2 -0
  109. package/src/cli/gallery-fixtures/fs.ts +25 -0
  110. package/src/cli/session-picker.ts +34 -17
  111. package/src/cli/ttsr-cli.ts +19 -1
  112. package/src/cli.ts +9 -0
  113. package/src/collab/guest.ts +37 -4
  114. package/src/collab/host.ts +1 -0
  115. package/src/commands/launch.ts +6 -0
  116. package/src/config/model-discovery.ts +32 -5
  117. package/src/config/model-registry.ts +107 -25
  118. package/src/config/settings-schema.ts +77 -6
  119. package/src/config/settings.ts +25 -0
  120. package/src/cursor-bridge-tools.ts +81 -0
  121. package/src/cursor.ts +463 -12
  122. package/src/edit/edit-clipboard.ts +23 -0
  123. package/src/edit/hashline/diff.ts +49 -10
  124. package/src/edit/hashline/execute.ts +38 -6
  125. package/src/edit/hashline/filesystem.ts +27 -3
  126. package/src/edit/index.ts +12 -2
  127. package/src/edit/renderer.ts +4 -4
  128. package/src/edit/streaming.ts +17 -2
  129. package/src/eval/py/prelude.py +7 -3
  130. package/src/export/html/template.js +1 -1
  131. package/src/extensibility/extensions/runner.ts +70 -0
  132. package/src/extensibility/extensions/types.ts +30 -0
  133. package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
  134. package/src/internal-urls/index.ts +1 -0
  135. package/src/internal-urls/local-protocol.ts +3 -2
  136. package/src/internal-urls/mcp-protocol.ts +4 -1
  137. package/src/internal-urls/router.ts +4 -1
  138. package/src/internal-urls/security-protocol.ts +261 -0
  139. package/src/internal-urls/types.ts +1 -1
  140. package/src/launch/terminal-output-worker-client.ts +53 -0
  141. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  142. package/src/launch/terminal-output-worker.ts +23 -0
  143. package/src/lsp/client.ts +18 -3
  144. package/src/lsp/index.ts +7 -0
  145. package/src/lsp/render.ts +1 -1
  146. package/src/main.ts +127 -56
  147. package/src/mcp/manager.ts +120 -18
  148. package/src/modes/acp/acp-agent.ts +15 -9
  149. package/src/modes/components/chat-transcript-builder.ts +30 -11
  150. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  151. package/src/modes/components/read-tool-group.ts +159 -17
  152. package/src/modes/components/session-selector.ts +19 -3
  153. package/src/modes/components/status-line/component.ts +381 -54
  154. package/src/modes/components/tool-execution.ts +78 -31
  155. package/src/modes/components/usage-row.ts +10 -5
  156. package/src/modes/controllers/event-controller.ts +26 -4
  157. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  158. package/src/modes/controllers/selector-controller.ts +94 -36
  159. package/src/modes/controllers/tan-command-controller.ts +14 -0
  160. package/src/modes/interactive-mode.ts +31 -17
  161. package/src/modes/rpc/host-uris.ts +6 -0
  162. package/src/modes/rpc/rpc-client.ts +18 -0
  163. package/src/modes/rpc/rpc-mode.ts +16 -1
  164. package/src/modes/rpc/rpc-types.ts +11 -0
  165. package/src/modes/theme/theme.ts +151 -6
  166. package/src/modes/types.ts +2 -1
  167. package/src/modes/utils/ui-helpers.ts +38 -21
  168. package/src/prompts/agents/security-reviewer.md +75 -0
  169. package/src/prompts/security/scan-coordinator.md +7 -0
  170. package/src/prompts/security/scan-request.md +21 -0
  171. package/src/prompts/security/validate-request.md +8 -0
  172. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  173. package/src/prompts/system/plan-mode-active.md +2 -2
  174. package/src/prompts/system/system-prompt.md +6 -0
  175. package/src/prompts/system/xdev-mount-notice.md +1 -1
  176. package/src/prompts/tools/bash.md +15 -17
  177. package/src/prompts/tools/checkpoint.md +1 -1
  178. package/src/prompts/tools/glob.md +7 -6
  179. package/src/prompts/tools/grep.md +7 -6
  180. package/src/prompts/tools/security-publish.md +1 -0
  181. package/src/prompts/tools/security-scan.md +1 -0
  182. package/src/sdk.ts +212 -21
  183. package/src/security/auth.ts +98 -0
  184. package/src/security/cloud.ts +686 -0
  185. package/src/security/comparison.ts +255 -0
  186. package/src/security/contracts/ids.ts +111 -0
  187. package/src/security/contracts/index.ts +4 -0
  188. package/src/security/contracts/schemas.ts +201 -0
  189. package/src/security/contracts/types.ts +254 -0
  190. package/src/security/contracts/validation.ts +65 -0
  191. package/src/security/coordinator.ts +708 -0
  192. package/src/security/importers/codex-security.ts +387 -0
  193. package/src/security/importers/index.ts +2 -0
  194. package/src/security/importers/sarif.ts +357 -0
  195. package/src/security/index.ts +13 -0
  196. package/src/security/preflight.ts +405 -0
  197. package/src/security/provenance.ts +106 -0
  198. package/src/security/publication.ts +326 -0
  199. package/src/security/remediation.ts +93 -0
  200. package/src/security/resource-output.ts +50 -0
  201. package/src/security/sarif.ts +78 -0
  202. package/src/security/store.ts +430 -0
  203. package/src/session/agent-session-events.ts +1 -0
  204. package/src/session/agent-session-types.ts +40 -1
  205. package/src/session/agent-session.ts +302 -175
  206. package/src/session/claude-session-store.ts +426 -0
  207. package/src/session/codex-session-store.ts +673 -0
  208. package/src/session/credential-pin.ts +93 -0
  209. package/src/session/foreign-session-import.ts +52 -0
  210. package/src/session/foreign-session-jsonl.ts +29 -0
  211. package/src/session/foreign-session-store.ts +26 -0
  212. package/src/session/model-controls.ts +11 -3
  213. package/src/session/session-advisors.ts +140 -16
  214. package/src/session/session-entries.ts +21 -1
  215. package/src/session/session-handoff.ts +20 -3
  216. package/src/session/session-loader.ts +25 -9
  217. package/src/session/session-maintenance.ts +59 -11
  218. package/src/session/session-manager.ts +61 -0
  219. package/src/session/session-tools.ts +107 -5
  220. package/src/session/settings-stream-fn.ts +1 -0
  221. package/src/session/turn-recovery.ts +97 -51
  222. package/src/slash-commands/builtin-registry.ts +152 -3
  223. package/src/slash-commands/helpers/security.ts +451 -0
  224. package/src/system-prompt.ts +7 -2
  225. package/src/task/agents.ts +2 -0
  226. package/src/task/executor.ts +20 -4
  227. package/src/task/render.ts +20 -4
  228. package/src/task/structured-subagent.ts +6 -4
  229. package/src/thinking.ts +29 -10
  230. package/src/tools/acp-bridge.ts +52 -8
  231. package/src/tools/browser.ts +5 -0
  232. package/src/tools/builtin-names.ts +1 -0
  233. package/src/tools/checkpoint.ts +0 -13
  234. package/src/tools/grep.ts +64 -8
  235. package/src/tools/hub/launch.ts +10 -15
  236. package/src/tools/index.ts +37 -5
  237. package/src/tools/path-utils.ts +91 -0
  238. package/src/tools/read.ts +3 -0
  239. package/src/tools/security-scan.ts +287 -0
  240. package/src/tools/write.ts +9 -3
  241. package/src/tools/xdev.ts +39 -14
  242. package/src/utils/changelog.ts +144 -4
  243. package/src/utils/git.ts +61 -14
  244. package/src/utils/jj.ts +19 -13
  245. package/src/web/search/providers/codex.ts +34 -0
  246. package/src/web/search/types.ts +1 -1
@@ -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
 
@@ -956,6 +962,7 @@ export class AgentSession {
956
962
  scheduleAgentContinue: options => this.#scheduleAgentContinue(options),
957
963
  waitForSessionMessagePersistence: message => this.#waitForSessionMessagePersistence(message),
958
964
  appendSessionMessage: message => this.#appendSessionMessage(message),
965
+ persistedAssistantEntryId: message => (message as PersistedAssistantMessage)[kPersistedSessionEntryId],
959
966
  sessionMessageAlreadyPersisted: message => this.#sessionMessageAlreadyPersisted(message),
960
967
  setModelWithProviderSessionReset: model => this.#setModelWithProviderSessionReset(model),
961
968
  resetCurrentResponsesProviderSession: reason => this.#resetCurrentResponsesProviderSession(reason),
@@ -1274,8 +1281,8 @@ export class AgentSession {
1274
1281
  hasPendingNextTurnMessages: () => this.#pendingNextTurnMessages.length > 0,
1275
1282
  convertToLlmForSideRequest: messages => this.#convertToLlmForSideRequest(messages),
1276
1283
  effectiveServiceTier: model => this.#models.effectiveServiceTier(model),
1277
- resolveContextPromotionTarget: (model, contextWindow) =>
1278
- this.#maintenance.resolveContextPromotionTarget(model, contextWindow),
1284
+ resolveContextPromotionTarget: (model, contextWindow, signal) =>
1285
+ this.#maintenance.resolveContextPromotionTarget(model, contextWindow, signal),
1279
1286
  resolveCompactionModelCandidates: (model, availableModels) =>
1280
1287
  this.#maintenance.resolveCompactionModelCandidates(model, availableModels),
1281
1288
  resolveRetryFallbackRole: (selector, model) => this.#recovery.resolveRetryFallbackRole(selector, model),
@@ -1290,12 +1297,17 @@ export class AgentSession {
1290
1297
  this.#advisors = new SessionAdvisors(advisorsHost, {
1291
1298
  enabled: this.settings.get("advisor.enabled"),
1292
1299
  tools: config.advisorTools,
1300
+ createGrepTool: config.advisorCreateGrepTool,
1301
+ createEditTool: config.advisorCreateEditTool,
1302
+ getToolContext: config.advisorGetToolContext,
1303
+ mcpResources: config.advisorMcpResources,
1293
1304
  watchdogPrompt: config.advisorWatchdogPrompt,
1294
1305
  sharedInstructions: config.advisorSharedInstructions,
1295
1306
  contextPrompt: config.advisorContextPrompt,
1296
1307
  configs: config.advisorConfigs,
1297
1308
  streamFn: config.advisorStreamFn,
1298
1309
  transformProviderContext: config.transformProviderContext,
1310
+ initialCosts: config.initialAdvisorCosts,
1299
1311
  });
1300
1312
 
1301
1313
  const maintenanceHost: SessionMaintenanceHost = {
@@ -1401,7 +1413,10 @@ export class AgentSession {
1401
1413
  },
1402
1414
  resetTodoCycle: () => this.#todo.resetCycle(),
1403
1415
  buildDisplaySessionContext: () => this.buildDisplaySessionContext(),
1404
- resetAdvisorRuntimes: () => this.#advisors.resetAllRuntimes(),
1416
+ resetAdvisorSessionState: () => this.#advisors.resetSessionState(),
1417
+ drainAndDetachAdvisorRecorders: () => this.#advisors.drainAndDetachRecorders(),
1418
+ reattachAdvisorRecorderFeeds: () => this.#advisors.reattachRecorderFeeds(),
1419
+ clearAdvisorCost: () => this.#advisors.clearCost(),
1405
1420
  syncTodoPhasesFromBranch: () => this.#todo.syncFromBranch(),
1406
1421
  };
1407
1422
  this.#handoff = new SessionHandoff(handoffHost);
@@ -2061,6 +2076,9 @@ export class AgentSession {
2061
2076
  const cache = this.#persistedMessageKeys;
2062
2077
  const wasFresh = cache !== undefined && cache.anchor === this.#persistedMessageKeysAnchor();
2063
2078
  const entryId = this.sessionManager.appendMessage(message);
2079
+ if (message.role === "assistant") {
2080
+ (message as PersistedAssistantMessage)[kPersistedSessionEntryId] = entryId;
2081
+ }
2064
2082
  const key = sessionMessagePersistenceKey(message);
2065
2083
  if (wasFresh && cache && key) {
2066
2084
  cache.keys.add(key);
@@ -2413,6 +2431,15 @@ export class AgentSession {
2413
2431
  },
2414
2432
  costUsd: assistantMsg.usage.cost.total,
2415
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
+ );
2416
2443
  }
2417
2444
  if (event.message.role === "toolResult") {
2418
2445
  const { toolName, toolCallId, isError, content } = event.message;
@@ -2596,6 +2623,11 @@ export class AgentSession {
2596
2623
  return;
2597
2624
  }
2598
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
+
2599
2631
  let compactionResult = COMPACTION_CHECK_NONE;
2600
2632
  let checkedCompaction = false;
2601
2633
  if (activeGoal) {
@@ -3380,6 +3412,14 @@ export class AgentSession {
3380
3412
  this.agent.setMetadataResolver((provider: string) =>
3381
3413
  buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage),
3382
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
+ }
3383
3423
  // Keep every live advisor's provider identity in lockstep with the primary's
3384
3424
  // across every session-boundary transition — including branch paths that
3385
3425
  // skip conversation restore — so advisors never emit the previous
@@ -4337,6 +4377,7 @@ export class AgentSession {
4337
4377
  this.agent.clearDeferredToolDirectives();
4338
4378
  this.#toolChoiceQueue.clear();
4339
4379
  this.#tools.clearAcpPermissionDecisions();
4380
+ this.#tools.resetAnnouncedMounts();
4340
4381
  }
4341
4382
 
4342
4383
  /**
@@ -5170,6 +5211,7 @@ export class AgentSession {
5170
5211
  void this.dispose().finally(() => process.exit(0));
5171
5212
  },
5172
5213
  getContextUsage: () => this.getContextUsage(),
5214
+ getAsyncJobSnapshot: () => this.getAsyncJobSnapshot(),
5173
5215
  waitForIdle: () => this.waitForIdle(),
5174
5216
  newSession: async options => {
5175
5217
  const success = await this.newSession({ parentSession: options?.parentSession });
@@ -5988,6 +6030,7 @@ export class AgentSession {
5988
6030
  }
5989
6031
 
5990
6032
  this.#disconnectFromAgent();
6033
+ let advisorRecordersDetached = false;
5991
6034
  await this.abort();
5992
6035
  this.#cancelOwnAsyncJobs();
5993
6036
  this.#closeAllProviderSessions("new session");
@@ -5995,69 +6038,73 @@ export class AgentSession {
5995
6038
  const bashTransition = this.#bash.beginSessionTransition({ persistDetached: options?.drop !== true });
5996
6039
  let sessionTransitioned = false;
5997
6040
  try {
5998
- this.agent.reset();
5999
- if (options?.drop && previousSessionFile) {
6000
- // Detach the advisor recorder feed and drain its writer BEFORE deleting the
6001
- // old artifacts dir: `await this.abort()` only stops the primary, so a still-
6002
- // running advisor turn could otherwise finish, emit `message_end`, and recreate
6003
- // `<old>/__advisor.jsonl`. #resetAdvisorSessionState (after newSession) re-primes
6004
- // the advisor and re-attaches the feed at the new session's path.
6005
- await this.#advisors.detachAndCloseRecorders();
6006
- try {
6007
- await this.sessionManager.dropSession(previousSessionFile);
6008
- } catch (err) {
6009
- 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();
6010
6053
  }
6011
- } else {
6012
- 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);
6013
6066
  }
6014
- await this.sessionManager.newSession({
6015
- ...options,
6016
- additionalDirectories: this.settings.get("workspace.additionalDirectories"),
6017
- });
6018
- this.#bash.markSessionTransition(bashTransition);
6019
- // The new session owns the transcript from here, so the previous
6020
- // conversation's advisor spend is retired with it. Clearing at the commit
6021
- // point keeps the status line honest even if a later step below throws.
6022
- this.#advisors.clearCost();
6023
- sessionTransitioned = true;
6024
- } finally {
6025
- this.#bash.finishSessionTransition(bashTransition, sessionTransitioned);
6026
- }
6027
6067
 
6028
- this.#clearSessionScopedToolState();
6029
- this.#clearCheckpointRuntimeState();
6030
- this.setTodoPhases([]);
6031
- this.#freshProviderSessionId = undefined;
6032
- this.#clearInheritedProviderPromptCacheKey();
6033
- this.#syncAgentSessionId();
6034
- this.#memory.rekeyForCurrentSessionId();
6035
- await this.#memory.resetContextForNewTranscript();
6036
- this.#pendingNextTurnMessages = [];
6037
- 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;
6038
6078
 
6039
- this.sessionManager.appendThinkingLevelChange(this.thinkingLevel, this.configuredThinkingLevel());
6040
- this.sessionManager.appendServiceTierChange(this.#models.serviceTierEntry());
6079
+ this.sessionManager.appendThinkingLevelChange(this.thinkingLevel, this.configuredThinkingLevel());
6080
+ this.sessionManager.appendServiceTierChange(this.#models.serviceTierEntry());
6041
6081
 
6042
- this.#todo.resetCycle();
6043
- this.#planReferenceSent = false;
6044
- this.#planReferencePath = "local://PLAN.md";
6045
- this.#advisors.resetSessionState();
6046
- this.#reconnectToAgent();
6047
- // The workspace-roots block must reflect the new session's directory set,
6048
- // not the previous session's refresh before the next turn goes out.
6049
- 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();
6050
6091
 
6051
- // Emit session_switch event with reason "new" to hooks
6052
- if (this.#extensionRunner) {
6053
- await this.#extensionRunner.emit({
6054
- type: "session_switch",
6055
- reason: "new",
6056
- previousSessionFile,
6057
- });
6058
- }
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
+ }
6059
6100
 
6060
- return true;
6101
+ return true;
6102
+ } finally {
6103
+ if (advisorRecordersDetached) {
6104
+ if (sessionTransitioned) this.#advisors.resetSessionState();
6105
+ else this.#advisors.reattachRecorderFeeds();
6106
+ }
6107
+ }
6061
6108
  }
6062
6109
 
6063
6110
  /**
@@ -6093,59 +6140,70 @@ export class AgentSession {
6093
6140
  await this.#bash.flushPending();
6094
6141
  // Flush current session to ensure all entries are written
6095
6142
  await this.sessionManager.flush();
6096
- const bashTransition = this.#bash.beginSessionTransition();
6097
-
6098
- // Fork the session (creates new session file with same entries)
6099
- let forkResult: { oldSessionFile: string; newSessionFile: string } | undefined;
6143
+ let advisorRecordersDetached = false;
6100
6144
  try {
6101
- forkResult = await this.sessionManager.fork();
6102
- } catch (error) {
6103
- this.#bash.finishSessionTransition(bashTransition, false);
6104
- throw error;
6105
- }
6106
- if (!forkResult) {
6107
- this.#bash.finishSessionTransition(bashTransition, false);
6108
- return false;
6109
- }
6110
- this.#bash.markSessionTransition(bashTransition);
6111
- 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);
6112
6165
 
6113
- // Copy artifacts directory if it exists
6114
- const oldArtifactDir = forkResult.oldSessionFile.slice(0, -6);
6115
- 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);
6116
6169
 
6117
- try {
6118
- const oldDirStat = await fs.promises.stat(oldArtifactDir);
6119
- if (oldDirStat.isDirectory()) {
6120
- 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
+ }
6121
6183
  }
6122
- } catch (err) {
6123
- if (!isEnoent(err)) {
6124
- logger.warn("Failed to copy artifacts during fork", {
6125
- oldArtifactDir,
6126
- newArtifactDir,
6127
- 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,
6128
6200
  });
6129
6201
  }
6130
- }
6131
-
6132
- // Update agent session ID
6133
- this.#freshProviderSessionId = undefined;
6134
- this.#adoptInheritedProviderPromptCacheKey();
6135
- this.#syncAgentSessionId();
6136
- this.#memory.rekeyForCurrentSessionId();
6137
- await this.#memory.resetContextForNewTranscript();
6138
6202
 
6139
- // Emit session_switch event with reason "fork" to hooks
6140
- if (this.#extensionRunner) {
6141
- await this.#extensionRunner.emit({
6142
- type: "session_switch",
6143
- reason: "fork",
6144
- previousSessionFile,
6145
- });
6203
+ return true;
6204
+ } finally {
6205
+ if (advisorRecordersDetached) this.#advisors.reattachRecorderFeeds();
6146
6206
  }
6147
-
6148
- return true;
6149
6207
  }
6150
6208
 
6151
6209
  /** Move the active session and artifacts after enforcing mode transition invariants. */
@@ -6519,13 +6577,30 @@ export class AgentSession {
6519
6577
 
6520
6578
  async #setModelWithProviderSessionReset(model: Model): Promise<void> {
6521
6579
  const currentModel = this.model;
6580
+ const isChanging = !currentModel || !modelsAreEqual(currentModel, model);
6522
6581
  if (currentModel) {
6523
6582
  this.#closeProviderSessionsForModelSwitch(currentModel, model);
6524
- if (!modelsAreEqual(currentModel, model)) {
6583
+ if (isChanging) {
6525
6584
  this.#clearInheritedProviderPromptCacheKey();
6526
6585
  }
6527
6586
  }
6528
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
+ }
6529
6604
 
6530
6605
  // Re-evaluate append-only context mode — provider or setting may have changed
6531
6606
  this.#syncAppendOnlyContext(model);
@@ -7044,6 +7119,11 @@ export class AgentSession {
7044
7119
  this.#scheduledHiddenNextTurnGeneration = undefined;
7045
7120
 
7046
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
+ }
7047
7127
  await this.sessionManager.setSessionFile(sessionPath);
7048
7128
  this.#bash.markSessionTransition(bashTransition);
7049
7129
  if (switchingToDifferentSession) {
@@ -7156,7 +7236,7 @@ export class AgentSession {
7156
7236
  if (switchingToDifferentSession) {
7157
7237
  await this.#memory.resetContextForNewTranscript();
7158
7238
  }
7159
- if (switchingToDifferentSession) {
7239
+ if (switchingToDifferentSession || didReloadConversationChange) {
7160
7240
  this.#clearSessionScopedToolState();
7161
7241
  }
7162
7242
  this.#reconnectToAgent();
@@ -7179,9 +7259,14 @@ export class AgentSession {
7179
7259
  error: String(refreshErr),
7180
7260
  });
7181
7261
  }
7182
- // Only a committed switch retires the previous conversation's advisor spend:
7183
- // an earlier clear would be lost work if any step above rolled the switch back.
7184
- 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
+ }
7185
7270
  this.#bash.finishSessionTransition(bashTransition, true);
7186
7271
  return true;
7187
7272
  } catch (error) {
@@ -7202,13 +7287,32 @@ export class AgentSession {
7202
7287
  this.#pendingRewindReport = previousPendingRewindReport;
7203
7288
  this.#lastCompletedRewind = previousLastCompletedRewind;
7204
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;
7205
7303
  if (previousModel) {
7304
+ const rolledBackModel = this.model;
7206
7305
  this.agent.setModel(previousModel);
7306
+ modelRolledBack = !modelsAreEqual(rolledBackModel, previousModel);
7207
7307
  }
7208
7308
  this.#models.restoreThinkingSnapshot(previousThinkingLevel, previousAutoThinking, previousAutoResolvedLevel);
7209
7309
  this.#models.restoreServiceTiers(previousServiceTierByFamily);
7310
+ if (modelRolledBack) {
7311
+ this.#emit({ type: "model_changed" });
7312
+ }
7210
7313
  this.#todo.syncFromBranch();
7211
7314
  this.#advisors.resetAllRuntimes();
7315
+ this.#advisors.reattachRecorderFeeds();
7212
7316
  this.#reconnectToAgent();
7213
7317
  try {
7214
7318
  await this.#sessionSwitchReconciler?.();
@@ -7276,45 +7380,57 @@ export class AgentSession {
7276
7380
  await this.#drainAutolearnCapture();
7277
7381
 
7278
7382
  let sessionTransitioned = false;
7383
+ let advisorRecordersDetached = false;
7279
7384
  try {
7280
- if (!selectedEntry.parentId) {
7281
- await this.sessionManager.newSession({ parentSession: previousSessionFile });
7282
- } else {
7283
- 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);
7284
7398
  }
7285
- this.#bash.markSessionTransition(bashTransition);
7286
- this.#advisors.clearCost();
7287
- sessionTransitioned = true;
7288
- } finally {
7289
- this.#bash.finishSessionTransition(bashTransition, sessionTransitioned);
7290
- }
7291
- this.#clearSessionScopedToolState();
7292
- this.#rehydrateCheckpointRewindState();
7293
- this.#todo.syncFromBranch();
7294
- this.#freshProviderSessionId = undefined;
7295
- this.#clearInheritedProviderPromptCacheKey();
7296
- this.#syncAgentSessionId();
7297
- this.#memory.rekeyForCurrentSessionId();
7298
- 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();
7299
7407
 
7300
- // Reload messages from entries (works for both file and in-memory mode)
7301
- const sessionContext = this.buildDisplaySessionContext();
7408
+ // Reload messages from entries (works for both file and in-memory mode)
7409
+ const sessionContext = this.buildDisplaySessionContext();
7302
7410
 
7303
- // Emit session_branch event to hooks (after branch completes)
7304
- if (this.#extensionRunner) {
7305
- await this.#extensionRunner.emit({
7306
- type: "session_branch",
7307
- previousSessionFile,
7308
- });
7309
- }
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
+ }
7310
7418
 
7311
- if (!skipConversationRestore) {
7312
- this.agent.replaceMessages(sessionContext.messages);
7313
- this.#advisors.resetSessionState();
7314
- this.#closeCodexProviderSessionsForHistoryRewrite();
7315
- }
7419
+ if (!skipConversationRestore) {
7420
+ this.agent.replaceMessages(sessionContext.messages);
7421
+ this.#advisors.resetSessionState();
7422
+ this.#closeCodexProviderSessionsForHistoryRewrite();
7423
+ }
7316
7424
 
7317
- 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
+ }
7318
7434
  }
7319
7435
 
7320
7436
  async branchFromBtw(
@@ -7378,44 +7494,55 @@ export class AgentSession {
7378
7494
  await this.#drainAutolearnCapture();
7379
7495
 
7380
7496
  let sessionTransitioned = false;
7497
+ let advisorRecordersDetached = false;
7381
7498
  try {
7382
- this.sessionManager.createBranchedSession(leafId);
7383
- this.#bash.markSessionTransition(bashTransition);
7384
- this.#advisors.clearCost();
7385
- sessionTransitioned = true;
7386
- } finally {
7387
- this.#bash.finishSessionTransition(bashTransition, sessionTransitioned);
7388
- }
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
+ }
7389
7509
 
7390
- this.#clearSessionScopedToolState();
7510
+ this.#clearSessionScopedToolState();
7391
7511
 
7392
- this.#rehydrateCheckpointRewindState();
7393
- this.sessionManager.appendMessage({
7394
- role: "user",
7395
- content: [{ type: "text", text: question }],
7396
- timestamp: Date.now(),
7397
- });
7398
- this.sessionManager.appendMessage(sanitizeAssistantForReparentedHistory(assistantMessage));
7399
- this.#todo.syncFromBranch();
7400
- this.#freshProviderSessionId = undefined;
7401
- this.#syncAgentSessionId();
7402
- this.#memory.rekeyForCurrentSessionId();
7403
- 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();
7404
7524
 
7405
- const sessionContext = this.buildDisplaySessionContext();
7525
+ const sessionContext = this.buildDisplaySessionContext();
7406
7526
 
7407
- if (this.#extensionRunner) {
7408
- await this.#extensionRunner.emit({
7409
- type: "session_branch",
7410
- previousSessionFile,
7411
- });
7412
- }
7527
+ if (this.#extensionRunner) {
7528
+ await this.#extensionRunner.emit({
7529
+ type: "session_branch",
7530
+ previousSessionFile,
7531
+ });
7532
+ }
7413
7533
 
7414
- this.agent.replaceMessages(sessionContext.messages);
7415
- this.#advisors.resetSessionState();
7416
- this.#closeCodexProviderSessionsForHistoryRewrite();
7534
+ this.agent.replaceMessages(sessionContext.messages);
7535
+ this.#advisors.resetSessionState();
7536
+ this.#closeCodexProviderSessionsForHistoryRewrite();
7537
+ advisorRecordersDetached = false;
7417
7538
 
7418
- 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
+ }
7419
7546
  }
7420
7547
 
7421
7548
  // =========================================================================