@oh-my-pi/pi-coding-agent 17.2.3 → 17.2.5
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 +92 -0
- package/dist/{CHANGELOG-c5hpqt9r.md → CHANGELOG-q2w43aw3.md} +92 -0
- package/dist/cli.js +3979 -3548
- package/dist/{template-8vdv6xb4.js → template-c2hyaytt.js} +8 -26
- package/dist/types/advisor/advise-tool.d.ts +6 -0
- package/dist/types/advisor/runtime.d.ts +4 -4
- package/dist/types/capability/mcp.d.ts +9 -0
- package/dist/types/cli/args.d.ts +4 -1
- package/dist/types/cli/browser-relay-cli.d.ts +15 -0
- package/dist/types/cli/command-help.d.ts +99 -0
- package/dist/types/cli/help-extra.d.ts +2 -0
- package/dist/types/cli/models-cli.d.ts +1 -1
- package/dist/types/cli/thinking-levels.d.ts +5 -0
- package/dist/types/cli/update-cli.d.ts +13 -0
- package/dist/types/commands/browser-relay.d.ts +38 -0
- package/dist/types/commands/commit.d.ts +3 -0
- package/dist/types/commands/complete.d.ts +1 -1
- package/dist/types/commands/completions.d.ts +6 -0
- package/dist/types/commands/grep.d.ts +3 -0
- package/dist/types/commands/join.d.ts +4 -0
- package/dist/types/commands/launch-help.d.ts +171 -0
- package/dist/types/commands/launch.d.ts +5 -1
- package/dist/types/commands/models.d.ts +3 -0
- package/dist/types/commands/say.d.ts +10 -0
- package/dist/types/commands/worktree.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/model-discovery.d.ts +2 -2
- package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/models-config.d.ts +1 -0
- package/dist/types/config/service-tier.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +70 -22
- package/dist/types/cursor-bridge-tools.d.ts +1 -1
- package/dist/types/cursor.d.ts +7 -4
- package/dist/types/discovery/helpers.d.ts +6 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +21 -2
- package/dist/types/edit/index.d.ts +3 -3
- package/dist/types/edit/modes/replace.d.ts +22 -15
- package/dist/types/edit/renderer.d.ts +1 -1
- package/dist/types/eval/py/kernel.d.ts +3 -1
- package/dist/types/eval/py/spawn-options.d.ts +12 -16
- package/dist/types/extensibility/extensions/loader.d.ts +6 -2
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +2 -2
- package/dist/types/launch/broker.d.ts +1 -1
- package/dist/types/launch/client.d.ts +6 -3
- package/dist/types/launch/ensure.d.ts +8 -0
- package/dist/types/launch/paths.d.ts +3 -2
- package/dist/types/launch/protocol.d.ts +2 -2
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/mux/daemon.d.ts +15 -0
- package/dist/types/lsp/mux/protocol.d.ts +57 -0
- package/dist/types/lsp/mux/server.d.ts +18 -0
- package/dist/types/lsp/types.d.ts +22 -2
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/request-id.d.ts +19 -0
- package/dist/types/mcp/types.d.ts +14 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/messages.d.ts +15 -0
- package/dist/types/mnemopi/embed-client.d.ts +1 -1
- package/dist/types/mnemopi/state.d.ts +0 -23
- package/dist/types/modes/components/assistant-message.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -1
- package/dist/types/modes/components/model-browser.d.ts +7 -5
- package/dist/types/modes/components/model-picker.d.ts +9 -3
- package/dist/types/modes/components/read-tool-group.d.ts +11 -0
- package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/modes/workflow.d.ts +2 -1
- package/dist/types/registry/agent-lifecycle.d.ts +14 -5
- package/dist/types/registry/agent-registry.d.ts +4 -3
- package/dist/types/sdk.d.ts +4 -2
- package/dist/types/secrets/index.d.ts +7 -9
- package/dist/types/session/agent-session-types.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +6 -1
- package/dist/types/session/indexed-session-storage.d.ts +1 -0
- package/dist/types/session/model-controls.d.ts +0 -1
- package/dist/types/session/session-advisors.d.ts +8 -1
- package/dist/types/session/session-context.d.ts +2 -1
- package/dist/types/session/session-maintenance.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +16 -5
- package/dist/types/session/session-stats.d.ts +10 -0
- package/dist/types/session/session-storage.d.ts +15 -3
- package/dist/types/system-prompt.d.ts +6 -1
- package/dist/types/task/index.d.ts +2 -1
- package/dist/types/task/spawn-policy.d.ts +5 -0
- package/dist/types/thinking.d.ts +1 -7
- package/dist/types/tools/ast-grep.d.ts +1 -1
- package/dist/types/tools/browser/registry.d.ts +2 -1
- package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
- package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
- package/dist/types/tools/browser/relay/kind.d.ts +19 -0
- package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
- package/dist/types/tools/browser/relay/server.d.ts +35 -0
- package/dist/types/tools/browser/render.d.ts +1 -0
- package/dist/types/tools/browser.d.ts +3 -0
- package/dist/types/tools/computer/exposure.d.ts +9 -7
- package/dist/types/tools/computer/protocol.d.ts +70 -17
- package/dist/types/tools/computer/supervisor.d.ts +23 -10
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +42 -7
- package/dist/types/tools/computer-renderer.d.ts +7 -5
- package/dist/types/tools/computer.d.ts +25 -44
- package/dist/types/tools/context.d.ts +3 -0
- package/dist/types/tools/default-renderer.d.ts +3 -0
- package/dist/types/tools/glob.d.ts +1 -1
- package/dist/types/tools/grep.d.ts +1 -1
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
- package/dist/types/tools/tool-timeouts.d.ts +5 -0
- package/dist/types/tools/xdev.d.ts +8 -0
- package/dist/types/utils/clipboard.d.ts +2 -2
- package/dist/types/web/kagi.d.ts +1 -0
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/providers/anthropic.d.ts +1 -0
- package/dist/types/web/search/providers/base.d.ts +2 -0
- package/dist/types/web/search/providers/brave.d.ts +1 -0
- package/dist/types/web/search/providers/browser-page.d.ts +1 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -0
- package/dist/types/web/search/providers/exa.d.ts +1 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
- package/dist/types/web/search/providers/gemini.d.ts +1 -0
- package/dist/types/web/search/providers/jina.d.ts +1 -0
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +1 -0
- package/dist/types/web/search/providers/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/perplexity.d.ts +1 -0
- package/dist/types/web/search/providers/searxng.d.ts +1 -0
- package/dist/types/web/search/providers/tavily.d.ts +1 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +6 -9
- package/dist/types/web/search/providers/zai.d.ts +1 -0
- package/dist/types/web/search/types.d.ts +4 -0
- package/package.json +12 -12
- package/scripts/legacy-pi-virtual-module.ts +12 -2
- package/src/advisor/advise-tool.ts +18 -0
- package/src/advisor/runtime.ts +6 -6
- package/src/capability/mcp.ts +7 -0
- package/src/cli/args.ts +7 -87
- package/src/cli/browser-relay-cli.ts +119 -0
- package/src/cli/command-help.ts +103 -0
- package/src/cli/completion-gen.ts +5 -5
- package/src/cli/flag-tables.ts +9 -0
- package/src/cli/help-extra.ts +89 -0
- package/src/cli/models-cli.ts +13 -16
- package/src/cli/setup-cli.ts +13 -21
- package/src/cli/thinking-levels.ts +7 -0
- package/src/cli/update-cli.ts +26 -7
- package/src/cli-commands.ts +170 -33
- package/src/cli.ts +17 -5
- package/src/collab/host.ts +1 -1
- package/src/commands/acp.ts +3 -1
- package/src/commands/agents.ts +3 -2
- package/src/commands/auth-broker.ts +3 -2
- package/src/commands/auth-gateway.ts +3 -2
- package/src/commands/bench.ts +2 -3
- package/src/commands/browser-relay.ts +53 -0
- package/src/commands/cleanse.ts +2 -2
- package/src/commands/commit.ts +3 -2
- package/src/commands/complete.ts +2 -1
- package/src/commands/completions.ts +3 -2
- package/src/commands/config.ts +3 -2
- package/src/commands/dry-balance.ts +2 -2
- package/src/commands/gallery.ts +3 -2
- package/src/commands/gc.ts +3 -2
- package/src/commands/grep.ts +3 -2
- package/src/commands/grievances.ts +3 -2
- package/src/commands/install.ts +2 -2
- package/src/commands/join.ts +3 -2
- package/src/commands/launch-help.ts +116 -0
- package/src/commands/launch.ts +7 -191
- package/src/commands/models.ts +3 -2
- package/src/commands/plugin.ts +3 -2
- package/src/commands/read.ts +3 -2
- package/src/commands/say.ts +3 -2
- package/src/commands/setup.ts +3 -2
- package/src/commands/shell.ts +3 -2
- package/src/commands/ssh.ts +3 -2
- package/src/commands/stats.ts +3 -2
- package/src/commands/tiny-models.ts +2 -2
- package/src/commands/token.ts +2 -2
- package/src/commands/ttsr.ts +2 -2
- package/src/commands/update.ts +3 -2
- package/src/commands/usage.ts +3 -2
- package/src/commands/web-search.ts +3 -2
- package/src/commands/worktree.ts +3 -2
- package/src/config/keybindings.ts +19 -9
- package/src/config/mcp-schema.json +5 -0
- package/src/config/model-discovery.ts +30 -13
- package/src/config/model-registry.ts +16 -6
- package/src/config/models-config-schema-bundle.ts +9 -0
- package/src/config/resolve-config-value.ts +2 -1
- package/src/config/service-tier.ts +5 -0
- package/src/config/settings-schema.ts +73 -21
- package/src/cursor-bridge-tools.ts +1 -1
- package/src/cursor.ts +10 -14
- package/src/discovery/builtin.ts +10 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/mcp-json.ts +11 -1
- package/src/discovery/omp-extension-roots.ts +79 -20
- package/src/discovery/omp-plugins.ts +10 -1
- package/src/edit/diff.ts +17 -8
- package/src/edit/index.ts +20 -7
- package/src/edit/modes/replace.ts +93 -40
- package/src/edit/renderer.ts +1 -1
- package/src/edit/streaming.ts +14 -17
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/spawn-options.ts +24 -29
- package/src/export/html/template.js +8 -26
- package/src/extensibility/extensions/loader.ts +66 -28
- package/src/extensibility/extensions/wrapper.ts +1 -0
- package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
- package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
- package/src/extensibility/plugins/marketplace/registry.ts +4 -8
- package/src/extensibility/skills.ts +11 -0
- package/src/extensibility/utils.ts +10 -2
- package/src/launch/broker.ts +1 -1
- package/src/launch/client.ts +32 -13
- package/src/launch/ensure.ts +73 -0
- package/src/launch/paths.ts +3 -6
- package/src/launch/protocol.ts +2 -2
- package/src/lsp/client.ts +23 -2
- package/src/lsp/index.ts +8 -0
- package/src/lsp/mux/daemon.ts +348 -0
- package/src/lsp/mux/protocol.ts +96 -0
- package/src/lsp/mux/server.ts +797 -0
- package/src/lsp/types.ts +28 -2
- package/src/main.ts +31 -24
- package/src/mcp/config.ts +1 -0
- package/src/mcp/oauth-flow.ts +11 -1
- package/src/mcp/request-id.ts +24 -0
- package/src/mcp/transports/http.ts +4 -2
- package/src/mcp/transports/sse.ts +4 -2
- package/src/mcp/transports/stdio.ts +4 -2
- package/src/mcp/types.ts +15 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/messages.ts +19 -0
- package/src/mnemopi/embed-client.ts +49 -2
- package/src/mnemopi/state.ts +57 -23
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/assistant-message.ts +11 -0
- package/src/modes/components/chat-transcript-builder.ts +4 -0
- package/src/modes/components/custom-editor.ts +10 -1
- package/src/modes/components/model-browser.ts +25 -17
- package/src/modes/components/model-picker.ts +10 -6
- package/src/modes/components/read-tool-group.ts +36 -0
- package/src/modes/components/status-line/component.ts +1 -1
- package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
- package/src/modes/components/tool-execution.ts +85 -20
- package/src/modes/components/tree-selector.ts +46 -48
- package/src/modes/controllers/command-controller.ts +2 -2
- package/src/modes/controllers/event-controller.ts +199 -10
- package/src/modes/controllers/input-controller.ts +39 -1
- package/src/modes/controllers/mcp-command-controller.ts +11 -5
- package/src/modes/controllers/selector-controller.ts +47 -6
- package/src/modes/interactive-mode.ts +2 -0
- package/src/modes/types.ts +6 -1
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/interactive-context-helpers.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +6 -10
- package/src/modes/workflow.ts +8 -2
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +26 -52
- package/src/prompts/system/workflow-notice.md +3 -3
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/computer.md +21 -21
- package/src/prompts/tools/glob.md +1 -1
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/replace.md +5 -4
- package/src/prompts/tools/task.md +4 -4
- package/src/registry/agent-lifecycle.ts +36 -11
- package/src/registry/agent-registry.ts +12 -5
- package/src/sdk.ts +102 -35
- package/src/secrets/index.ts +16 -18
- package/src/session/agent-session-types.ts +3 -1
- package/src/session/agent-session.ts +23 -2
- package/src/session/agent-storage.ts +6 -4
- package/src/session/history-storage.ts +4 -2
- package/src/session/indexed-session-storage.ts +9 -0
- package/src/session/messages.ts +31 -7
- package/src/session/model-controls.ts +0 -1
- package/src/session/prewalk.ts +24 -2
- package/src/session/session-advisors.ts +21 -3
- package/src/session/session-context.ts +16 -12
- package/src/session/session-dump-format.ts +1 -1
- package/src/session/session-maintenance.ts +41 -4
- package/src/session/session-manager.ts +181 -94
- package/src/session/session-paths.ts +118 -55
- package/src/session/session-stats.ts +48 -3
- package/src/session/session-storage.ts +30 -22
- package/src/session/session-tools.ts +25 -23
- package/src/session/stream-guards.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +29 -26
- package/src/system-prompt.ts +14 -4
- package/src/task/executor.ts +8 -1
- package/src/task/index.ts +23 -8
- package/src/task/spawn-policy.ts +14 -0
- package/src/thinking.ts +2 -8
- package/src/tiny/text.ts +12 -0
- package/src/tools/ast-grep.ts +10 -4
- package/src/tools/browser/cmux/cmux-tab.ts +5 -10
- package/src/tools/browser/cmux/rpc.ts +2 -8
- package/src/tools/browser/registry.ts +53 -2
- package/src/tools/browser/relay/bridge.ts +945 -0
- package/src/tools/browser/relay/daemon.ts +117 -0
- package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
- package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
- package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
- package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
- package/src/tools/browser/relay/kind.ts +41 -0
- package/src/tools/browser/relay/protocol.ts +54 -0
- package/src/tools/browser/relay/server.ts +141 -0
- package/src/tools/browser/render.ts +4 -1
- package/src/tools/browser/shared-daemon.ts +7 -58
- package/src/tools/browser/tab-supervisor.ts +5 -4
- package/src/tools/browser/tab-worker.ts +33 -12
- package/src/tools/browser.ts +26 -1
- package/src/tools/computer/exposure.ts +11 -35
- package/src/tools/computer/protocol.ts +51 -10
- package/src/tools/computer/supervisor.ts +227 -124
- package/src/tools/computer/worker-entry.ts +11 -6
- package/src/tools/computer/worker.ts +695 -81
- package/src/tools/computer-renderer.ts +104 -65
- package/src/tools/computer.ts +134 -428
- package/src/tools/context.ts +3 -0
- package/src/tools/default-renderer.ts +19 -4
- package/src/tools/glob.ts +9 -2
- package/src/tools/grep.ts +12 -6
- package/src/tools/index.ts +10 -6
- package/src/tools/inspect-image.ts +32 -6
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/tools/xdev.ts +29 -2
- package/src/utils/clipboard.ts +40 -14
- package/src/utils/command-args.ts +26 -28
- package/src/utils/tool-choice.ts +0 -13
- package/src/web/kagi.ts +2 -1
- package/src/web/search/index.ts +18 -2
- package/src/web/search/providers/anthropic.ts +7 -2
- package/src/web/search/providers/base.ts +2 -0
- package/src/web/search/providers/brave.ts +3 -1
- package/src/web/search/providers/browser-page.ts +6 -4
- package/src/web/search/providers/codex.ts +4 -1
- package/src/web/search/providers/duckduckgo.ts +2 -1
- package/src/web/search/providers/ecosia.ts +2 -1
- package/src/web/search/providers/exa.ts +4 -2
- package/src/web/search/providers/firecrawl.ts +3 -1
- package/src/web/search/providers/gemini.ts +8 -2
- package/src/web/search/providers/google.ts +2 -1
- package/src/web/search/providers/jina.ts +12 -2
- package/src/web/search/providers/kagi.ts +3 -0
- package/src/web/search/providers/kimi.ts +5 -1
- package/src/web/search/providers/mojeek.ts +2 -1
- package/src/web/search/providers/parallel.ts +5 -1
- package/src/web/search/providers/perplexity.ts +6 -3
- package/src/web/search/providers/public.ts +1 -1
- package/src/web/search/providers/searxng.ts +11 -5
- package/src/web/search/providers/startpage.ts +10 -4
- package/src/web/search/providers/synthetic.ts +11 -5
- package/src/web/search/providers/tavily.ts +3 -1
- package/src/web/search/providers/tinyfish.ts +3 -1
- package/src/web/search/providers/utils.ts +12 -10
- package/src/web/search/providers/xai.ts +2 -2
- package/src/web/search/providers/zai.ts +18 -3
- package/src/web/search/types.ts +6 -0
|
@@ -405,11 +405,22 @@ export class SessionPersistenceIndeterminateError extends AggregateError {
|
|
|
405
405
|
* tree by `(id, parentId)`, and the mutable leaf pointer selects which path is
|
|
406
406
|
* active for future appends and for LLM context construction.
|
|
407
407
|
*
|
|
408
|
-
* Durability is software-crash safe but not power-loss safe:
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
408
|
+
* Durability is software-crash safe but not power-loss safe: completed entries
|
|
409
|
+
* (user/assistant/toolResult messages, tool_execution_start markers, custom
|
|
410
|
+
* entries) are handed to the OS synchronously in-body on append and never
|
|
411
|
+
* `fsync`'d. In-flight streaming text is intentionally not durable until
|
|
412
|
+
* `message_end` persists the finished message.
|
|
413
|
+
*
|
|
414
|
+
* While an in-place atomic rewrite is publishing, a concurrent completed append
|
|
415
|
+
* supersedes that publish with a synchronous full-body rewrite so the entry is
|
|
416
|
+
* software-crash durable before the append returns; the abandoned atomic's
|
|
417
|
+
* `commitGuard` then refuses to clobber the fresher body.
|
|
418
|
+
*
|
|
419
|
+
* During {@link moveTo}, appends write a full body to the live relocation path
|
|
420
|
+
* (source until rename, destination once the rename has landed) so a crash mid-
|
|
421
|
+
* move still preserves completed entries without recreating a vacated source.
|
|
422
|
+
* A trailing atomic rewrite still rewrites the header cwd after the path is
|
|
423
|
+
* repointed.
|
|
413
424
|
*/
|
|
414
425
|
export class SessionManager {
|
|
415
426
|
#cwd: string;
|
|
@@ -477,6 +488,13 @@ export class SessionManager {
|
|
|
477
488
|
#atomicRewriteFenceEpoch: number | null = null;
|
|
478
489
|
/** Set by synchronous appends that land while an atomic replacement is active. */
|
|
479
490
|
#atomicRewriteDirty = false;
|
|
491
|
+
/**
|
|
492
|
+
* Active {@link moveTo} relocation. Concurrent completed appends write a
|
|
493
|
+
* full body to the live path: source while it still exists, destination
|
|
494
|
+
* once rename has landed (source gone). Never recreates a vacated source.
|
|
495
|
+
* `null` outside an active relocation.
|
|
496
|
+
*/
|
|
497
|
+
#sessionFileRelocating: { source: string; dest: string } | null = null;
|
|
480
498
|
/** Atomic entry batch currently staged for a full-file commit. */
|
|
481
499
|
#atomicEntryBatch: AtomicEntryBatch | undefined;
|
|
482
500
|
|
|
@@ -747,24 +765,56 @@ export class SessionManager {
|
|
|
747
765
|
return this.#forceFileCreation || this.#fileIsCurrent || this.#historyContainsAssistantMessage();
|
|
748
766
|
}
|
|
749
767
|
|
|
768
|
+
/**
|
|
769
|
+
* Live path for concurrent completed appends during {@link moveTo}.
|
|
770
|
+
* Prefers destination once rename has landed (source gone); otherwise
|
|
771
|
+
* source. Never invents a path that does not already exist.
|
|
772
|
+
*/
|
|
773
|
+
#liveRelocationWritePath(): string | null {
|
|
774
|
+
const relocating = this.#sessionFileRelocating;
|
|
775
|
+
if (!relocating) return null;
|
|
776
|
+
if (this.#storage.existsSync(relocating.dest)) return relocating.dest;
|
|
777
|
+
if (this.#storage.existsSync(relocating.source)) return relocating.source;
|
|
778
|
+
// Rename in flight with neither path visible (rare cross-device edge):
|
|
779
|
+
// fall back to destination so we do not recreate a vacated source.
|
|
780
|
+
return relocating.dest;
|
|
781
|
+
}
|
|
782
|
+
|
|
750
783
|
/**
|
|
751
784
|
* Synchronously rewrite the whole file (header + entries) and keep no open
|
|
752
785
|
* writer; the next append re-opens one. `writeTextSync` returns with the
|
|
753
786
|
* bytes in the kernel page cache, so the file is software-crash durable.
|
|
787
|
+
*
|
|
788
|
+
* During {@link moveTo}, writes to the live relocation path (source pre-
|
|
789
|
+
* rename, destination post-rename) rather than always `#sessionFile`, so
|
|
790
|
+
* concurrent completed entries are durable without recreating a vacated source.
|
|
754
791
|
*/
|
|
755
792
|
#rewriteSynchronously(): void {
|
|
756
|
-
if (!this.#persist || !this.#
|
|
793
|
+
if (!this.#persist || !this.#shouldHaveSessionFile()) return;
|
|
794
|
+
const targetPath = this.#liveRelocationWritePath() ?? this.#sessionFile;
|
|
795
|
+
if (!targetPath) return;
|
|
757
796
|
|
|
758
797
|
try {
|
|
759
798
|
const body = this.#fileBody();
|
|
760
799
|
this.#diskEpoch++;
|
|
761
800
|
this.#diskTail = Promise.resolve();
|
|
762
801
|
this.#closeWriterEventually();
|
|
763
|
-
this.#storage.writeTextSync(
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
this.#
|
|
802
|
+
this.#storage.writeTextSync(targetPath, body);
|
|
803
|
+
// Only mark the manager current when writing the active session path.
|
|
804
|
+
// Mid-move writes update the live relocation path; `#sessionFile` is
|
|
805
|
+
// still the pre-repoint source until moveTo repoints it.
|
|
806
|
+
if (!this.#sessionFileRelocating || targetPath === this.#sessionFile) {
|
|
807
|
+
this.#fileIsCurrent = true;
|
|
808
|
+
this.#materializeBreadcrumb();
|
|
809
|
+
this.#rewriteRequired = false;
|
|
810
|
+
this.#hasTitleSlot = true;
|
|
811
|
+
} else {
|
|
812
|
+
// Destination body is current on disk; in-memory still needs a
|
|
813
|
+
// header-cwd rewrite after repoint, but entries are durable.
|
|
814
|
+
this.#fileIsCurrent = false;
|
|
815
|
+
this.#rewriteRequired = true;
|
|
816
|
+
this.#hasTitleSlot = true;
|
|
817
|
+
}
|
|
768
818
|
} catch (err) {
|
|
769
819
|
this.#noteDiskFailure(err);
|
|
770
820
|
}
|
|
@@ -850,17 +900,21 @@ export class SessionManager {
|
|
|
850
900
|
return;
|
|
851
901
|
}
|
|
852
902
|
|
|
853
|
-
// Atomic replacement window:
|
|
854
|
-
//
|
|
855
|
-
//
|
|
856
|
-
//
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
//
|
|
903
|
+
// Atomic replacement / move window: do not open a fresh append writer that
|
|
904
|
+
// a Windows EPERM replace could detach from the current JSONL path.
|
|
905
|
+
// - moveTo: write a full body to the live relocation path (source pre-
|
|
906
|
+
// rename, destination post-rename) so completed entries are durable
|
|
907
|
+
// without recreating a vacated source.
|
|
908
|
+
// - in-place atomic fence: supersede the pending publish with a
|
|
909
|
+
// synchronous full-body rewrite; bumping `#diskEpoch` abandons the
|
|
910
|
+
// in-flight atomic via its `commitGuard`.
|
|
911
|
+
if (this.#sessionFileRelocating) {
|
|
912
|
+
this.#rewriteSynchronously();
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
860
915
|
if (this.#atomicRewriteFenceEpoch !== null && this.#atomicRewriteFenceEpoch === this.#diskEpoch) {
|
|
861
|
-
this.#fileIsCurrent = false;
|
|
862
|
-
this.#rewriteRequired = true;
|
|
863
916
|
this.#atomicRewriteDirty = true;
|
|
917
|
+
this.#rewriteSynchronously();
|
|
864
918
|
return;
|
|
865
919
|
}
|
|
866
920
|
// Cold/divergent: not on disk yet, or in-memory entries diverged from the
|
|
@@ -870,15 +924,23 @@ export class SessionManager {
|
|
|
870
924
|
return;
|
|
871
925
|
}
|
|
872
926
|
|
|
873
|
-
// Hot path:
|
|
874
|
-
// chain.
|
|
875
|
-
//
|
|
927
|
+
// Hot path: write the entry directly on the writer, outside the async disk
|
|
928
|
+
// chain. Prefer appendSync so write failures latch `#diskFailure` before
|
|
929
|
+
// this call returns (not via a discarded rejected Promise after a later
|
|
930
|
+
// microtask). Callers stay non-throwing here — the core turn loop invokes
|
|
931
|
+
// appendMessage/appendCustomEntry without try/catch; flushSync/close and
|
|
932
|
+
// subsequent appends still throw the latched error. File writers apply
|
|
933
|
+
// each line to the OS page cache before return.
|
|
876
934
|
// A mid-close writer leaves `#writer` undefined, so `#appendWriter` simply
|
|
877
935
|
// opens a fresh append handle and the entry still lands.
|
|
878
936
|
try {
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
937
|
+
const writer = this.#appendWriter();
|
|
938
|
+
const line = this.#lineFor(entry);
|
|
939
|
+
if (writer.appendSync) {
|
|
940
|
+
writer.appendSync(line);
|
|
941
|
+
} else {
|
|
942
|
+
void writer.append(line).catch(err => this.#noteDiskFailure(err));
|
|
943
|
+
}
|
|
882
944
|
} catch (err) {
|
|
883
945
|
this.#noteDiskFailure(err);
|
|
884
946
|
}
|
|
@@ -893,6 +955,15 @@ export class SessionManager {
|
|
|
893
955
|
return;
|
|
894
956
|
}
|
|
895
957
|
|
|
958
|
+
// Title changes use their own asynchronous append path rather than
|
|
959
|
+
// #appendToSessionFile. During move, write the full body (including the
|
|
960
|
+
// title entry) to the live relocation path so a crash mid-move still
|
|
961
|
+
// keeps the title change; the trailing rewrite still updates header cwd.
|
|
962
|
+
if (this.#sessionFileRelocating) {
|
|
963
|
+
this.#rewriteSynchronously();
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
896
967
|
if (
|
|
897
968
|
!this.#fileIsCurrent ||
|
|
898
969
|
this.#rewriteRequired ||
|
|
@@ -1325,90 +1396,106 @@ export class SessionManager {
|
|
|
1325
1396
|
: computeDefaultSessionDir(resolvedCwd, this.#storage));
|
|
1326
1397
|
|
|
1327
1398
|
let sessionFileExisted = false;
|
|
1328
|
-
|
|
1399
|
+
// Track source+dest for concurrent completed appends during relocation
|
|
1400
|
+
// (see `#sessionFileRelocating`). Existence of either path decides the
|
|
1401
|
+
// live write target — not a `#diskEpoch` bump, which would cancel any
|
|
1402
|
+
// disk task already queued at the current epoch (e.g. a header-only
|
|
1403
|
+
// `ensureOnDisk()` materializing rewrite) before the drain below runs it.
|
|
1329
1404
|
if (this.#persist && this.#sessionFile) {
|
|
1330
|
-
this.#
|
|
1331
|
-
|
|
1332
|
-
this.#
|
|
1333
|
-
|
|
1334
|
-
const oldSessionFile = this.#sessionFile;
|
|
1335
|
-
const newSessionFile = path.join(nextSessionDir, path.basename(oldSessionFile));
|
|
1336
|
-
const oldArtifactsDir = artifactsDirectoryFor(oldSessionFile)!;
|
|
1337
|
-
const newArtifactsDir = artifactsDirectoryFor(newSessionFile)!;
|
|
1338
|
-
const sessionPathChanged = path.resolve(oldSessionFile) !== path.resolve(newSessionFile);
|
|
1339
|
-
const artifactPathChanged = path.resolve(oldArtifactsDir) !== path.resolve(newArtifactsDir);
|
|
1340
|
-
sessionFileExisted = this.#storage.existsSync(oldSessionFile);
|
|
1405
|
+
const source = this.#sessionFile;
|
|
1406
|
+
const dest = path.join(nextSessionDir, path.basename(source));
|
|
1407
|
+
this.#sessionFileRelocating = { source, dest };
|
|
1408
|
+
}
|
|
1341
1409
|
|
|
1342
|
-
|
|
1343
|
-
|
|
1410
|
+
try {
|
|
1411
|
+
if (this.#persist && this.#sessionFile) {
|
|
1412
|
+
this.#storage.ensureDirSync(nextSessionDir);
|
|
1413
|
+
await this.#drainAndCloseWriter();
|
|
1414
|
+
this.#clearDiskError();
|
|
1415
|
+
|
|
1416
|
+
const oldSessionFile = this.#sessionFile;
|
|
1417
|
+
const newSessionFile = path.join(nextSessionDir, path.basename(oldSessionFile));
|
|
1418
|
+
const oldArtifactsDir = artifactsDirectoryFor(oldSessionFile)!;
|
|
1419
|
+
const newArtifactsDir = artifactsDirectoryFor(newSessionFile)!;
|
|
1420
|
+
const sessionPathChanged = path.resolve(oldSessionFile) !== path.resolve(newSessionFile);
|
|
1421
|
+
const artifactPathChanged = path.resolve(oldArtifactsDir) !== path.resolve(newArtifactsDir);
|
|
1422
|
+
sessionFileExisted = this.#storage.existsSync(oldSessionFile);
|
|
1423
|
+
|
|
1424
|
+
let sessionMoved = false;
|
|
1425
|
+
let artifactsMoved = false;
|
|
1344
1426
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1427
|
+
try {
|
|
1428
|
+
if (sessionFileExisted && sessionPathChanged) {
|
|
1429
|
+
await fs.promises.rename(oldSessionFile, newSessionFile);
|
|
1430
|
+
sessionMoved = true;
|
|
1431
|
+
}
|
|
1350
1432
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1433
|
+
if (artifactPathChanged) {
|
|
1434
|
+
try {
|
|
1435
|
+
const artifactStat = await fs.promises.stat(oldArtifactsDir);
|
|
1436
|
+
if (artifactStat.isDirectory()) {
|
|
1437
|
+
await fs.promises.rename(oldArtifactsDir, newArtifactsDir);
|
|
1438
|
+
artifactsMoved = true;
|
|
1439
|
+
}
|
|
1440
|
+
} catch (err) {
|
|
1441
|
+
if (!isEnoent(err)) throw err;
|
|
1357
1442
|
}
|
|
1358
|
-
} catch (err) {
|
|
1359
|
-
if (!isEnoent(err)) throw err;
|
|
1360
1443
|
}
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1444
|
+
} catch (err) {
|
|
1445
|
+
if (artifactsMoved) {
|
|
1446
|
+
try {
|
|
1447
|
+
await fs.promises.rename(newArtifactsDir, oldArtifactsDir);
|
|
1448
|
+
} catch (rollbackErr) {
|
|
1449
|
+
throw new Error(
|
|
1450
|
+
`Failed to move artifacts and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1370
1453
|
}
|
|
1371
|
-
}
|
|
1372
1454
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1455
|
+
if (sessionMoved) {
|
|
1456
|
+
try {
|
|
1457
|
+
await fs.promises.rename(newSessionFile, oldSessionFile);
|
|
1458
|
+
} catch (rollbackErr) {
|
|
1459
|
+
throw new Error(
|
|
1460
|
+
`Failed to move session file and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1380
1463
|
}
|
|
1464
|
+
|
|
1465
|
+
throw err;
|
|
1381
1466
|
}
|
|
1382
1467
|
|
|
1383
|
-
|
|
1468
|
+
this.#sessionFile = newSessionFile;
|
|
1469
|
+
this.#artifactManager = null;
|
|
1470
|
+
this.#artifactManagerSessionFile = null;
|
|
1471
|
+
// Path is repointed; hot-path appends may use `#sessionFile` again.
|
|
1472
|
+
this.#sessionFileRelocating = null;
|
|
1384
1473
|
}
|
|
1385
1474
|
|
|
1386
|
-
this.#
|
|
1387
|
-
this.#
|
|
1388
|
-
this.#
|
|
1389
|
-
|
|
1475
|
+
this.#cwd = resolvedCwd;
|
|
1476
|
+
this.#sessionDir = nextSessionDir;
|
|
1477
|
+
this.#header.cwd = resolvedCwd;
|
|
1478
|
+
// Re-filter additional roots: the new cwd may have been an additional root,
|
|
1479
|
+
// or it may now contain/subsume one. Re-normalize to keep the invariant
|
|
1480
|
+
// that cwd is never also listed as an additional directory.
|
|
1481
|
+
if (this.#additionalDirectories.length > 0) {
|
|
1482
|
+
this.#additionalDirectories = this.#additionalDirectories.filter(d => d !== resolvedCwd);
|
|
1483
|
+
this.#header.additionalDirectories =
|
|
1484
|
+
this.#additionalDirectories.length > 0 ? this.#additionalDirectories : undefined;
|
|
1485
|
+
}
|
|
1390
1486
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
this.#additionalDirectories = this.#additionalDirectories.filter(d => d !== resolvedCwd);
|
|
1399
|
-
this.#header.additionalDirectories =
|
|
1400
|
-
this.#additionalDirectories.length > 0 ? this.#additionalDirectories : undefined;
|
|
1401
|
-
}
|
|
1487
|
+
// Rewrite at the new location when the file already existed (update cwd) or
|
|
1488
|
+
// there is in-memory output worth materializing; otherwise stay lazy.
|
|
1489
|
+
const hasAssistant = this.#historyContainsAssistantMessage();
|
|
1490
|
+
if (this.#persist && this.#sessionFile && (sessionFileExisted || hasAssistant)) {
|
|
1491
|
+
this.#forceFileCreation = true;
|
|
1492
|
+
await this.#rewriteAtomically();
|
|
1493
|
+
}
|
|
1402
1494
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
if (this.#persist && this.#sessionFile && (sessionFileExisted || hasAssistant)) {
|
|
1407
|
-
this.#forceFileCreation = true;
|
|
1408
|
-
await this.#rewriteAtomically();
|
|
1495
|
+
if (this.#sessionFile) this.#rememberBreadcrumb(resolvedCwd, this.#sessionFile);
|
|
1496
|
+
} finally {
|
|
1497
|
+
this.#sessionFileRelocating = null;
|
|
1409
1498
|
}
|
|
1410
|
-
|
|
1411
|
-
if (this.#sessionFile) this.#rememberBreadcrumb(resolvedCwd, this.#sessionFile);
|
|
1412
1499
|
}
|
|
1413
1500
|
|
|
1414
1501
|
/**
|
|
@@ -2,10 +2,17 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { getTerminalId } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getSessionsDir,
|
|
7
|
+
getTerminalSessionsDir,
|
|
8
|
+
isEnoent,
|
|
9
|
+
isRecord,
|
|
10
|
+
logger,
|
|
11
|
+
resolveEquivalentPath,
|
|
12
|
+
} from "@oh-my-pi/pi-utils";
|
|
6
13
|
import type { SessionStorage } from "./session-storage";
|
|
7
14
|
|
|
8
|
-
const
|
|
15
|
+
const SESSION_HEADER_PREFIX_BYTES = 4096;
|
|
9
16
|
|
|
10
17
|
/**
|
|
11
18
|
* Merge or rename a legacy session directory into its canonical target.
|
|
@@ -35,12 +42,16 @@ function encodeLegacyAbsoluteSessionDirName(cwd: string): string {
|
|
|
35
42
|
return `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
function
|
|
45
|
+
function encodeLegacyRelativeSessionDirName(prefix: string, relative: string): string {
|
|
39
46
|
const encoded = relative.replace(/[/\\:]/g, "-");
|
|
40
47
|
return encoded ? (prefix.endsWith("-") ? `${prefix}${encoded}` : `${prefix}-${encoded}`) : prefix;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
function getDefaultSessionDirName(cwd: string): {
|
|
50
|
+
function getDefaultSessionDirName(cwd: string): {
|
|
51
|
+
encodedDirName: string;
|
|
52
|
+
legacyRelativeDirName: string | undefined;
|
|
53
|
+
resolvedCwd: string;
|
|
54
|
+
} {
|
|
44
55
|
const resolvedCwd = path.resolve(cwd);
|
|
45
56
|
const canonicalCwd = resolveEquivalentPath(resolvedCwd);
|
|
46
57
|
const home = os.homedir();
|
|
@@ -49,72 +60,111 @@ function getDefaultSessionDirName(cwd: string): { encodedDirName: string; resolv
|
|
|
49
60
|
const canonicalTempRoot = resolveEquivalentPath(tempRoot);
|
|
50
61
|
const homeRelative = path.relative(canonicalHome, canonicalCwd);
|
|
51
62
|
const tempRelative = path.relative(canonicalTempRoot, canonicalCwd);
|
|
52
|
-
const encodedDirName =
|
|
53
|
-
homeRelative === "" || (!homeRelative.startsWith("..") && !path.isAbsolute(homeRelative))
|
|
54
|
-
? encodeRelativeSessionDirName("-", homeRelative)
|
|
55
|
-
: tempRelative === "" || (!tempRelative.startsWith("..") && !path.isAbsolute(tempRelative))
|
|
56
|
-
? encodeRelativeSessionDirName("-tmp", tempRelative)
|
|
57
|
-
: encodeLegacyAbsoluteSessionDirName(canonicalCwd);
|
|
58
|
-
return { encodedDirName, resolvedCwd };
|
|
59
|
-
}
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
64
|
+
let scope: "home" | "tmp" | "abs";
|
|
65
|
+
let legacyRelativeDirName: string | undefined;
|
|
66
|
+
if (homeRelative === "" || (!homeRelative.startsWith("..") && !path.isAbsolute(homeRelative))) {
|
|
67
|
+
scope = "home";
|
|
68
|
+
legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-", homeRelative);
|
|
69
|
+
} else if (tempRelative === "" || (!tempRelative.startsWith("..") && !path.isAbsolute(tempRelative))) {
|
|
70
|
+
scope = "tmp";
|
|
71
|
+
legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-tmp", tempRelative);
|
|
72
|
+
} else {
|
|
73
|
+
scope = "abs";
|
|
74
|
+
}
|
|
68
75
|
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
const normalized = canonicalCwd.replaceAll("\\", "/");
|
|
77
|
+
const readable = path
|
|
78
|
+
.basename(canonicalCwd)
|
|
79
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
80
|
+
.replace(/^-+|-+$/g, "")
|
|
81
|
+
.slice(-80);
|
|
82
|
+
const digest = Bun.SHA256.hash(normalized, "hex");
|
|
83
|
+
const encodedDirName = `${scope}-${readable || "project"}-${digest}`;
|
|
84
|
+
return { encodedDirName, legacyRelativeDirName, resolvedCwd };
|
|
85
|
+
}
|
|
73
86
|
|
|
74
|
-
|
|
87
|
+
function readSessionCwd(sessionFile: string, buffer: Buffer): string | undefined {
|
|
88
|
+
let descriptor: number | undefined;
|
|
75
89
|
try {
|
|
76
|
-
|
|
90
|
+
descriptor = fs.openSync(sessionFile, "r");
|
|
91
|
+
const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
92
|
+
const prefix = buffer.toString("utf8", 0, bytesRead);
|
|
93
|
+
for (const line of prefix.split(/\r?\n/, 3)) {
|
|
94
|
+
try {
|
|
95
|
+
const record: unknown = JSON.parse(line);
|
|
96
|
+
if (isRecord(record) && record.type === "session" && typeof record.cwd === "string") {
|
|
97
|
+
return record.cwd;
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// Ignore title slots or truncated/corrupt headers.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
77
103
|
} catch {
|
|
78
|
-
|
|
104
|
+
// Best-effort migration leaves unreadable sessions in the current bucket.
|
|
105
|
+
} finally {
|
|
106
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
79
107
|
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
80
110
|
|
|
111
|
+
function moveSessionBundle(sourceDir: string, targetDir: string, sessionName: string, entries: string[]): void {
|
|
112
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
113
|
+
const artifactsName = path.basename(sessionName, ".jsonl");
|
|
81
114
|
for (const entry of entries) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
if (entry !== sessionName && entry !== artifactsName && !entry.startsWith(`${sessionName}.`)) continue;
|
|
116
|
+
const source = path.join(sourceDir, entry);
|
|
117
|
+
if (!fs.existsSync(source)) continue;
|
|
118
|
+
const target = path.join(targetDir, entry);
|
|
119
|
+
const existing = fs.statSync(target, { throwIfNoEntry: false });
|
|
120
|
+
if (!existing) {
|
|
121
|
+
fs.renameSync(source, target);
|
|
122
|
+
} else if (existing.isDirectory() && fs.statSync(source).isDirectory()) {
|
|
123
|
+
migrateSessionDirPath(source, target);
|
|
87
124
|
} else {
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const newName = remainder ? `-${remainder}` : "-";
|
|
92
|
-
const oldPath = path.join(sessionsRoot, entry);
|
|
93
|
-
const newPath = path.join(sessionsRoot, newName);
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
migrateSessionDirPath(oldPath, newPath);
|
|
97
|
-
} catch {
|
|
98
|
-
// Best effort
|
|
125
|
+
fs.rmSync(source, { recursive: true, force: true });
|
|
99
126
|
}
|
|
100
127
|
}
|
|
101
128
|
}
|
|
102
129
|
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
130
|
+
function rerouteCollidingSessions(
|
|
131
|
+
cwd: string,
|
|
132
|
+
legacyDir: string,
|
|
133
|
+
sessionsRoot: string,
|
|
134
|
+
kind: "relative" | "absolute",
|
|
135
|
+
): void {
|
|
136
|
+
const entries = fs.readdirSync(legacyDir);
|
|
137
|
+
const currentCwd = resolveEquivalentPath(path.resolve(cwd));
|
|
138
|
+
const buffer = Buffer.allocUnsafe(SESSION_HEADER_PREFIX_BYTES);
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
141
|
+
const recordedCwd = readSessionCwd(path.join(legacyDir, entry), buffer);
|
|
142
|
+
if (!recordedCwd) continue;
|
|
143
|
+
const recordedCanonical = resolveEquivalentPath(path.resolve(recordedCwd));
|
|
144
|
+
if (
|
|
145
|
+
recordedCanonical === currentCwd ||
|
|
146
|
+
!fs.statSync(recordedCanonical, { throwIfNoEntry: false })?.isDirectory()
|
|
147
|
+
) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const recordedNames = getDefaultSessionDirName(recordedCanonical);
|
|
151
|
+
const recordedLegacyName =
|
|
152
|
+
kind === "relative"
|
|
153
|
+
? recordedNames.legacyRelativeDirName
|
|
154
|
+
: encodeLegacyAbsoluteSessionDirName(recordedCanonical);
|
|
155
|
+
if (recordedLegacyName !== path.basename(legacyDir)) continue;
|
|
156
|
+
moveSessionBundle(legacyDir, path.join(sessionsRoot, recordedNames.encodedDirName), entry, entries);
|
|
111
157
|
}
|
|
112
158
|
}
|
|
113
159
|
|
|
114
160
|
export function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | undefined {
|
|
115
161
|
const currentDirName = path.basename(sessionDir);
|
|
116
|
-
const { encodedDirName } = getDefaultSessionDirName(cwd);
|
|
117
|
-
if (
|
|
162
|
+
const { encodedDirName, legacyRelativeDirName } = getDefaultSessionDirName(cwd);
|
|
163
|
+
if (
|
|
164
|
+
currentDirName !== encodedDirName &&
|
|
165
|
+
currentDirName !== legacyRelativeDirName &&
|
|
166
|
+
currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)
|
|
167
|
+
) {
|
|
118
168
|
return undefined;
|
|
119
169
|
}
|
|
120
170
|
return path.dirname(sessionDir);
|
|
@@ -130,10 +180,23 @@ export function computeDefaultSessionDir(
|
|
|
130
180
|
storage: SessionStorage,
|
|
131
181
|
sessionsRoot: string = getSessionsDir(),
|
|
132
182
|
): string {
|
|
133
|
-
const { encodedDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
|
|
134
|
-
migrateHomeSessionDirs(sessionsRoot);
|
|
183
|
+
const { encodedDirName, legacyRelativeDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
|
|
135
184
|
const sessionDir = path.join(sessionsRoot, encodedDirName);
|
|
136
|
-
|
|
185
|
+
const legacyDirs: Array<{ kind: "relative" | "absolute"; name: string | undefined }> = [
|
|
186
|
+
{ kind: "relative", name: legacyRelativeDirName },
|
|
187
|
+
{ kind: "absolute", name: encodeLegacyAbsoluteSessionDirName(resolvedCwd) },
|
|
188
|
+
];
|
|
189
|
+
for (const legacy of legacyDirs) {
|
|
190
|
+
if (!legacy.name) continue;
|
|
191
|
+
const legacyDir = path.join(sessionsRoot, legacy.name);
|
|
192
|
+
if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) continue;
|
|
193
|
+
try {
|
|
194
|
+
rerouteCollidingSessions(resolvedCwd, legacyDir, sessionsRoot, legacy.kind);
|
|
195
|
+
migrateSessionDirPath(legacyDir, sessionDir);
|
|
196
|
+
} catch {
|
|
197
|
+
// Best effort
|
|
198
|
+
}
|
|
199
|
+
}
|
|
137
200
|
storage.ensureDirSync(sessionDir);
|
|
138
201
|
return sessionDir;
|
|
139
202
|
}
|
|
@@ -34,6 +34,11 @@ export interface SessionStatsTrackerHost {
|
|
|
34
34
|
sessionId(): string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function correctedPromptTokens(assistant: AssistantMessage): number {
|
|
38
|
+
const providerPromptTokens = assistant.contextSnapshot?.promptTokens ?? calculatePromptTokens(assistant.usage);
|
|
39
|
+
return Math.max(0, providerPromptTokens - (assistant.contextSnapshot?.historyRewriteTokensRemoved ?? 0));
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
/** Computes session totals and tracks the in-flight context estimate. */
|
|
38
43
|
export class SessionStatsTracker {
|
|
39
44
|
readonly #host: SessionStatsTrackerHost;
|
|
@@ -160,8 +165,7 @@ export class SessionStatsTracker {
|
|
|
160
165
|
const useAnchor =
|
|
161
166
|
anchorAssistant !== undefined && anchorIndex !== -1 && (!pending || anchorIndex >= pending.cutoffCount);
|
|
162
167
|
if (useAnchor && anchorAssistant) {
|
|
163
|
-
const promptTokens =
|
|
164
|
-
anchorAssistant.contextSnapshot?.promptTokens ?? calculatePromptTokens(anchorAssistant.usage);
|
|
168
|
+
const promptTokens = correctedPromptTokens(anchorAssistant);
|
|
165
169
|
const nonMessageTokens =
|
|
166
170
|
anchorAssistant.contextSnapshot?.nonMessageTokens ?? computeNonMessageTokens(this.#host.session);
|
|
167
171
|
anchored = true;
|
|
@@ -199,7 +203,7 @@ export class SessionStatsTracker {
|
|
|
199
203
|
) {
|
|
200
204
|
continue;
|
|
201
205
|
}
|
|
202
|
-
const promptTokens =
|
|
206
|
+
const promptTokens = correctedPromptTokens(message);
|
|
203
207
|
const nonMessageTokens =
|
|
204
208
|
message.contextSnapshot?.nonMessageTokens ?? computeNonMessageTokens(this.#host.session);
|
|
205
209
|
let tailTokens = 0;
|
|
@@ -256,6 +260,47 @@ export class SessionStatsTracker {
|
|
|
256
260
|
return this.#pendingContextSnapshot?.nonMessageTokens;
|
|
257
261
|
}
|
|
258
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Apply an estimated prompt-prefix reduction to the current provider anchor.
|
|
265
|
+
*
|
|
266
|
+
* History after the anchor is estimated live by {@link getContextBreakdown};
|
|
267
|
+
* callers must pass only savings from entries already included in the
|
|
268
|
+
* anchor's provider-reported prompt. Persisting the correction on the
|
|
269
|
+
* assistant snapshot keeps reloads accurate, and the next successful
|
|
270
|
+
* assistant response naturally replaces it with a fresh provider anchor.
|
|
271
|
+
*/
|
|
272
|
+
recordAnchoredHistoryRewrite(tokensRemoved: number): void {
|
|
273
|
+
if (!Number.isFinite(tokensRemoved) || tokensRemoved <= 0) return;
|
|
274
|
+
|
|
275
|
+
const branchEntries = this.#host.sessionManager.getBranch();
|
|
276
|
+
const latestCompaction = getLatestCompactionEntry(branchEntries);
|
|
277
|
+
const compactionIndex = latestCompaction ? branchEntries.lastIndexOf(latestCompaction) : -1;
|
|
278
|
+
for (let index = branchEntries.length - 1; index > compactionIndex; index--) {
|
|
279
|
+
const entry = branchEntries[index];
|
|
280
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
281
|
+
const assistant = entry.message;
|
|
282
|
+
if (
|
|
283
|
+
assistant.stopReason === "aborted" ||
|
|
284
|
+
assistant.stopReason === "error" ||
|
|
285
|
+
!assistant.usage ||
|
|
286
|
+
!hasContextTokenUsage(assistant.usage)
|
|
287
|
+
) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!assistant.contextSnapshot) {
|
|
292
|
+
assistant.contextSnapshot = {
|
|
293
|
+
promptTokens: calculatePromptTokens(assistant.usage),
|
|
294
|
+
nonMessageTokens: computeNonMessageTokens(this.#host.session),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const snapshot = assistant.contextSnapshot;
|
|
298
|
+
snapshot.historyRewriteTokensRemoved = (snapshot.historyRewriteTokensRemoved ?? 0) + Math.floor(tokensRemoved);
|
|
299
|
+
this.#contextUsageRevision++;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
259
304
|
/** Sets or clears the in-flight context snapshot. */
|
|
260
305
|
setPendingSnapshot(snapshot: PendingContextSnapshot | undefined): void {
|
|
261
306
|
this.#pendingContextSnapshot = snapshot;
|