@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.2

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 (238) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/cli/startup-cwd.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +3 -0
  5. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  6. package/dist/types/commit/analysis/summary.d.ts +2 -2
  7. package/dist/types/commit/changelog/generate.d.ts +2 -2
  8. package/dist/types/commit/changelog/index.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  12. package/dist/types/commit/model-selection.d.ts +10 -4
  13. package/dist/types/config/api-key-resolver.d.ts +34 -0
  14. package/dist/types/config/keybindings.d.ts +2 -2
  15. package/dist/types/config/model-provider-priority.d.ts +1 -0
  16. package/dist/types/config/model-registry.d.ts +17 -1
  17. package/dist/types/config/model-resolver.d.ts +4 -1
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/config/settings.d.ts +7 -2
  20. package/dist/types/dap/config.d.ts +14 -1
  21. package/dist/types/dap/types.d.ts +10 -0
  22. package/dist/types/debug/report-bundle.d.ts +3 -0
  23. package/dist/types/edit/file-snapshot-store.d.ts +18 -10
  24. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  25. package/dist/types/extensibility/extensions/types.d.ts +4 -1
  26. package/dist/types/lsp/client.d.ts +10 -0
  27. package/dist/types/lsp/utils.d.ts +3 -2
  28. package/dist/types/main.d.ts +3 -9
  29. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  30. package/dist/types/modes/components/chat-block.d.ts +64 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +4 -1
  32. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  33. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  34. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  35. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  36. package/dist/types/modes/components/status-line.d.ts +2 -0
  37. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  39. package/dist/types/modes/controllers/event-controller.d.ts +17 -1
  40. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  43. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  44. package/dist/types/modes/interactive-mode.d.ts +16 -5
  45. package/dist/types/modes/magic-keywords.d.ts +1 -1
  46. package/dist/types/modes/markdown-prose.d.ts +1 -1
  47. package/dist/types/modes/theme/theme.d.ts +1 -1
  48. package/dist/types/modes/types.d.ts +21 -5
  49. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  50. package/dist/types/modes/workflow.d.ts +3 -3
  51. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  52. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +21 -0
  55. package/dist/types/session/auth-storage.d.ts +1 -1
  56. package/dist/types/session/messages.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +8 -3
  58. package/dist/types/slash-commands/types.d.ts +4 -6
  59. package/dist/types/task/executor.d.ts +17 -0
  60. package/dist/types/task/index.d.ts +1 -0
  61. package/dist/types/task/render.d.ts +3 -2
  62. package/dist/types/tools/archive-reader.d.ts +5 -0
  63. package/dist/types/tools/ast-edit.d.ts +3 -0
  64. package/dist/types/tools/ast-grep.d.ts +3 -0
  65. package/dist/types/tools/bash.d.ts +1 -0
  66. package/dist/types/tools/eval.d.ts +8 -0
  67. package/dist/types/tools/find.d.ts +8 -4
  68. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  69. package/dist/types/tools/github-cache.d.ts +12 -0
  70. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  71. package/dist/types/tools/memory-render.d.ts +4 -1
  72. package/dist/types/tools/path-utils.d.ts +8 -0
  73. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  74. package/dist/types/tools/render-utils.d.ts +5 -9
  75. package/dist/types/tools/search.d.ts +6 -2
  76. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  77. package/dist/types/tools/todo.d.ts +3 -2
  78. package/dist/types/tools/write.d.ts +3 -0
  79. package/dist/types/tools/yield.d.ts +8 -0
  80. package/dist/types/tui/output-block.d.ts +16 -4
  81. package/dist/types/tui/status-line.d.ts +3 -0
  82. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  83. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  84. package/package.json +9 -9
  85. package/src/auto-thinking/classifier.ts +5 -1
  86. package/src/cli/args.ts +3 -1
  87. package/src/cli/dry-balance-cli.ts +54 -21
  88. package/src/cli/gallery-cli.ts +4 -1
  89. package/src/cli/gallery-fixtures/misc.ts +29 -0
  90. package/src/cli/startup-cwd.ts +68 -0
  91. package/src/commands/launch.ts +3 -0
  92. package/src/commit/analysis/conventional.ts +2 -2
  93. package/src/commit/analysis/summary.ts +2 -2
  94. package/src/commit/changelog/generate.ts +2 -2
  95. package/src/commit/changelog/index.ts +2 -2
  96. package/src/commit/map-reduce/index.ts +3 -3
  97. package/src/commit/map-reduce/map-phase.ts +2 -2
  98. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  99. package/src/commit/model-selection.ts +36 -11
  100. package/src/commit/pipeline.ts +4 -4
  101. package/src/config/api-key-resolver.ts +58 -0
  102. package/src/config/model-provider-priority.ts +55 -0
  103. package/src/config/model-registry.ts +29 -24
  104. package/src/config/model-resolver.ts +39 -7
  105. package/src/config/settings-schema.ts +10 -0
  106. package/src/config/settings.ts +106 -43
  107. package/src/dap/config.ts +41 -2
  108. package/src/dap/defaults.json +1 -0
  109. package/src/dap/session.ts +1 -0
  110. package/src/dap/types.ts +10 -0
  111. package/src/debug/index.ts +47 -53
  112. package/src/debug/raw-sse-buffer.ts +7 -4
  113. package/src/debug/report-bundle.ts +9 -0
  114. package/src/edit/file-snapshot-store.ts +33 -1
  115. package/src/edit/hashline/filesystem.ts +2 -1
  116. package/src/edit/renderer.ts +82 -78
  117. package/src/eval/__tests__/llm-bridge.test.ts +110 -31
  118. package/src/eval/js/context-manager.ts +32 -15
  119. package/src/eval/llm-bridge.ts +22 -6
  120. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  121. package/src/eval/py/executor.ts +23 -11
  122. package/src/eval/py/prelude.py +1 -1
  123. package/src/extensibility/extensions/types.ts +10 -1
  124. package/src/goals/tools/goal-tool.ts +36 -26
  125. package/src/internal-urls/docs-index.generated.ts +8 -8
  126. package/src/lsp/client.ts +23 -11
  127. package/src/lsp/config.ts +11 -1
  128. package/src/lsp/index.ts +61 -9
  129. package/src/lsp/utils.ts +3 -2
  130. package/src/main.ts +100 -72
  131. package/src/mcp/tool-bridge.ts +2 -0
  132. package/src/memories/index.ts +14 -7
  133. package/src/mnemopi/backend.ts +5 -1
  134. package/src/modes/acp/acp-agent.ts +33 -26
  135. package/src/modes/components/assistant-message.ts +2 -9
  136. package/src/modes/components/chat-block.ts +111 -0
  137. package/src/modes/components/copy-selector.ts +1 -44
  138. package/src/modes/components/custom-editor.ts +164 -109
  139. package/src/modes/components/custom-message.ts +1 -3
  140. package/src/modes/components/execution-shared.ts +1 -2
  141. package/src/modes/components/hook-message.ts +1 -3
  142. package/src/modes/components/model-selector.ts +59 -13
  143. package/src/modes/components/oauth-selector.ts +33 -7
  144. package/src/modes/components/overlay-box.ts +108 -0
  145. package/src/modes/components/plan-review-overlay.ts +799 -0
  146. package/src/modes/components/plan-toc.ts +138 -0
  147. package/src/modes/components/read-tool-group.ts +20 -4
  148. package/src/modes/components/skill-message.ts +0 -1
  149. package/src/modes/components/status-line.ts +19 -4
  150. package/src/modes/components/tips.txt +2 -1
  151. package/src/modes/components/todo-reminder.ts +0 -2
  152. package/src/modes/components/tool-execution.ts +68 -88
  153. package/src/modes/components/transcript-container.ts +84 -24
  154. package/src/modes/components/user-message.ts +2 -3
  155. package/src/modes/controllers/command-controller-shared.ts +7 -6
  156. package/src/modes/controllers/command-controller.ts +57 -55
  157. package/src/modes/controllers/event-controller.ts +67 -40
  158. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  159. package/src/modes/controllers/input-controller.ts +170 -126
  160. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  161. package/src/modes/controllers/selector-controller.ts +23 -25
  162. package/src/modes/controllers/streaming-reveal.ts +212 -0
  163. package/src/modes/controllers/tan-command-controller.ts +173 -0
  164. package/src/modes/interactive-mode.ts +274 -112
  165. package/src/modes/magic-keywords.ts +1 -1
  166. package/src/modes/markdown-prose.ts +1 -1
  167. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  168. package/src/modes/theme/shimmer.ts +20 -9
  169. package/src/modes/theme/theme-schema.json +1 -1
  170. package/src/modes/theme/theme.ts +8 -4
  171. package/src/modes/types.ts +21 -7
  172. package/src/modes/utils/copy-targets.ts +133 -27
  173. package/src/modes/utils/ui-helpers.ts +44 -46
  174. package/src/modes/workflow.ts +10 -10
  175. package/src/plan-mode/approved-plan.ts +66 -43
  176. package/src/plan-mode/plan-protection.ts +4 -4
  177. package/src/prompts/system/background-tan-dispatch.md +8 -0
  178. package/src/prompts/system/plan-mode-active.md +67 -58
  179. package/src/prompts/system/plan-mode-approved.md +1 -1
  180. package/src/prompts/system/workflow-notice.md +1 -1
  181. package/src/prompts/tools/bash.md +9 -0
  182. package/src/prompts/tools/browser.md +1 -1
  183. package/src/prompts/tools/eval.md +2 -1
  184. package/src/prompts/tools/read.md +2 -2
  185. package/src/sdk.ts +37 -46
  186. package/src/session/agent-session.ts +119 -18
  187. package/src/session/auth-storage.ts +2 -0
  188. package/src/session/messages.ts +26 -0
  189. package/src/session/session-manager.ts +109 -28
  190. package/src/slash-commands/builtin-registry.ts +36 -9
  191. package/src/slash-commands/types.ts +4 -6
  192. package/src/task/executor.ts +76 -38
  193. package/src/task/index.ts +4 -0
  194. package/src/task/render.ts +211 -147
  195. package/src/tools/archive-reader.ts +64 -0
  196. package/src/tools/ask.ts +119 -164
  197. package/src/tools/ast-edit.ts +98 -71
  198. package/src/tools/ast-grep.ts +37 -43
  199. package/src/tools/bash.ts +57 -6
  200. package/src/tools/browser/tab-supervisor.ts +13 -1
  201. package/src/tools/browser/tab-worker.ts +33 -4
  202. package/src/tools/debug.ts +20 -8
  203. package/src/tools/eval.ts +13 -2
  204. package/src/tools/fetch.ts +297 -7
  205. package/src/tools/find.ts +51 -30
  206. package/src/tools/gh-cache-invalidation.ts +200 -0
  207. package/src/tools/gh-renderer.ts +81 -42
  208. package/src/tools/github-cache.ts +25 -0
  209. package/src/tools/grouped-file-output.ts +272 -48
  210. package/src/tools/image-gen.ts +150 -103
  211. package/src/tools/inspect-image-renderer.ts +63 -41
  212. package/src/tools/inspect-image.ts +10 -3
  213. package/src/tools/job.ts +3 -4
  214. package/src/tools/memory-render.ts +4 -1
  215. package/src/tools/path-utils.ts +28 -2
  216. package/src/tools/plan-mode-guard.ts +66 -39
  217. package/src/tools/read.ts +48 -28
  218. package/src/tools/render-utils.ts +21 -37
  219. package/src/tools/resolve.ts +14 -0
  220. package/src/tools/search-tool-bm25.ts +36 -23
  221. package/src/tools/search.ts +118 -81
  222. package/src/tools/sqlite-reader.ts +9 -12
  223. package/src/tools/todo.ts +118 -52
  224. package/src/tools/write.ts +83 -64
  225. package/src/tools/yield.ts +10 -1
  226. package/src/tui/output-block.ts +60 -13
  227. package/src/tui/status-line.ts +5 -1
  228. package/src/utils/commit-message-generator.ts +11 -3
  229. package/src/utils/enhanced-paste.ts +230 -0
  230. package/src/utils/title-generator.ts +2 -1
  231. package/src/web/search/providers/anthropic.ts +25 -19
  232. package/src/web/search/providers/codex.ts +37 -8
  233. package/src/web/search/providers/exa.ts +11 -3
  234. package/src/web/search/providers/kimi.ts +28 -17
  235. package/src/web/search/providers/parallel.ts +35 -24
  236. package/src/web/search/providers/synthetic.ts +8 -6
  237. package/src/web/search/providers/tavily.ts +9 -8
  238. package/src/web/search/providers/zai.ts +8 -6
@@ -109,6 +109,7 @@ import {
109
109
  extractExplicitThinkingSelector,
110
110
  formatModelSelectorValue,
111
111
  formatModelString,
112
+ getModelMatchPreferences,
112
113
  parseModelString,
113
114
  type ResolvedModelRoleValue,
114
115
  resolveModelRoleValue,
@@ -283,6 +284,11 @@ export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
283
284
  export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime">;
284
285
 
285
286
  const EMPTY_STOP_MAX_RETRIES = 3;
287
+ const NON_WHITESPACE_RE = /\S/;
288
+
289
+ function hasNonWhitespace(value: string): boolean {
290
+ return NON_WHITESPACE_RE.test(value);
291
+ }
286
292
 
287
293
  export interface AsyncJobSnapshot {
288
294
  running: AsyncJobSnapshotItem[];
@@ -471,6 +477,12 @@ export interface SessionStats {
471
477
  cost: number;
472
478
  }
473
479
 
480
+ export interface FreshSessionResult {
481
+ previousSessionId: string;
482
+ sessionId: string;
483
+ closedProviderSessions: number;
484
+ }
485
+
474
486
  /** Internal marker for hook messages queued through the agent loop */
475
487
  // ============================================================================
476
488
  // Constants
@@ -922,6 +934,7 @@ export class AgentSession {
922
934
  #agentId: string | undefined;
923
935
  #agentRegistry: AgentRegistry | undefined;
924
936
  #providerSessionId: string | undefined;
937
+ #freshProviderSessionId: string | undefined;
925
938
  #isDisposed = false;
926
939
  // Extension system
927
940
  #extensionRunner: ExtensionRunner | undefined = undefined;
@@ -1275,6 +1288,14 @@ export class AgentSession {
1275
1288
  return this.#modelRegistry;
1276
1289
  }
1277
1290
 
1291
+ get asyncJobManager(): AsyncJobManager | undefined {
1292
+ return this.#asyncJobManager;
1293
+ }
1294
+
1295
+ getAgentId(): string | undefined {
1296
+ return this.#agentId;
1297
+ }
1298
+
1278
1299
  /** Advance the tool-choice queue and return the next directive for the upcoming LLM call. */
1279
1300
  nextToolChoice(): ToolChoice | undefined {
1280
1301
  return this.#toolChoiceQueue.nextToolChoice();
@@ -1681,7 +1702,7 @@ export class AgentSession {
1681
1702
  // Abort the stream immediately — do not gate on extension callbacks
1682
1703
  this.#ttsrAbortPending = true;
1683
1704
  this.#ensureTtsrResumePromise();
1684
- this.agent.abort();
1705
+ this.agent.abort(this.#formatTtsrAbortReason(matches));
1685
1706
  // Notify extensions (fire-and-forget, does not block abort)
1686
1707
  this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
1687
1708
  // Schedule retry after a short delay
@@ -2162,6 +2183,12 @@ export class AgentSession {
2162
2183
  }
2163
2184
  }
2164
2185
 
2186
+ #formatTtsrAbortReason(rules: Rule[]): string {
2187
+ const label = rules.length === 1 ? "rule" : "rules";
2188
+ const ruleNames = rules.map(rule => rule.name).join(", ");
2189
+ return `TTSR matched ${label}: ${ruleNames}`;
2190
+ }
2191
+
2165
2192
  /** Get TTSR injection payload and clear pending injections. */
2166
2193
  #getTtsrInjectionContent(): { content: string; rules: Rule[] } | undefined {
2167
2194
  if (this.#pendingTtsrInjections.length === 0) return undefined;
@@ -2185,13 +2212,20 @@ export class AgentSession {
2185
2212
  * project, `~`-relative when it lives under home, else the raw path.
2186
2213
  */
2187
2214
  #displayRulePath(rulePath: string): string {
2188
- const cwdRel = relativePathWithinRoot(this.sessionManager.getCwd(), rulePath);
2215
+ const cwdRel =
2216
+ relativePathWithinRoot(this.sessionManager.getCwd(), rulePath) ??
2217
+ this.#displayPathWithinRoot(this.sessionManager.getCwd(), rulePath);
2189
2218
  if (cwdRel) return cwdRel;
2190
2219
  const homeRel = relativePathWithinRoot(os.homedir(), rulePath);
2191
2220
  if (homeRel) return `~/${homeRel}`;
2192
2221
  return rulePath;
2193
2222
  }
2194
2223
 
2224
+ #displayPathWithinRoot(root: string, candidate: string): string | null {
2225
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
2226
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : null;
2227
+ }
2228
+
2195
2229
  #addPendingTtsrInjections(rules: Rule[]): void {
2196
2230
  const seen = new Set(this.#pendingTtsrInjections.map(rule => rule.name));
2197
2231
  for (const rule of rules) {
@@ -2946,6 +2980,10 @@ export class AgentSession {
2946
2980
  this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
2947
2981
  }
2948
2982
 
2983
+ #activeProviderSessionId(sessionId?: string): string {
2984
+ return this.#freshProviderSessionId ?? this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
2985
+ }
2986
+
2949
2987
  /**
2950
2988
  * Set agent.sessionId from the session manager and install a dynamic
2951
2989
  * metadata resolver so every Anthropic API request carries
@@ -2958,7 +2996,7 @@ export class AgentSession {
2958
2996
  * `#syncAgentSessionId()` on every such event.
2959
2997
  */
2960
2998
  #syncAgentSessionId(sessionId?: string): void {
2961
- const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
2999
+ const sid = this.#activeProviderSessionId(sessionId);
2962
3000
  this.agent.sessionId = sid;
2963
3001
  this.agent.setMetadataResolver((provider: string) =>
2964
3002
  buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage),
@@ -3088,6 +3126,23 @@ export class AgentSession {
3088
3126
  this.#providerSessionState.clear();
3089
3127
  }
3090
3128
 
3129
+ freshSession(): FreshSessionResult | undefined {
3130
+ if (this.isStreaming) return undefined;
3131
+ const previousSessionId = this.sessionId;
3132
+ const closedProviderSessions = this.#providerSessionState.size;
3133
+ this.#closeAllProviderSessions("fresh session");
3134
+ this.#freshProviderSessionId = Bun.randomUUIDv7();
3135
+ this.#syncAgentSessionId();
3136
+ this.#rekeyHindsightMemoryForCurrentSessionId();
3137
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
3138
+ this.agent.appendOnlyContext?.invalidateForModelChange();
3139
+ return {
3140
+ previousSessionId,
3141
+ sessionId: this.sessionId,
3142
+ closedProviderSessions,
3143
+ };
3144
+ }
3145
+
3091
3146
  // =========================================================================
3092
3147
  // Read-only State Access
3093
3148
  // =========================================================================
@@ -3992,7 +4047,7 @@ export class AgentSession {
3992
4047
 
3993
4048
  /** Current session ID */
3994
4049
  get sessionId(): string {
3995
- return this.#providerSessionId ?? this.sessionManager.getSessionId();
4050
+ return this.#activeProviderSessionId();
3996
4051
  }
3997
4052
  getEvalSessionId(): string | null {
3998
4053
  if (this.#parentEvalSessionId !== undefined) return this.#parentEvalSessionId;
@@ -5091,8 +5146,13 @@ export class AgentSession {
5091
5146
 
5092
5147
  /**
5093
5148
  * Abort current operation and wait for agent to become idle.
5149
+ *
5150
+ * `reason` (e.g. `USER_INTERRUPT_LABEL`) rides the agent's `AbortController`
5151
+ * and surfaces verbatim on the aborted assistant message's `errorMessage`, so
5152
+ * the transcript can distinguish a deliberate user interrupt from an opaque
5153
+ * abort. Omit it for internal/lifecycle aborts.
5094
5154
  */
5095
- async abort(options?: { goalReason?: "interrupted" | "internal" }): Promise<void> {
5155
+ async abort(options?: { goalReason?: "interrupted" | "internal"; reason?: string }): Promise<void> {
5096
5156
  this.abortRetry();
5097
5157
  this.#promptGeneration++;
5098
5158
  this.#scheduledHiddenNextTurnGeneration = undefined;
@@ -5101,7 +5161,7 @@ export class AgentSession {
5101
5161
  this.abortBash();
5102
5162
  this.abortEval();
5103
5163
  const postPromptDrain = this.#cancelPostPromptTasks();
5104
- this.agent.abort();
5164
+ this.agent.abort(options?.reason);
5105
5165
  await postPromptDrain;
5106
5166
  await this.agent.waitForIdle();
5107
5167
  await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
@@ -5118,6 +5178,19 @@ export class AgentSession {
5118
5178
  }
5119
5179
  }
5120
5180
 
5181
+ /**
5182
+ * Abort active work, then immediately resume the agent so queued steer/follow-up
5183
+ * messages drain instead of waiting for another natural turn boundary.
5184
+ */
5185
+ async interruptAndFlushQueuedMessages(options?: { reason?: string }): Promise<void> {
5186
+ if (!this.agent.hasQueuedMessages()) return;
5187
+ await this.abort({ reason: options?.reason });
5188
+ if (!this.agent.hasQueuedMessages()) return;
5189
+ if (this.isCompacting || this.isGeneratingHandoff) return;
5190
+ await this.#maybeRestoreRetryFallbackPrimary();
5191
+ await this.agent.continue();
5192
+ }
5193
+
5121
5194
  /**
5122
5195
  * Start a new session, optionally with initial messages and parent tracking.
5123
5196
  * Clears all messages and starts a new session.
@@ -5162,6 +5235,7 @@ export class AgentSession {
5162
5235
  }
5163
5236
  await this.sessionManager.newSession(options);
5164
5237
  this.setTodoPhases([]);
5238
+ this.#freshProviderSessionId = undefined;
5165
5239
  this.#syncAgentSessionId();
5166
5240
  this.#rekeyHindsightMemoryForCurrentSessionId();
5167
5241
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -5259,6 +5333,7 @@ export class AgentSession {
5259
5333
  }
5260
5334
 
5261
5335
  // Update agent session ID
5336
+ this.#freshProviderSessionId = undefined;
5262
5337
  this.#syncAgentSessionId();
5263
5338
  this.#rekeyHindsightMemoryForCurrentSessionId();
5264
5339
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -5376,7 +5451,7 @@ export class AgentSession {
5376
5451
 
5377
5452
  const currentModel = this.model;
5378
5453
  if (!currentModel) return undefined;
5379
- const matchPreferences = { usageOrder: this.settings.getStorage()?.getModelUsageOrder() };
5454
+ const matchPreferences = getModelMatchPreferences(this.settings);
5380
5455
  const models: ResolvedRoleModel[] = [];
5381
5456
 
5382
5457
  for (const role of roleOrder) {
@@ -6226,6 +6301,7 @@ export class AgentSession {
6226
6301
  this.#cancelOwnAsyncJobs();
6227
6302
  await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
6228
6303
  this.agent.reset();
6304
+ this.#freshProviderSessionId = undefined;
6229
6305
  this.#syncAgentSessionId();
6230
6306
  this.#rekeyHindsightMemoryForCurrentSessionId();
6231
6307
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -6469,9 +6545,13 @@ export class AgentSession {
6469
6545
  this.#retryAttempt = 0;
6470
6546
  }
6471
6547
  this.#resolveRetry();
6548
+ // Tool-use orphans corrupt Anthropic message history (tool_result without
6549
+ // matching tool_use). Always remove them even when the retry cap is hit.
6550
+ if (assistantMessage.stopReason === "toolUse") {
6551
+ this.#removeEmptyStopFromActiveContext(assistantMessage);
6552
+ }
6472
6553
  return true;
6473
6554
  }
6474
-
6475
6555
  this.#removeEmptyStopFromActiveContext(assistantMessage);
6476
6556
  this.agent.appendMessage({
6477
6557
  role: "developer",
@@ -6484,12 +6564,26 @@ export class AgentSession {
6484
6564
  }
6485
6565
 
6486
6566
  #isEmptyAssistantStop(assistantMessage: AssistantMessage): boolean {
6487
- if (assistantMessage.stopReason !== "stop") return false;
6488
- return !assistantMessage.content.some(content => {
6489
- if (content.type === "text") return content.text.trim().length > 0;
6490
- if (content.type === "thinking") return content.thinking.trim().length > 0;
6491
- return content.type === "toolCall";
6492
- });
6567
+ switch (assistantMessage.stopReason) {
6568
+ case "stop":
6569
+ for (const content of assistantMessage.content) {
6570
+ if (content.type === "toolCall") return false;
6571
+ if (content.type === "text" && hasNonWhitespace(content.text)) return false;
6572
+ if (content.type === "thinking" && hasNonWhitespace(content.thinking)) return false;
6573
+ }
6574
+ return true;
6575
+ case "toolUse":
6576
+ // An orphaned toolUse stop (no tool_use block) corrupts Anthropic history:
6577
+ // a later tool_result has nothing to anchor to. Thinking alone cannot anchor
6578
+ // a tool_result, so it does not rescue a toolUse stop here.
6579
+ for (const content of assistantMessage.content) {
6580
+ if (content.type === "toolCall") return false;
6581
+ if (content.type === "text" && hasNonWhitespace(content.text)) return false;
6582
+ }
6583
+ return true;
6584
+ default:
6585
+ return false;
6586
+ }
6493
6587
  }
6494
6588
 
6495
6589
  #emptyStopRetryReminder(): string {
@@ -7073,7 +7167,7 @@ export class AgentSession {
7073
7167
 
7074
7168
  return resolveModelRoleValue(roleModelStr, availableModels, {
7075
7169
  settings: this.settings,
7076
- matchPreferences: { usageOrder: this.settings.getStorage()?.getModelUsageOrder() },
7170
+ matchPreferences: getModelMatchPreferences(this.settings),
7077
7171
  modelRegistry: this.#modelRegistry,
7078
7172
  });
7079
7173
  }
@@ -7804,11 +7898,12 @@ export class AgentSession {
7804
7898
  #isTransientTransportErrorMessage(errorMessage: string): boolean {
7805
7899
  // Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
7806
7900
  // service unavailable, provider-suggested retry, network/connection/socket errors, fetch failed,
7807
- // terminated, retry delay exceeded, Bun HTTP/2 stream resets (RST_STREAM / REFUSED_STREAM /
7808
- // ENHANCE_YOUR_CALM, surfaced verbatim from src/http/h2_client/dispatch.zig)
7901
+ // gateway upstream failures, terminated, retry delay exceeded, Bun HTTP/2 stream resets
7902
+ // (RST_STREAM / REFUSED_STREAM / ENHANCE_YOUR_CALM, surfaced verbatim from
7903
+ // src/http/h2_client/dispatch.zig)
7809
7904
  return (
7810
7905
  isUnexpectedSocketCloseMessage(errorMessage) ||
7811
- /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)/i.test(
7906
+ /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)/i.test(
7812
7907
  errorMessage,
7813
7908
  )
7814
7909
  );
@@ -8941,6 +9036,7 @@ export class AgentSession {
8941
9036
  const previousTools = [...this.agent.state.tools];
8942
9037
  const previousBaseSystemPrompt = this.#baseSystemPrompt;
8943
9038
  const previousSystemPrompt = this.agent.state.systemPrompt;
9039
+ const previousFreshProviderSessionId = this.#freshProviderSessionId;
8944
9040
  const previousFallbackSelectedMCPToolNames = previousSessionFile
8945
9041
  ? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
8946
9042
  : undefined;
@@ -8952,6 +9048,9 @@ export class AgentSession {
8952
9048
 
8953
9049
  try {
8954
9050
  await this.sessionManager.setSessionFile(sessionPath);
9051
+ if (switchingToDifferentSession) {
9052
+ this.#freshProviderSessionId = undefined;
9053
+ }
8955
9054
  this.#syncAgentSessionId();
8956
9055
  this.#rekeyHindsightMemoryForCurrentSessionId();
8957
9056
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -9061,6 +9160,7 @@ export class AgentSession {
9061
9160
  return true;
9062
9161
  } catch (error) {
9063
9162
  this.sessionManager.restoreState(previousSessionState);
9163
+ this.#freshProviderSessionId = previousFreshProviderSessionId;
9064
9164
  this.#syncAgentSessionId(previousSessionState.sessionId);
9065
9165
  this.#rekeyHindsightMemoryForCurrentSessionId();
9066
9166
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -9159,6 +9259,7 @@ export class AgentSession {
9159
9259
  this.sessionManager.createBranchedSession(selectedEntry.parentId);
9160
9260
  }
9161
9261
  this.#syncTodoPhasesFromBranch();
9262
+ this.#freshProviderSessionId = undefined;
9162
9263
  this.#syncAgentSessionId();
9163
9264
  this.#rekeyHindsightMemoryForCurrentSessionId();
9164
9265
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -10,6 +10,8 @@ export type {
10
10
  AuthCredentialStore,
11
11
  AuthStorageData,
12
12
  AuthStorageOptions,
13
+ CredentialOrigin,
14
+ CredentialOriginKind,
13
15
  OAuthCredential,
14
16
  SerializedAuthStorage,
15
17
  SnapshotResponse,
@@ -70,6 +70,32 @@ export function isSilentAbort(errorMessage: string | undefined): boolean {
70
70
  return errorMessage === SILENT_ABORT_MARKER;
71
71
  }
72
72
 
73
+ /** Reason threaded through `AbortController.abort(reason)` when the user aborts
74
+ * the turn with Esc (see `AgentSession.abort`). The agent surfaces it verbatim
75
+ * on the aborted assistant message's `errorMessage`, so the transcript reads as
76
+ * a deliberate user interrupt instead of an opaque failure. */
77
+ export const USER_INTERRUPT_LABEL = "Interrupted by user";
78
+
79
+ /** Sentinel `errorMessage` the agent stamps on any abort that carried no custom
80
+ * reason (bare `abort()`). Renderers treat it as "no specific reason given". */
81
+ const GENERIC_ABORT_SENTINEL = "Request was aborted";
82
+
83
+ /** Resolve the operator-facing label for an aborted assistant turn. A custom
84
+ * abort reason (e.g. `USER_INTERRUPT_LABEL`) threaded onto `errorMessage` is
85
+ * shown verbatim; aborts with no threaded reason fall back to the retry-aware
86
+ * generic label. Centralizes the live-stream (`EventController`), replay
87
+ * (`ui-helpers`), and component (`AssistantMessageComponent`) render paths so
88
+ * they stay in lockstep. */
89
+ export function resolveAbortLabel(errorMessage: string | undefined, retryAttempt = 0): string {
90
+ if (errorMessage && errorMessage !== GENERIC_ABORT_SENTINEL && !isSilentAbort(errorMessage)) {
91
+ return errorMessage;
92
+ }
93
+ if (retryAttempt > 0) {
94
+ return `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`;
95
+ }
96
+ return "Operation aborted";
97
+ }
98
+
73
99
  /** Extract the optional `__pendingDisplayTag` field from a CustomMessage's
74
100
  * `details` blob. Safe over `unknown`; returns undefined when the field is
75
101
  * absent or non-string. */
@@ -845,11 +845,18 @@ function writeTerminalBreadcrumb(cwd: string, sessionFile: string): void {
845
845
  Bun.write(breadcrumbFile, content).catch(() => {});
846
846
  }
847
847
 
848
+ interface TerminalBreadcrumb {
849
+ cwd: string;
850
+ sessionFile: string;
851
+ }
852
+
848
853
  /**
849
- * Read the terminal breadcrumb for the current terminal, scoped to a cwd.
850
- * Returns the session file path if it exists and matches the cwd, null otherwise.
854
+ * Read the raw terminal breadcrumb for the current terminal.
855
+ * Returns the recorded cwd + session file (verified to exist) regardless of
856
+ * whether the recorded cwd still matches the current one. Callers decide how
857
+ * to interpret a cwd mismatch (e.g. a moved/renamed worktree).
851
858
  */
852
- async function readTerminalBreadcrumb(cwd: string): Promise<string | null> {
859
+ async function readTerminalBreadcrumbEntry(): Promise<TerminalBreadcrumb | null> {
853
860
  const terminalId = getTerminalId();
854
861
  if (!terminalId) return null;
855
862
 
@@ -862,12 +869,9 @@ async function readTerminalBreadcrumb(cwd: string): Promise<string | null> {
862
869
  const breadcrumbCwd = lines[0];
863
870
  const sessionFile = lines[1];
864
871
 
865
- // Only return if cwd matches (user might have cd'd)
866
- if (path.resolve(breadcrumbCwd) !== path.resolve(cwd)) return null;
867
-
868
872
  // Verify the session file still exists
869
873
  const stat = fs.statSync(sessionFile, { throwIfNoEntry: false });
870
- if (stat?.isFile()) return sessionFile;
874
+ if (stat?.isFile()) return { cwd: breadcrumbCwd, sessionFile };
871
875
  } catch (err) {
872
876
  if (!isEnoent(err)) logger.debug("Terminal breadcrumb read failed", { err });
873
877
  // Breadcrumb doesn't exist or is corrupt — fall through
@@ -1967,6 +1971,8 @@ export class SessionManager {
1967
1971
  #inMemoryArtifacts: Map<string, string> | null = null;
1968
1972
  #inMemoryArtifactCounter = 0;
1969
1973
  readonly #blobStore: BlobStore;
1974
+ #suppressBreadcrumb = false;
1975
+ #sessionNameChangedCallbacks = new Set<() => void>();
1970
1976
 
1971
1977
  private constructor(
1972
1978
  private cwd: string,
@@ -1981,6 +1987,11 @@ export class SessionManager {
1981
1987
  // Note: call _initSession() or _initSessionFile() after construction
1982
1988
  }
1983
1989
 
1990
+ #maybeWriteBreadcrumb(cwd: string, sessionFile: string): void {
1991
+ if (this.#suppressBreadcrumb) return;
1992
+ writeTerminalBreadcrumb(cwd, sessionFile);
1993
+ }
1994
+
1984
1995
  /** Puts a binary blob into the blob store and returns the blob reference */
1985
1996
  async putBlob(data: Buffer, options?: BlobPutOptions): Promise<BlobPutResult> {
1986
1997
  return this.#blobStore.put(data, options);
@@ -2027,7 +2038,7 @@ export class SessionManager {
2027
2038
  this.#adoptedArtifactManager = null;
2028
2039
  this.#buildIndex();
2029
2040
  if (this.#sessionFile) {
2030
- writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
2041
+ this.#maybeWriteBreadcrumb(this.cwd, this.#sessionFile);
2031
2042
  }
2032
2043
  }
2033
2044
 
@@ -2047,7 +2058,7 @@ export class SessionManager {
2047
2058
  this.#persistError = undefined;
2048
2059
  this.#persistErrorReported = false;
2049
2060
  this.#sessionFile = path.resolve(sessionFile);
2050
- writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
2061
+ this.#maybeWriteBreadcrumb(this.cwd, this.#sessionFile);
2051
2062
  this.#fileEntries = await loadEntriesFromFile(this.#sessionFile, this.storage);
2052
2063
  if (this.#fileEntries.length > 0) {
2053
2064
  const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
@@ -2064,7 +2075,7 @@ export class SessionManager {
2064
2075
  if (headerCwd && headerCwd !== this.cwd) {
2065
2076
  this.cwd = headerCwd;
2066
2077
  this.sessionDir = path.resolve(this.#sessionFile, "..");
2067
- writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
2078
+ this.#maybeWriteBreadcrumb(this.cwd, this.#sessionFile);
2068
2079
  }
2069
2080
 
2070
2081
  this.#needsFullRewriteOnNextPersist = migrateToCurrentVersion(this.#fileEntries);
@@ -2157,19 +2168,24 @@ export class SessionManager {
2157
2168
  /**
2158
2169
  * Move the session to a new working directory.
2159
2170
  * Moves session files and artifacts on disk, updates all internal references,
2160
- * and rewrites the session header with the new cwd.
2171
+ * and rewrites the session header with the new cwd. When provided,
2172
+ * `targetSessionDir` is used instead of deriving the default directory for
2173
+ * the new cwd (for `--continue --session-dir` / `--resume --session-dir`).
2161
2174
  */
2162
- async moveTo(newCwd: string): Promise<void> {
2175
+ async moveTo(newCwd: string, targetSessionDir?: string): Promise<void> {
2163
2176
  const resolvedCwd = path.resolve(newCwd);
2164
- if (resolvedCwd === this.cwd) return;
2177
+ if (resolvedCwd === this.cwd && (!targetSessionDir || path.resolve(targetSessionDir) === this.sessionDir)) return;
2165
2178
 
2166
2179
  const managedSessionsRoot = resolveManagedSessionRoot(this.sessionDir, this.cwd);
2167
- const newSessionDir = managedSessionsRoot
2168
- ? computeDefaultSessionDir(resolvedCwd, this.storage, managedSessionsRoot)
2169
- : computeDefaultSessionDir(resolvedCwd, this.storage);
2180
+ const newSessionDir = targetSessionDir
2181
+ ? path.resolve(targetSessionDir)
2182
+ : managedSessionsRoot
2183
+ ? computeDefaultSessionDir(resolvedCwd, this.storage, managedSessionsRoot)
2184
+ : computeDefaultSessionDir(resolvedCwd, this.storage);
2170
2185
  let hadSessionFile = false;
2171
2186
 
2172
2187
  if (this.persist && this.#sessionFile) {
2188
+ this.storage.ensureDirSync(newSessionDir);
2173
2189
  // Close the persist writer before moving files
2174
2190
  await this.#closePersistWriter();
2175
2191
  this.#persistChain = Promise.resolve();
@@ -2180,25 +2196,29 @@ export class SessionManager {
2180
2196
  const newSessionFile = path.join(newSessionDir, path.basename(oldSessionFile));
2181
2197
  const oldArtifactDir = oldSessionFile.slice(0, -6); // strip .jsonl
2182
2198
  const newArtifactDir = newSessionFile.slice(0, -6);
2199
+ const sameSessionFile = path.resolve(oldSessionFile) === path.resolve(newSessionFile);
2200
+ const sameArtifactDir = path.resolve(oldArtifactDir) === path.resolve(newArtifactDir);
2183
2201
  hadSessionFile = this.storage.existsSync(oldSessionFile);
2184
2202
  let movedSessionFile = false;
2185
2203
  let movedArtifactDir = false;
2186
2204
 
2187
2205
  try {
2188
2206
  // Guard: session file may not exist yet (no assistant messages persisted)
2189
- if (hadSessionFile) {
2207
+ if (hadSessionFile && !sameSessionFile) {
2190
2208
  await fs.promises.rename(oldSessionFile, newSessionFile);
2191
2209
  movedSessionFile = true;
2192
2210
  }
2193
2211
 
2194
- try {
2195
- const stat = await fs.promises.stat(oldArtifactDir);
2196
- if (stat.isDirectory()) {
2197
- await fs.promises.rename(oldArtifactDir, newArtifactDir);
2198
- movedArtifactDir = true;
2212
+ if (!sameArtifactDir) {
2213
+ try {
2214
+ const stat = await fs.promises.stat(oldArtifactDir);
2215
+ if (stat.isDirectory()) {
2216
+ await fs.promises.rename(oldArtifactDir, newArtifactDir);
2217
+ movedArtifactDir = true;
2218
+ }
2219
+ } catch (err) {
2220
+ if (!isEnoent(err)) throw err;
2199
2221
  }
2200
- } catch (err) {
2201
- if (!isEnoent(err)) throw err;
2202
2222
  }
2203
2223
  } catch (err) {
2204
2224
  if (movedArtifactDir) {
@@ -2245,7 +2265,7 @@ export class SessionManager {
2245
2265
 
2246
2266
  // Update terminal breadcrumb
2247
2267
  if (this.#sessionFile) {
2248
- writeTerminalBreadcrumb(resolvedCwd, this.#sessionFile);
2268
+ this.#maybeWriteBreadcrumb(resolvedCwd, this.#sessionFile);
2249
2269
  }
2250
2270
  }
2251
2271
 
@@ -2280,7 +2300,7 @@ export class SessionManager {
2280
2300
  if (this.persist) {
2281
2301
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
2282
2302
  this.#sessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${this.#sessionId}.jsonl`);
2283
- writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
2303
+ this.#maybeWriteBreadcrumb(this.cwd, this.#sessionFile);
2284
2304
  }
2285
2305
  return this.#sessionFile;
2286
2306
  }
@@ -2724,6 +2744,23 @@ export class SessionManager {
2724
2744
  return this.#sessionName;
2725
2745
  }
2726
2746
 
2747
+ onSessionNameChanged(cb: () => void): () => void {
2748
+ this.#sessionNameChangedCallbacks.add(cb);
2749
+ return () => {
2750
+ this.#sessionNameChangedCallbacks.delete(cb);
2751
+ };
2752
+ }
2753
+
2754
+ #fireSessionNameChanged(): void {
2755
+ for (const cb of [...this.#sessionNameChangedCallbacks]) {
2756
+ try {
2757
+ cb();
2758
+ } catch (err) {
2759
+ logger.warn("SessionManager: session name change hook failed", { error: String(err) });
2760
+ }
2761
+ }
2762
+ }
2763
+
2727
2764
  /** Strip C0/C1 control characters (includes ESC, so removes ANSI sequences) and collapse whitespace. */
2728
2765
  static #sanitizeName(name: string): string {
2729
2766
  return name
@@ -2759,6 +2796,7 @@ export class SessionManager {
2759
2796
  if (this.persist && sessionFile && this.storage.existsSync(sessionFile)) {
2760
2797
  await this.#rewriteFile();
2761
2798
  }
2799
+ this.#fireSessionNameChanged();
2762
2800
  return true;
2763
2801
  }
2764
2802
 
@@ -3429,9 +3467,11 @@ export class SessionManager {
3429
3467
  cwd: string,
3430
3468
  sessionDir?: string,
3431
3469
  storage: SessionStorage = new FileSessionStorage(),
3470
+ options?: { suppressBreadcrumb?: boolean },
3432
3471
  ): Promise<SessionManager> {
3433
3472
  const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
3434
3473
  const manager = new SessionManager(cwd, dir, true, storage);
3474
+ manager.#suppressBreadcrumb = options?.suppressBreadcrumb === true;
3435
3475
  const forkEntries = structuredClone(await loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
3436
3476
  migrateToCurrentVersion(forkEntries);
3437
3477
  await resolveBlobRefsInEntries(forkEntries, manager.#blobStore);
@@ -3483,8 +3523,49 @@ export class SessionManager {
3483
3523
  ): Promise<SessionManager> {
3484
3524
  const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
3485
3525
  // Prefer terminal-scoped breadcrumb (handles concurrent sessions correctly)
3486
- const terminalSession = await readTerminalBreadcrumb(cwd);
3487
- const mostRecent = terminalSession ?? (await findMostRecentSession(dir, storage));
3526
+ const breadcrumb = await readTerminalBreadcrumbEntry();
3527
+ const breadcrumbCwd = breadcrumb ? path.resolve(breadcrumb.cwd) : undefined;
3528
+ const resolvedCwd = path.resolve(cwd);
3529
+ let mostRecent: string | null | undefined;
3530
+ if (breadcrumb && breadcrumbCwd !== resolvedCwd) {
3531
+ // The terminal's last session was started in a different cwd. If that cwd no
3532
+ // longer exists (e.g. `git worktree move`/dir rename) and the new location has
3533
+ // no sessions of its own, re-root the session here instead of silently starting
3534
+ // fresh — otherwise the relocated session would be unreachable via --continue.
3535
+ // When an explicit sessionDir is reused across the move, the stale breadcrumb
3536
+ // file itself may be the most recent entry there; don't count it as a
3537
+ // current-directory session. If that shared dir also contains an older session
3538
+ // that already belongs to the current cwd, prefer that local session instead
3539
+ // of re-rooting the stale breadcrumb over it.
3540
+ const resolvedBreadcrumbCwd = path.resolve(breadcrumb.cwd);
3541
+ mostRecent = await findMostRecentSession(dir, storage);
3542
+ const sourceCwdGone = !fs.existsSync(resolvedBreadcrumbCwd);
3543
+ const breadcrumbSessionFile = path.resolve(breadcrumb.sessionFile);
3544
+ const mostRecentIsBreadcrumb =
3545
+ mostRecent !== null && mostRecent !== undefined && path.resolve(mostRecent) === breadcrumbSessionFile;
3546
+ let hasCurrentCwdSession = false;
3547
+ if (sourceCwdGone && mostRecentIsBreadcrumb) {
3548
+ const currentCwdSession = (await SessionManager.list(cwd, dir, storage)).find(
3549
+ session =>
3550
+ path.resolve(session.path) !== breadcrumbSessionFile &&
3551
+ session.cwd &&
3552
+ path.resolve(session.cwd) === resolvedCwd,
3553
+ );
3554
+ if (currentCwdSession) {
3555
+ mostRecent = currentCwdSession.path;
3556
+ hasCurrentCwdSession = true;
3557
+ }
3558
+ }
3559
+ const relocated = sourceCwdGone && (mostRecent === null || (mostRecentIsBreadcrumb && !hasCurrentCwdSession));
3560
+ if (relocated) {
3561
+ process.stderr.write(`Re-rooting moved session from ${resolvedBreadcrumbCwd} to ${resolvedCwd}.\n`);
3562
+ const manager = await SessionManager.open(breadcrumb.sessionFile, undefined, storage);
3563
+ await manager.moveTo(cwd, sessionDir);
3564
+ return manager;
3565
+ }
3566
+ }
3567
+ const terminalSession = breadcrumb && breadcrumbCwd === resolvedCwd ? breadcrumb.sessionFile : null;
3568
+ if (mostRecent === undefined) mostRecent = terminalSession ?? (await findMostRecentSession(dir, storage));
3488
3569
  const manager = new SessionManager(cwd, dir, true, storage);
3489
3570
  if (mostRecent) {
3490
3571
  await manager.#initSessionFile(mostRecent);