@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.
- package/CHANGELOG.md +142 -1
- package/dist/types/cli/dry-balance-cli.d.ts +15 -1
- package/dist/types/cli/startup-cwd.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/commit/analysis/conventional.d.ts +2 -2
- package/dist/types/commit/analysis/summary.d.ts +2 -2
- package/dist/types/commit/changelog/generate.d.ts +2 -2
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/index.d.ts +3 -3
- package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
- package/dist/types/commit/model-selection.d.ts +10 -4
- package/dist/types/config/api-key-resolver.d.ts +34 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-provider-priority.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +17 -1
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/config/settings.d.ts +7 -2
- package/dist/types/dap/config.d.ts +14 -1
- package/dist/types/dap/types.d.ts +10 -0
- package/dist/types/debug/report-bundle.d.ts +3 -0
- package/dist/types/edit/file-snapshot-store.d.ts +18 -10
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -1
- package/dist/types/lsp/client.d.ts +10 -0
- package/dist/types/lsp/utils.d.ts +3 -2
- package/dist/types/main.d.ts +3 -9
- package/dist/types/mcp/tool-bridge.d.ts +2 -0
- package/dist/types/modes/components/chat-block.d.ts +64 -0
- package/dist/types/modes/components/custom-editor.d.ts +4 -1
- package/dist/types/modes/components/overlay-box.d.ts +17 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
- package/dist/types/modes/components/plan-toc.d.ts +41 -0
- package/dist/types/modes/components/read-tool-group.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +11 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +17 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
- package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +16 -5
- package/dist/types/modes/magic-keywords.d.ts +1 -1
- package/dist/types/modes/markdown-prose.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +21 -5
- package/dist/types/modes/utils/copy-targets.d.ts +21 -1
- package/dist/types/modes/workflow.d.ts +3 -3
- package/dist/types/plan-mode/approved-plan.d.ts +27 -8
- package/dist/types/plan-mode/plan-protection.d.ts +4 -4
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +21 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/messages.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +8 -3
- package/dist/types/slash-commands/types.d.ts +4 -6
- package/dist/types/task/executor.d.ts +17 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/render.d.ts +3 -2
- package/dist/types/tools/archive-reader.d.ts +5 -0
- package/dist/types/tools/ast-edit.d.ts +3 -0
- package/dist/types/tools/ast-grep.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/eval.d.ts +8 -0
- package/dist/types/tools/find.d.ts +8 -4
- package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
- package/dist/types/tools/github-cache.d.ts +12 -0
- package/dist/types/tools/grouped-file-output.d.ts +95 -12
- package/dist/types/tools/memory-render.d.ts +4 -1
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/plan-mode-guard.d.ts +8 -9
- package/dist/types/tools/render-utils.d.ts +5 -9
- package/dist/types/tools/search.d.ts +6 -2
- package/dist/types/tools/sqlite-reader.d.ts +1 -0
- package/dist/types/tools/todo.d.ts +3 -2
- package/dist/types/tools/write.d.ts +3 -0
- package/dist/types/tools/yield.d.ts +8 -0
- package/dist/types/tui/output-block.d.ts +16 -4
- package/dist/types/tui/status-line.d.ts +3 -0
- package/dist/types/utils/enhanced-paste.d.ts +20 -0
- package/dist/types/web/search/providers/kimi.d.ts +1 -1
- package/package.json +9 -9
- package/src/auto-thinking/classifier.ts +5 -1
- package/src/cli/args.ts +3 -1
- package/src/cli/dry-balance-cli.ts +54 -21
- package/src/cli/gallery-cli.ts +4 -1
- package/src/cli/gallery-fixtures/misc.ts +29 -0
- package/src/cli/startup-cwd.ts +68 -0
- package/src/commands/launch.ts +3 -0
- package/src/commit/analysis/conventional.ts +2 -2
- package/src/commit/analysis/summary.ts +2 -2
- package/src/commit/changelog/generate.ts +2 -2
- package/src/commit/changelog/index.ts +2 -2
- package/src/commit/map-reduce/index.ts +3 -3
- package/src/commit/map-reduce/map-phase.ts +2 -2
- package/src/commit/map-reduce/reduce-phase.ts +2 -2
- package/src/commit/model-selection.ts +36 -11
- package/src/commit/pipeline.ts +4 -4
- package/src/config/api-key-resolver.ts +58 -0
- package/src/config/model-provider-priority.ts +55 -0
- package/src/config/model-registry.ts +29 -24
- package/src/config/model-resolver.ts +39 -7
- package/src/config/settings-schema.ts +10 -0
- package/src/config/settings.ts +106 -43
- package/src/dap/config.ts +41 -2
- package/src/dap/defaults.json +1 -0
- package/src/dap/session.ts +1 -0
- package/src/dap/types.ts +10 -0
- package/src/debug/index.ts +47 -53
- package/src/debug/raw-sse-buffer.ts +7 -4
- package/src/debug/report-bundle.ts +9 -0
- package/src/edit/file-snapshot-store.ts +33 -1
- package/src/edit/hashline/filesystem.ts +2 -1
- package/src/edit/renderer.ts +82 -78
- package/src/eval/__tests__/llm-bridge.test.ts +110 -31
- package/src/eval/js/context-manager.ts +32 -15
- package/src/eval/llm-bridge.ts +22 -6
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/executor.ts +23 -11
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +10 -1
- package/src/goals/tools/goal-tool.ts +36 -26
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/lsp/client.ts +23 -11
- package/src/lsp/config.ts +11 -1
- package/src/lsp/index.ts +61 -9
- package/src/lsp/utils.ts +3 -2
- package/src/main.ts +100 -72
- package/src/mcp/tool-bridge.ts +2 -0
- package/src/memories/index.ts +14 -7
- package/src/mnemopi/backend.ts +5 -1
- package/src/modes/acp/acp-agent.ts +33 -26
- package/src/modes/components/assistant-message.ts +2 -9
- package/src/modes/components/chat-block.ts +111 -0
- package/src/modes/components/copy-selector.ts +1 -44
- package/src/modes/components/custom-editor.ts +164 -109
- package/src/modes/components/custom-message.ts +1 -3
- package/src/modes/components/execution-shared.ts +1 -2
- package/src/modes/components/hook-message.ts +1 -3
- package/src/modes/components/model-selector.ts +59 -13
- package/src/modes/components/oauth-selector.ts +33 -7
- package/src/modes/components/overlay-box.ts +108 -0
- package/src/modes/components/plan-review-overlay.ts +799 -0
- package/src/modes/components/plan-toc.ts +138 -0
- package/src/modes/components/read-tool-group.ts +20 -4
- package/src/modes/components/skill-message.ts +0 -1
- package/src/modes/components/status-line.ts +19 -4
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/todo-reminder.ts +0 -2
- package/src/modes/components/tool-execution.ts +68 -88
- package/src/modes/components/transcript-container.ts +84 -24
- package/src/modes/components/user-message.ts +2 -3
- package/src/modes/controllers/command-controller-shared.ts +7 -6
- package/src/modes/controllers/command-controller.ts +57 -55
- package/src/modes/controllers/event-controller.ts +67 -40
- package/src/modes/controllers/extension-ui-controller.ts +10 -73
- package/src/modes/controllers/input-controller.ts +170 -126
- package/src/modes/controllers/mcp-command-controller.ts +69 -60
- package/src/modes/controllers/selector-controller.ts +23 -25
- package/src/modes/controllers/streaming-reveal.ts +212 -0
- package/src/modes/controllers/tan-command-controller.ts +173 -0
- package/src/modes/interactive-mode.ts +274 -112
- package/src/modes/magic-keywords.ts +1 -1
- package/src/modes/markdown-prose.ts +1 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/theme/shimmer.ts +20 -9
- package/src/modes/theme/theme-schema.json +1 -1
- package/src/modes/theme/theme.ts +8 -4
- package/src/modes/types.ts +21 -7
- package/src/modes/utils/copy-targets.ts +133 -27
- package/src/modes/utils/ui-helpers.ts +44 -46
- package/src/modes/workflow.ts +10 -10
- package/src/plan-mode/approved-plan.ts +66 -43
- package/src/plan-mode/plan-protection.ts +4 -4
- package/src/prompts/system/background-tan-dispatch.md +8 -0
- package/src/prompts/system/plan-mode-active.md +67 -58
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/eval.md +2 -1
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +37 -46
- package/src/session/agent-session.ts +119 -18
- package/src/session/auth-storage.ts +2 -0
- package/src/session/messages.ts +26 -0
- package/src/session/session-manager.ts +109 -28
- package/src/slash-commands/builtin-registry.ts +36 -9
- package/src/slash-commands/types.ts +4 -6
- package/src/task/executor.ts +76 -38
- package/src/task/index.ts +4 -0
- package/src/task/render.ts +211 -147
- package/src/tools/archive-reader.ts +64 -0
- package/src/tools/ask.ts +119 -164
- package/src/tools/ast-edit.ts +98 -71
- package/src/tools/ast-grep.ts +37 -43
- package/src/tools/bash.ts +57 -6
- package/src/tools/browser/tab-supervisor.ts +13 -1
- package/src/tools/browser/tab-worker.ts +33 -4
- package/src/tools/debug.ts +20 -8
- package/src/tools/eval.ts +13 -2
- package/src/tools/fetch.ts +297 -7
- package/src/tools/find.ts +51 -30
- package/src/tools/gh-cache-invalidation.ts +200 -0
- package/src/tools/gh-renderer.ts +81 -42
- package/src/tools/github-cache.ts +25 -0
- package/src/tools/grouped-file-output.ts +272 -48
- package/src/tools/image-gen.ts +150 -103
- package/src/tools/inspect-image-renderer.ts +63 -41
- package/src/tools/inspect-image.ts +10 -3
- package/src/tools/job.ts +3 -4
- package/src/tools/memory-render.ts +4 -1
- package/src/tools/path-utils.ts +28 -2
- package/src/tools/plan-mode-guard.ts +66 -39
- package/src/tools/read.ts +48 -28
- package/src/tools/render-utils.ts +21 -37
- package/src/tools/resolve.ts +14 -0
- package/src/tools/search-tool-bm25.ts +36 -23
- package/src/tools/search.ts +118 -81
- package/src/tools/sqlite-reader.ts +9 -12
- package/src/tools/todo.ts +118 -52
- package/src/tools/write.ts +83 -64
- package/src/tools/yield.ts +10 -1
- package/src/tui/output-block.ts +60 -13
- package/src/tui/status-line.ts +5 -1
- package/src/utils/commit-message-generator.ts +11 -3
- package/src/utils/enhanced-paste.ts +230 -0
- package/src/utils/title-generator.ts +2 -1
- package/src/web/search/providers/anthropic.ts +25 -19
- package/src/web/search/providers/codex.ts +37 -8
- package/src/web/search/providers/exa.ts +11 -3
- package/src/web/search/providers/kimi.ts +28 -17
- package/src/web/search/providers/parallel.ts +35 -24
- package/src/web/search/providers/synthetic.ts +8 -6
- package/src/web/search/providers/tavily.ts +9 -8
- 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 =
|
|
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.#
|
|
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.#
|
|
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 =
|
|
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
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
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:
|
|
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
|
|
7808
|
-
// ENHANCE_YOUR_CALM, surfaced verbatim from
|
|
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();
|
package/src/session/messages.ts
CHANGED
|
@@ -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
|
|
850
|
-
* Returns the
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
2168
|
-
?
|
|
2169
|
-
:
|
|
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
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3487
|
-
const
|
|
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);
|