@kr78/pi-coding-agent 17.2.10
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 +14861 -0
- package/README.md +35 -0
- package/dist/CHANGELOG-chtdxh1z.md +14861 -0
- package/dist/cli.js +20483 -0
- package/dist/template-c2hyaytt.js +1653 -0
- package/dist/template-f8wx9vfn.css +1355 -0
- package/dist/template-qat058wr.html +55 -0
- package/dist/tool-views.generated-jdfmzwmn.js +35 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +104 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/extensions/README.md +142 -0
- package/examples/extensions/api-demo.ts +78 -0
- package/examples/extensions/chalk-logger.ts +25 -0
- package/examples/extensions/hello.ts +31 -0
- package/examples/extensions/pirate.ts +43 -0
- package/examples/extensions/plan-mode.ts +549 -0
- package/examples/extensions/reload-runtime.ts +38 -0
- package/examples/extensions/thinking-note.ts +13 -0
- package/examples/extensions/tools.ts +145 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +17 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +48 -0
- package/examples/hooks/confirm-destructive.ts +58 -0
- package/examples/hooks/custom-compaction.ts +115 -0
- package/examples/hooks/dirty-repo-guard.ts +51 -0
- package/examples/hooks/file-trigger.ts +40 -0
- package/examples/hooks/git-checkpoint.ts +52 -0
- package/examples/hooks/handoff.ts +149 -0
- package/examples/hooks/permission-gate.ts +33 -0
- package/examples/hooks/protected-paths.ts +29 -0
- package/examples/hooks/qna.ts +118 -0
- package/examples/hooks/status-line.ts +39 -0
- package/examples/sdk/01-minimal.ts +21 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +46 -0
- package/examples/sdk/04-skills.ts +43 -0
- package/examples/sdk/06-extensions.ts +82 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +35 -0
- package/examples/sdk/08-prompt-templates.ts +41 -0
- package/examples/sdk/08-slash-commands.ts +46 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
- package/examples/sdk/11-sessions.ts +47 -0
- package/examples/sdk/12-redis-sessions.ts +54 -0
- package/examples/sdk/13-sql-sessions.ts +61 -0
- package/examples/sdk/README.md +169 -0
- package/package.json +587 -0
- package/scripts/bench-guard.ts +71 -0
- package/scripts/bench-title-models.ts +332 -0
- package/scripts/build-binary.ts +117 -0
- package/scripts/bundle-dist.ts +123 -0
- package/scripts/compile-binary.ts +69 -0
- package/scripts/embed-mupdf-wasm.ts +67 -0
- package/scripts/format-prompts.ts +68 -0
- package/scripts/generate-aria-snapshot.ts +134 -0
- package/scripts/generate-docs-index.ts +58 -0
- package/scripts/generate-share-viewer.ts +34 -0
- package/scripts/legacy-pi-virtual-module.ts +205 -0
- package/scripts/measure-prompt-tokens.ts +63 -0
- package/scripts/omp +51 -0
- package/scripts/omp.ts +19 -0
- package/scripts/security-compare.ts +40 -0
- package/src/advisor/advise-tool.ts +234 -0
- package/src/advisor/config.ts +341 -0
- package/src/advisor/emission-guard.ts +172 -0
- package/src/advisor/index.ts +6 -0
- package/src/advisor/runtime.ts +1412 -0
- package/src/advisor/transcript-recorder.ts +215 -0
- package/src/advisor/watchdog.ts +135 -0
- package/src/async/index.ts +1 -0
- package/src/async/job-manager.ts +847 -0
- package/src/auto-thinking/classifier.ts +220 -0
- package/src/autolearn/controller.ts +152 -0
- package/src/autolearn/managed-skills.ts +255 -0
- package/src/autoresearch/command-resume.md +14 -0
- package/src/autoresearch/dashboard.ts +436 -0
- package/src/autoresearch/git.ts +331 -0
- package/src/autoresearch/helpers.ts +218 -0
- package/src/autoresearch/index.ts +541 -0
- package/src/autoresearch/prompt-setup.md +43 -0
- package/src/autoresearch/prompt.md +103 -0
- package/src/autoresearch/resume-message.md +10 -0
- package/src/autoresearch/state.ts +273 -0
- package/src/autoresearch/storage.ts +700 -0
- package/src/autoresearch/tools/init-experiment.ts +268 -0
- package/src/autoresearch/tools/log-experiment.ts +520 -0
- package/src/autoresearch/tools/run-experiment.ts +407 -0
- package/src/autoresearch/tools/update-notes.ts +109 -0
- package/src/autoresearch/types.ts +168 -0
- package/src/capability/context-file.ts +44 -0
- package/src/capability/extension-module.ts +34 -0
- package/src/capability/extension.ts +47 -0
- package/src/capability/fs.ts +117 -0
- package/src/capability/hook.ts +40 -0
- package/src/capability/index.ts +467 -0
- package/src/capability/instruction.ts +37 -0
- package/src/capability/mcp.ts +115 -0
- package/src/capability/prompt.ts +35 -0
- package/src/capability/rule-buckets.ts +66 -0
- package/src/capability/rule.ts +298 -0
- package/src/capability/settings.ts +34 -0
- package/src/capability/skill.ts +69 -0
- package/src/capability/slash-command.ts +40 -0
- package/src/capability/ssh.ts +41 -0
- package/src/capability/system-prompt.ts +34 -0
- package/src/capability/tool.ts +38 -0
- package/src/capability/types.ts +187 -0
- package/src/cleanse/agent.ts +226 -0
- package/src/cleanse/balance.ts +79 -0
- package/src/cleanse/checkers.ts +996 -0
- package/src/cleanse/index.ts +190 -0
- package/src/cleanse/loop.ts +51 -0
- package/src/cleanse/parsers.ts +726 -0
- package/src/cleanse/progress.ts +50 -0
- package/src/cleanse/prompts/assignment.md +47 -0
- package/src/cleanse/types.ts +72 -0
- package/src/cli/agents-cli.ts +138 -0
- package/src/cli/args.ts +368 -0
- package/src/cli/auth-broker-cli.ts +940 -0
- package/src/cli/auth-gateway-cli.ts +674 -0
- package/src/cli/bench-cli.ts +990 -0
- package/src/cli/browser-relay-cli.ts +119 -0
- package/src/cli/classify-install-target.ts +76 -0
- package/src/cli/claude-trace-cli.ts +795 -0
- package/src/cli/command-help.ts +107 -0
- package/src/cli/commands/init-xdg.ts +27 -0
- package/src/cli/completion-gen.ts +550 -0
- package/src/cli/config-cli.ts +459 -0
- package/src/cli/dry-balance-cli.ts +864 -0
- package/src/cli/extension-flags.ts +48 -0
- package/src/cli/file-processor.ts +132 -0
- package/src/cli/flag-tables.ts +367 -0
- package/src/cli/gallery-cli.ts +272 -0
- package/src/cli/gallery-fixtures/agentic.ts +420 -0
- package/src/cli/gallery-fixtures/codeintel.ts +187 -0
- package/src/cli/gallery-fixtures/edit.ts +254 -0
- package/src/cli/gallery-fixtures/fs.ts +245 -0
- package/src/cli/gallery-fixtures/index.ts +40 -0
- package/src/cli/gallery-fixtures/interaction.ts +46 -0
- package/src/cli/gallery-fixtures/memory.ts +81 -0
- package/src/cli/gallery-fixtures/misc.ts +177 -0
- package/src/cli/gallery-fixtures/search.ts +135 -0
- package/src/cli/gallery-fixtures/shell.ts +241 -0
- package/src/cli/gallery-fixtures/types.ts +57 -0
- package/src/cli/gallery-fixtures/web.ts +158 -0
- package/src/cli/gallery-screenshot.ts +279 -0
- package/src/cli/gc-cli.ts +1566 -0
- package/src/cli/grep-cli.ts +161 -0
- package/src/cli/grievances-cli.ts +256 -0
- package/src/cli/help-extra.ts +89 -0
- package/src/cli/initial-message.ts +58 -0
- package/src/cli/models-cli.ts +385 -0
- package/src/cli/plugin-cli.ts +996 -0
- package/src/cli/profile-alias.ts +369 -0
- package/src/cli/profile-bootstrap.ts +233 -0
- package/src/cli/read-cli.ts +99 -0
- package/src/cli/session-picker.ts +110 -0
- package/src/cli/setup-cli.ts +312 -0
- package/src/cli/setup-model-picker.ts +43 -0
- package/src/cli/shell-cli.ts +176 -0
- package/src/cli/ssh-cli.ts +179 -0
- package/src/cli/startup-cwd.ts +58 -0
- package/src/cli/stats-cli.ts +229 -0
- package/src/cli/thinking-levels.ts +7 -0
- package/src/cli/tiny-models-cli.ts +153 -0
- package/src/cli/ttsr-cli.ts +1013 -0
- package/src/cli/update-cli.ts +1184 -0
- package/src/cli/usage-cli.ts +1084 -0
- package/src/cli/usage-error.ts +7 -0
- package/src/cli/web-search-cli.ts +144 -0
- package/src/cli/worktree-cli.ts +311 -0
- package/src/cli-commands.ts +308 -0
- package/src/cli.ts +428 -0
- package/src/collab/crypto.ts +63 -0
- package/src/collab/display-name.ts +13 -0
- package/src/collab/guest.ts +764 -0
- package/src/collab/host.ts +692 -0
- package/src/collab/protocol.ts +296 -0
- package/src/collab/relay-client.ts +282 -0
- package/src/collab/replication-shrink.ts +111 -0
- package/src/commands/acp.ts +35 -0
- package/src/commands/agents.ts +58 -0
- package/src/commands/auth-broker.ts +100 -0
- package/src/commands/auth-gateway.ts +70 -0
- package/src/commands/bench.ts +64 -0
- package/src/commands/browser-relay.ts +53 -0
- package/src/commands/cleanse.ts +45 -0
- package/src/commands/commit.ts +47 -0
- package/src/commands/complete.ts +67 -0
- package/src/commands/completions.ts +61 -0
- package/src/commands/config.ts +52 -0
- package/src/commands/dry-balance.ts +43 -0
- package/src/commands/gallery.ts +61 -0
- package/src/commands/gc.ts +47 -0
- package/src/commands/grep.ts +49 -0
- package/src/commands/grievances.ts +52 -0
- package/src/commands/install.ts +107 -0
- package/src/commands/join.ts +40 -0
- package/src/commands/launch-help.ts +116 -0
- package/src/commands/launch.ts +34 -0
- package/src/commands/models.ts +61 -0
- package/src/commands/plugin.ts +79 -0
- package/src/commands/read.ts +39 -0
- package/src/commands/say.ts +146 -0
- package/src/commands/setup.ts +68 -0
- package/src/commands/share.ts +71 -0
- package/src/commands/shell.ts +30 -0
- package/src/commands/ssh.ts +61 -0
- package/src/commands/stats.ts +30 -0
- package/src/commands/tiny-models.ts +36 -0
- package/src/commands/token.ts +164 -0
- package/src/commands/ttsr.ts +125 -0
- package/src/commands/update.ts +34 -0
- package/src/commands/usage.ts +55 -0
- package/src/commands/web-search.ts +43 -0
- package/src/commands/worktree.ts +63 -0
- package/src/commit/agentic/agent.ts +322 -0
- package/src/commit/agentic/fallback.ts +96 -0
- package/src/commit/agentic/index.ts +383 -0
- package/src/commit/agentic/lock-files.ts +107 -0
- package/src/commit/agentic/prompts/analyze-file.md +22 -0
- package/src/commit/agentic/prompts/session-user.md +25 -0
- package/src/commit/agentic/prompts/split-confirm.md +1 -0
- package/src/commit/agentic/prompts/system.md +38 -0
- package/src/commit/agentic/state.ts +60 -0
- package/src/commit/agentic/tools/analyze-file.ts +148 -0
- package/src/commit/agentic/tools/git-file-diff.ts +191 -0
- package/src/commit/agentic/tools/git-hunk.ts +52 -0
- package/src/commit/agentic/tools/git-overview.ts +62 -0
- package/src/commit/agentic/tools/index.ts +54 -0
- package/src/commit/agentic/tools/propose-changelog.ts +147 -0
- package/src/commit/agentic/tools/propose-commit.ts +109 -0
- package/src/commit/agentic/tools/recent-commits.ts +81 -0
- package/src/commit/agentic/tools/schemas.ts +11 -0
- package/src/commit/agentic/tools/split-commit.ts +241 -0
- package/src/commit/agentic/topo-sort.ts +44 -0
- package/src/commit/agentic/trivial.ts +51 -0
- package/src/commit/agentic/validation.ts +183 -0
- package/src/commit/analysis/conventional.ts +64 -0
- package/src/commit/analysis/index.ts +4 -0
- package/src/commit/analysis/scope.ts +242 -0
- package/src/commit/analysis/summary.ts +107 -0
- package/src/commit/analysis/validation.ts +66 -0
- package/src/commit/changelog/detect.ts +40 -0
- package/src/commit/changelog/generate.ts +101 -0
- package/src/commit/changelog/index.ts +234 -0
- package/src/commit/changelog/parse.ts +44 -0
- package/src/commit/cli.ts +85 -0
- package/src/commit/git/diff.ts +148 -0
- package/src/commit/index.ts +5 -0
- package/src/commit/map-reduce/index.ts +69 -0
- package/src/commit/map-reduce/map-phase.ts +193 -0
- package/src/commit/map-reduce/reduce-phase.ts +49 -0
- package/src/commit/map-reduce/utils.ts +9 -0
- package/src/commit/message.ts +11 -0
- package/src/commit/model-selection.ts +95 -0
- package/src/commit/pipeline.ts +244 -0
- package/src/commit/prompts/analysis-system.md +148 -0
- package/src/commit/prompts/analysis-user.md +38 -0
- package/src/commit/prompts/changelog-system.md +50 -0
- package/src/commit/prompts/changelog-user.md +18 -0
- package/src/commit/prompts/file-observer-system.md +24 -0
- package/src/commit/prompts/file-observer-user.md +8 -0
- package/src/commit/prompts/reduce-system.md +50 -0
- package/src/commit/prompts/reduce-user.md +17 -0
- package/src/commit/prompts/summary-retry.md +3 -0
- package/src/commit/prompts/summary-system.md +38 -0
- package/src/commit/prompts/summary-user.md +13 -0
- package/src/commit/prompts/types-description.md +2 -0
- package/src/commit/shared-llm.ts +70 -0
- package/src/commit/types.ts +118 -0
- package/src/commit/utils/exclusions.ts +42 -0
- package/src/commit/utils.ts +58 -0
- package/src/config/api-key-resolver.ts +81 -0
- package/src/config/append-only-context-mode.ts +76 -0
- package/src/config/config-file.ts +347 -0
- package/src/config/inline-tool-descriptors-mode.ts +26 -0
- package/src/config/keybindings.ts +703 -0
- package/src/config/mcp-schema.json +252 -0
- package/src/config/model-discovery.ts +1077 -0
- package/src/config/model-registry.ts +2822 -0
- package/src/config/model-resolver.ts +2115 -0
- package/src/config/model-roles.ts +113 -0
- package/src/config/models-config-schema-bundle.ts +315 -0
- package/src/config/models-config-schema.ts +14 -0
- package/src/config/models-config.ts +130 -0
- package/src/config/prompt-templates.ts +205 -0
- package/src/config/provider-globals.ts +25 -0
- package/src/config/resolve-config-value.ts +95 -0
- package/src/config/service-tier.ts +146 -0
- package/src/config/settings-schema.ts +5887 -0
- package/src/config/settings.ts +2394 -0
- package/src/config.ts +242 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +960 -0
- package/src/dap/client.ts +1043 -0
- package/src/dap/config.ts +480 -0
- package/src/dap/defaults.json +212 -0
- package/src/dap/index.ts +4 -0
- package/src/dap/session.ts +1841 -0
- package/src/dap/types.ts +611 -0
- package/src/debug/index.ts +584 -0
- package/src/debug/log-formatting.ts +58 -0
- package/src/debug/log-viewer.ts +966 -0
- package/src/debug/profiler.ts +168 -0
- package/src/debug/protocol-probe.ts +267 -0
- package/src/debug/raw-sse-buffer.ts +421 -0
- package/src/debug/raw-sse.ts +312 -0
- package/src/debug/remote-debugger.ts +151 -0
- package/src/debug/report-bundle.ts +411 -0
- package/src/debug/system-info.ts +111 -0
- package/src/debug/terminal-info.ts +124 -0
- package/src/discovery/agent-plugin-format.ts +551 -0
- package/src/discovery/agent-plugins.ts +341 -0
- package/src/discovery/agents-md.ts +67 -0
- package/src/discovery/agents.ts +300 -0
- package/src/discovery/at-imports.ts +273 -0
- package/src/discovery/builtin-defaults.ts +39 -0
- package/src/discovery/builtin-rules/go-add-cleanup.md +33 -0
- package/src/discovery/builtin-rules/go-bench-loop.md +36 -0
- package/src/discovery/builtin-rules/go-exp-promoted.md +40 -0
- package/src/discovery/builtin-rules/go-ioutil.md +37 -0
- package/src/discovery/builtin-rules/go-join-hostport.md +30 -0
- package/src/discovery/builtin-rules/go-new-expr.md +44 -0
- package/src/discovery/builtin-rules/go-rand-v2.md +41 -0
- package/src/discovery/builtin-rules/go-range-int.md +45 -0
- package/src/discovery/builtin-rules/index.ts +74 -0
- package/src/discovery/builtin-rules/rs-box-leak.md +49 -0
- package/src/discovery/builtin-rules/rs-future-prelude.md +24 -0
- package/src/discovery/builtin-rules/rs-lazylock.md +52 -0
- package/src/discovery/builtin-rules/rs-match-ergonomics.md +68 -0
- package/src/discovery/builtin-rules/rs-parking-lot.md +45 -0
- package/src/discovery/builtin-rules/rs-result-type.md +20 -0
- package/src/discovery/builtin-rules/ts-bare-catch.md +39 -0
- package/src/discovery/builtin-rules/ts-import-type.md +43 -0
- package/src/discovery/builtin-rules/ts-no-any.md +66 -0
- package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +45 -0
- package/src/discovery/builtin-rules/ts-no-dynamic-import.md +40 -0
- package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +56 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
- package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +51 -0
- package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +66 -0
- package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
- package/src/discovery/builtin-rules/ts-set-map.md +28 -0
- package/src/discovery/builtin.ts +945 -0
- package/src/discovery/claude-plugins.ts +640 -0
- package/src/discovery/claude.ts +591 -0
- package/src/discovery/cline.ts +83 -0
- package/src/discovery/codex.ts +553 -0
- package/src/discovery/contained-path.ts +78 -0
- package/src/discovery/cursor.ts +223 -0
- package/src/discovery/gemini.ts +386 -0
- package/src/discovery/github.ts +337 -0
- package/src/discovery/helpers.ts +1183 -0
- package/src/discovery/index.ts +82 -0
- package/src/discovery/mcp-json.ts +182 -0
- package/src/discovery/omp-extension-roots.ts +272 -0
- package/src/discovery/omp-plugins.ts +409 -0
- package/src/discovery/opencode.ts +441 -0
- package/src/discovery/plugin-dir-roots.ts +28 -0
- package/src/discovery/ssh.ts +153 -0
- package/src/discovery/substitute-plugin-root.ts +77 -0
- package/src/discovery/vscode.ts +106 -0
- package/src/discovery/windsurf.ts +149 -0
- package/src/edit/apply-patch/index.ts +87 -0
- package/src/edit/apply-patch/parser.ts +174 -0
- package/src/edit/diff.ts +1008 -0
- package/src/edit/edit-clipboard.ts +23 -0
- package/src/edit/file-snapshot-store.ts +148 -0
- package/src/edit/hashline/block-resolver.ts +33 -0
- package/src/edit/hashline/diff.ts +402 -0
- package/src/edit/hashline/execute.ts +313 -0
- package/src/edit/hashline/filesystem.ts +247 -0
- package/src/edit/hashline/index.ts +5 -0
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/edit/hashline/params.ts +12 -0
- package/src/edit/index.ts +699 -0
- package/src/edit/modes/apply-patch.lark +19 -0
- package/src/edit/modes/apply-patch.ts +53 -0
- package/src/edit/modes/patch.ts +1958 -0
- package/src/edit/modes/replace.ts +1191 -0
- package/src/edit/normalize.ts +345 -0
- package/src/edit/notebook.ts +242 -0
- package/src/edit/read-file.ts +25 -0
- package/src/edit/renderer.ts +998 -0
- package/src/edit/snapshot-details.ts +77 -0
- package/src/edit/streaming.ts +710 -0
- package/src/eval/agent-bridge.ts +224 -0
- package/src/eval/backend-helpers.ts +48 -0
- package/src/eval/backend.ts +71 -0
- package/src/eval/bridge-timeout.ts +65 -0
- package/src/eval/budget-bridge.ts +48 -0
- package/src/eval/completion-bridge.ts +212 -0
- package/src/eval/concurrency-bridge.ts +34 -0
- package/src/eval/executor-base.ts +562 -0
- package/src/eval/idle-timeout.ts +91 -0
- package/src/eval/index.ts +6 -0
- package/src/eval/jl/executor.ts +551 -0
- package/src/eval/jl/index.ts +54 -0
- package/src/eval/jl/kernel.ts +236 -0
- package/src/eval/jl/prelude.jl +736 -0
- package/src/eval/jl/prelude.ts +3 -0
- package/src/eval/jl/runner.jl +666 -0
- package/src/eval/jl/runtime.ts +118 -0
- package/src/eval/js/context-manager.ts +872 -0
- package/src/eval/js/executor.ts +178 -0
- package/src/eval/js/index.ts +41 -0
- package/src/eval/js/process-entry.ts +31 -0
- package/src/eval/js/shared/helpers.ts +170 -0
- package/src/eval/js/shared/indirect-eval.ts +30 -0
- package/src/eval/js/shared/local-module-loader.ts +364 -0
- package/src/eval/js/shared/prelude.ts +2 -0
- package/src/eval/js/shared/prelude.txt +294 -0
- package/src/eval/js/shared/rewrite-imports.ts +550 -0
- package/src/eval/js/shared/runtime.ts +590 -0
- package/src/eval/js/shared/types.ts +18 -0
- package/src/eval/js/tool-bridge.ts +163 -0
- package/src/eval/js/worker-core.ts +380 -0
- package/src/eval/js/worker-entry.ts +37 -0
- package/src/eval/js/worker-protocol.ts +47 -0
- package/src/eval/kernel-base.ts +569 -0
- package/src/eval/py/display.ts +71 -0
- package/src/eval/py/executor.ts +643 -0
- package/src/eval/py/index.ts +57 -0
- package/src/eval/py/kernel.ts +235 -0
- package/src/eval/py/prelude.py +673 -0
- package/src/eval/py/prelude.ts +3 -0
- package/src/eval/py/runner.py +1425 -0
- package/src/eval/py/runtime.ts +276 -0
- package/src/eval/py/spawn-options.ts +134 -0
- package/src/eval/py/tool-bridge.ts +201 -0
- package/src/eval/rb/executor.ts +511 -0
- package/src/eval/rb/index.ts +54 -0
- package/src/eval/rb/kernel.ts +231 -0
- package/src/eval/rb/prelude.rb +551 -0
- package/src/eval/rb/prelude.ts +3 -0
- package/src/eval/rb/runner.rb +581 -0
- package/src/eval/rb/runtime.ts +132 -0
- package/src/eval/runtime-env.ts +104 -0
- package/src/eval/session-id.ts +8 -0
- package/src/eval/types.ts +48 -0
- package/src/exa/index.ts +2 -0
- package/src/exa/mcp-client.ts +370 -0
- package/src/exa/types.ts +69 -0
- package/src/exec/bash-executor.ts +627 -0
- package/src/exec/direnv.ts +145 -0
- package/src/exec/exec.ts +53 -0
- package/src/exec/non-interactive-env.ts +118 -0
- package/src/export/custom-share.ts +65 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +320 -0
- package/src/export/html/share-loader.js +102 -0
- package/src/export/html/template.css +1355 -0
- package/src/export/html/template.html +55 -0
- package/src/export/html/template.js +1653 -0
- package/src/export/html/tool-views.generated.js +35 -0
- package/src/export/html/vendor/highlight.min.js +1213 -0
- package/src/export/html/vendor/marked.min.js +6 -0
- package/src/export/html/web-palette.ts +126 -0
- package/src/export/share.ts +686 -0
- package/src/export/ttsr.ts +590 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +54 -0
- package/src/extensibility/custom-commands/bundled/review/index.ts +698 -0
- package/src/extensibility/custom-commands/index.ts +2 -0
- package/src/extensibility/custom-commands/loader.ts +242 -0
- package/src/extensibility/custom-commands/types.ts +119 -0
- package/src/extensibility/custom-tools/index.ts +7 -0
- package/src/extensibility/custom-tools/loader.ts +301 -0
- package/src/extensibility/custom-tools/types.ts +287 -0
- package/src/extensibility/custom-tools/wrapper.ts +50 -0
- package/src/extensibility/extensions/compact-handler.ts +40 -0
- package/src/extensibility/extensions/get-commands-handler.ts +78 -0
- package/src/extensibility/extensions/index.ts +16 -0
- package/src/extensibility/extensions/load-errors.ts +13 -0
- package/src/extensibility/extensions/loader.ts +663 -0
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/model-api.ts +39 -0
- package/src/extensibility/extensions/runner.ts +1396 -0
- package/src/extensibility/extensions/types.ts +1603 -0
- package/src/extensibility/extensions/wrapper.ts +395 -0
- package/src/extensibility/hooks/index.ts +6 -0
- package/src/extensibility/hooks/loader.ts +243 -0
- package/src/extensibility/hooks/runner.ts +425 -0
- package/src/extensibility/hooks/tool-wrapper.ts +124 -0
- package/src/extensibility/hooks/types.ts +612 -0
- package/src/extensibility/legacy-pi-ai-shim.ts +161 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +1460 -0
- package/src/extensibility/legacy-pi-tui-shim.ts +43 -0
- package/src/extensibility/legacy-typebox.ts +101 -0
- package/src/extensibility/plugins/bun-git-cache.ts +91 -0
- package/src/extensibility/plugins/doctor.ts +65 -0
- package/src/extensibility/plugins/git-url.ts +367 -0
- package/src/extensibility/plugins/index.ts +9 -0
- package/src/extensibility/plugins/installer.ts +201 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +2643 -0
- package/src/extensibility/plugins/legacy-pi-virtual-modules.d.ts +4 -0
- package/src/extensibility/plugins/loader.ts +535 -0
- package/src/extensibility/plugins/manager.ts +1142 -0
- package/src/extensibility/plugins/marketplace/cache.ts +136 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +316 -0
- package/src/extensibility/plugins/marketplace/index.ts +6 -0
- package/src/extensibility/plugins/marketplace/manager.ts +927 -0
- package/src/extensibility/plugins/marketplace/registry.ts +192 -0
- package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
- package/src/extensibility/plugins/marketplace/types.ts +192 -0
- package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
- package/src/extensibility/plugins/parser.ts +107 -0
- package/src/extensibility/plugins/runtime-config.ts +9 -0
- package/src/extensibility/plugins/types.ts +194 -0
- package/src/extensibility/session-handler-types.ts +21 -0
- package/src/extensibility/shared-events.ts +401 -0
- package/src/extensibility/skills.ts +531 -0
- package/src/extensibility/slash-commands.ts +131 -0
- package/src/extensibility/tool-event-input.ts +80 -0
- package/src/extensibility/tool-proxy.ts +28 -0
- package/src/extensibility/utils.ts +192 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +521 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +251 -0
- package/src/hindsight/backend.ts +354 -0
- package/src/hindsight/bank.ts +156 -0
- package/src/hindsight/client.ts +680 -0
- package/src/hindsight/config.ts +193 -0
- package/src/hindsight/content.ts +266 -0
- package/src/hindsight/index.ts +8 -0
- package/src/hindsight/mental-models.ts +429 -0
- package/src/hindsight/seeds.json +32 -0
- package/src/hindsight/state.ts +530 -0
- package/src/hindsight/transcript.ts +71 -0
- package/src/index.ts +66 -0
- package/src/internal-urls/agent-protocol.ts +180 -0
- package/src/internal-urls/artifact-protocol.ts +151 -0
- package/src/internal-urls/docs-index.ts +102 -0
- package/src/internal-urls/filesystem-resource.ts +34 -0
- package/src/internal-urls/history-protocol.ts +198 -0
- package/src/internal-urls/index.ts +28 -0
- package/src/internal-urls/issue-pr-protocol.ts +594 -0
- package/src/internal-urls/json-query.ts +126 -0
- package/src/internal-urls/local-protocol.ts +471 -0
- package/src/internal-urls/mcp-protocol.ts +168 -0
- package/src/internal-urls/memory-protocol.ts +380 -0
- package/src/internal-urls/omp-protocol.ts +94 -0
- package/src/internal-urls/parse.ts +103 -0
- package/src/internal-urls/registry-helpers.ts +132 -0
- package/src/internal-urls/router.ts +153 -0
- package/src/internal-urls/rule-protocol.ts +45 -0
- package/src/internal-urls/security-protocol.ts +261 -0
- package/src/internal-urls/skill-protocol.ts +131 -0
- package/src/internal-urls/ssh-protocol.ts +368 -0
- package/src/internal-urls/types.ts +196 -0
- package/src/internal-urls/vault-protocol.ts +940 -0
- package/src/internal-urls/xd-protocol.ts +46 -0
- package/src/irc/bus.ts +380 -0
- package/src/jsonrpc/message-framing.ts +142 -0
- package/src/launch/broker.ts +1368 -0
- package/src/launch/client.ts +521 -0
- package/src/launch/ensure.ts +73 -0
- package/src/launch/paths.ts +14 -0
- package/src/launch/presence.ts +82 -0
- package/src/launch/protocol.ts +448 -0
- package/src/launch/spawn-options.ts +17 -0
- package/src/launch/terminal-output-worker-client.ts +53 -0
- package/src/launch/terminal-output-worker-protocol.ts +11 -0
- package/src/launch/terminal-output-worker.ts +23 -0
- package/src/launch/terminal-output.ts +46 -0
- package/src/lib/xai-http.ts +150 -0
- package/src/live/attestation.ts +91 -0
- package/src/live/controller.ts +517 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +422 -0
- package/src/live/visualizer.ts +221 -0
- package/src/live/voices.ts +18 -0
- package/src/lsp/client.ts +1465 -0
- package/src/lsp/clients/biome-client.ts +263 -0
- package/src/lsp/clients/index.ts +50 -0
- package/src/lsp/clients/lsp-linter-client.ts +85 -0
- package/src/lsp/clients/swiftlint-client.ts +120 -0
- package/src/lsp/config.ts +549 -0
- package/src/lsp/defaults.json +499 -0
- package/src/lsp/deferred-diagnostics.ts +66 -0
- package/src/lsp/diagnostics-ledger.ts +51 -0
- package/src/lsp/edits.ts +288 -0
- package/src/lsp/format-options.ts +119 -0
- package/src/lsp/index.ts +2821 -0
- package/src/lsp/lspmux.ts +233 -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/render.ts +668 -0
- package/src/lsp/startup-events.ts +13 -0
- package/src/lsp/types.ts +479 -0
- package/src/lsp/utils.ts +747 -0
- package/src/main.ts +1799 -0
- package/src/markit/NOTICE +32 -0
- package/src/markit/converters/docx.ts +56 -0
- package/src/markit/converters/epub.ts +136 -0
- package/src/markit/converters/pdf/columns.ts +103 -0
- package/src/markit/converters/pdf/extract.ts +598 -0
- package/src/markit/converters/pdf/grid.ts +780 -0
- package/src/markit/converters/pdf/headers.ts +106 -0
- package/src/markit/converters/pdf/index.ts +146 -0
- package/src/markit/converters/pdf/render.ts +501 -0
- package/src/markit/converters/pdf/types.ts +84 -0
- package/src/markit/converters/pptx.ts +325 -0
- package/src/markit/converters/xlsx.ts +173 -0
- package/src/markit/index.ts +2 -0
- package/src/markit/registry.ts +59 -0
- package/src/markit/types.ts +35 -0
- package/src/mcp/client.ts +511 -0
- package/src/mcp/config-writer.ts +377 -0
- package/src/mcp/config.ts +385 -0
- package/src/mcp/index.ts +29 -0
- package/src/mcp/json-rpc.ts +122 -0
- package/src/mcp/loader.ts +125 -0
- package/src/mcp/manager.ts +1524 -0
- package/src/mcp/oauth-credentials.ts +104 -0
- package/src/mcp/oauth-discovery.ts +587 -0
- package/src/mcp/oauth-flow.ts +830 -0
- package/src/mcp/render.ts +214 -0
- package/src/mcp/request-id.ts +24 -0
- package/src/mcp/smithery-auth.ts +108 -0
- package/src/mcp/smithery-connect.ts +154 -0
- package/src/mcp/smithery-registry.ts +500 -0
- package/src/mcp/startup-events.ts +116 -0
- package/src/mcp/timeout.ts +59 -0
- package/src/mcp/tool-bridge.ts +691 -0
- package/src/mcp/tool-cache.ts +117 -0
- package/src/mcp/transports/header-policy.ts +95 -0
- package/src/mcp/transports/http.ts +508 -0
- package/src/mcp/transports/index.ts +7 -0
- package/src/mcp/transports/sse.ts +372 -0
- package/src/mcp/transports/stdio.ts +905 -0
- package/src/mcp/types.ts +467 -0
- package/src/memories/index.ts +1434 -0
- package/src/memories/storage.ts +578 -0
- package/src/memory-backend/index.ts +19 -0
- package/src/memory-backend/local-backend.ts +47 -0
- package/src/memory-backend/messages.ts +19 -0
- package/src/memory-backend/off-backend.ts +25 -0
- package/src/memory-backend/resolve.ts +25 -0
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/memory-backend/types.ts +166 -0
- package/src/mnemopi/backend.ts +629 -0
- package/src/mnemopi/config.ts +267 -0
- package/src/mnemopi/embed-client.ts +293 -0
- package/src/mnemopi/embed-protocol.ts +35 -0
- package/src/mnemopi/embed-worker.ts +114 -0
- package/src/mnemopi/index.ts +3 -0
- package/src/mnemopi/state.ts +940 -0
- package/src/modes/acp/acp-agent.ts +2595 -0
- package/src/modes/acp/acp-client-bridge.ts +154 -0
- package/src/modes/acp/acp-event-mapper.ts +1084 -0
- package/src/modes/acp/acp-mode.ts +48 -0
- package/src/modes/acp/index.ts +2 -0
- package/src/modes/acp/terminal-auth.ts +37 -0
- package/src/modes/components/advisor-config.ts +635 -0
- package/src/modes/components/advisor-message.ts +109 -0
- package/src/modes/components/agent-dashboard.ts +1252 -0
- package/src/modes/components/agent-hub-projection.ts +248 -0
- package/src/modes/components/agent-hub-renderer.ts +194 -0
- package/src/modes/components/agent-hub.ts +1126 -0
- package/src/modes/components/agent-transcript-viewer.ts +647 -0
- package/src/modes/components/ask-dialog.ts +1017 -0
- package/src/modes/components/assistant-message.ts +978 -0
- package/src/modes/components/background-tan-message.ts +36 -0
- package/src/modes/components/bash-execution.ts +233 -0
- package/src/modes/components/bordered-loader.ts +41 -0
- package/src/modes/components/btw-panel.ts +161 -0
- package/src/modes/components/cache-invalidation-marker.ts +110 -0
- package/src/modes/components/chat-block.ts +111 -0
- package/src/modes/components/chat-transcript-builder.ts +518 -0
- package/src/modes/components/codex-reset-fireworks.ts +369 -0
- package/src/modes/components/collab-prompt-message.ts +32 -0
- package/src/modes/components/compaction-summary-message.ts +221 -0
- package/src/modes/components/copy-selector.ts +218 -0
- package/src/modes/components/countdown-timer.ts +75 -0
- package/src/modes/components/custom-editor.ts +1036 -0
- package/src/modes/components/custom-message.ts +70 -0
- package/src/modes/components/diff.ts +254 -0
- package/src/modes/components/dynamic-border.ts +37 -0
- package/src/modes/components/error-banner.ts +33 -0
- package/src/modes/components/eval-execution.ts +169 -0
- package/src/modes/components/execution-shared.ts +101 -0
- package/src/modes/components/extensions/extension-dashboard.ts +492 -0
- package/src/modes/components/extensions/extension-list.ts +507 -0
- package/src/modes/components/extensions/index.ts +9 -0
- package/src/modes/components/extensions/inspector-panel.ts +326 -0
- package/src/modes/components/extensions/state-manager.ts +648 -0
- package/src/modes/components/extensions/types.ts +186 -0
- package/src/modes/components/footer.ts +274 -0
- package/src/modes/components/history-search.ts +268 -0
- package/src/modes/components/hook-editor.ts +213 -0
- package/src/modes/components/hook-input.ts +87 -0
- package/src/modes/components/hook-message.ts +67 -0
- package/src/modes/components/hook-selector.ts +691 -0
- package/src/modes/components/index.ts +42 -0
- package/src/modes/components/keybinding-hints.ts +56 -0
- package/src/modes/components/late-diagnostics-message.ts +60 -0
- package/src/modes/components/login-dialog.ts +197 -0
- package/src/modes/components/logout-account-selector.ts +130 -0
- package/src/modes/components/mcp-add-wizard.ts +1413 -0
- package/src/modes/components/message-frame.ts +98 -0
- package/src/modes/components/model-browser.ts +888 -0
- package/src/modes/components/model-hub.ts +2014 -0
- package/src/modes/components/model-picker.ts +237 -0
- package/src/modes/components/move-overlay.ts +293 -0
- package/src/modes/components/oauth-selector.ts +474 -0
- package/src/modes/components/omfg-panel.ts +141 -0
- package/src/modes/components/overlay-box.ts +109 -0
- package/src/modes/components/pause-screen.ts +208 -0
- package/src/modes/components/plan-review-overlay.ts +1226 -0
- package/src/modes/components/plan-toc.ts +138 -0
- package/src/modes/components/plugin-selector.ts +100 -0
- package/src/modes/components/plugin-settings.ts +745 -0
- package/src/modes/components/queue-mode-selector.ts +61 -0
- package/src/modes/components/read-tool-group.ts +856 -0
- package/src/modes/components/reset-usage-selector.ts +161 -0
- package/src/modes/components/segment-track.ts +89 -0
- package/src/modes/components/select-list-mouse-routing.ts +35 -0
- package/src/modes/components/selector-helpers.ts +129 -0
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/session-selector.ts +1035 -0
- package/src/modes/components/settings-defs.ts +267 -0
- package/src/modes/components/settings-selector.ts +1445 -0
- package/src/modes/components/show-images-selector.ts +50 -0
- package/src/modes/components/skill-message.ts +110 -0
- package/src/modes/components/snapcompact-shape-preview-doc.md +14 -0
- package/src/modes/components/snapcompact-shape-preview.ts +192 -0
- package/src/modes/components/status-line/component.ts +1843 -0
- package/src/modes/components/status-line/context-thresholds.ts +86 -0
- package/src/modes/components/status-line/git-utils.ts +42 -0
- package/src/modes/components/status-line/index.ts +5 -0
- package/src/modes/components/status-line/presets.ts +106 -0
- package/src/modes/components/status-line/segments.ts +710 -0
- package/src/modes/components/status-line/separators.ts +55 -0
- package/src/modes/components/status-line/types.ts +159 -0
- package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
- package/src/modes/components/theme-selector.ts +68 -0
- package/src/modes/components/thinking-selector.ts +57 -0
- package/src/modes/components/tiny-title-download-progress.ts +90 -0
- package/src/modes/components/tips.txt +26 -0
- package/src/modes/components/todo-reminder.ts +43 -0
- package/src/modes/components/tool-execution.ts +1467 -0
- package/src/modes/components/transcript-container.ts +527 -0
- package/src/modes/components/tree-selector.ts +1006 -0
- package/src/modes/components/ttsr-notification.ts +123 -0
- package/src/modes/components/usage-row.ts +52 -0
- package/src/modes/components/user-message-selector.ts +227 -0
- package/src/modes/components/user-message.ts +167 -0
- package/src/modes/components/visual-truncate.ts +63 -0
- package/src/modes/components/welcome.ts +578 -0
- package/src/modes/controllers/btw-controller.ts +246 -0
- package/src/modes/controllers/command-controller-shared.ts +109 -0
- package/src/modes/controllers/command-controller.ts +2024 -0
- package/src/modes/controllers/event-controller.ts +2173 -0
- package/src/modes/controllers/extension-ui-controller.ts +1243 -0
- package/src/modes/controllers/input-controller.ts +2037 -0
- package/src/modes/controllers/live-command-controller.ts +261 -0
- package/src/modes/controllers/mcp-command-controller.ts +2552 -0
- package/src/modes/controllers/omfg-controller.ts +287 -0
- package/src/modes/controllers/omfg-rule.ts +647 -0
- package/src/modes/controllers/selector-controller.ts +2058 -0
- package/src/modes/controllers/session-focus-controller.ts +117 -0
- package/src/modes/controllers/ssh-command-controller.ts +385 -0
- package/src/modes/controllers/streaming-reveal.ts +399 -0
- package/src/modes/controllers/tan-command-controller.ts +243 -0
- package/src/modes/controllers/todo-command-controller.ts +487 -0
- package/src/modes/controllers/tool-args-reveal.ts +591 -0
- package/src/modes/data/emojis.json +1 -0
- package/src/modes/emoji-autocomplete.ts +285 -0
- package/src/modes/github-ref-autocomplete.ts +75 -0
- package/src/modes/gradient-highlight.ts +99 -0
- package/src/modes/image-references.ts +137 -0
- package/src/modes/index.ts +10 -0
- package/src/modes/interactive-mode.ts +5055 -0
- package/src/modes/internal-url-autocomplete.ts +158 -0
- package/src/modes/loop-limit.ts +192 -0
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/magic-keywords.ts +42 -0
- package/src/modes/markdown-prose.ts +247 -0
- package/src/modes/oauth-manual-input.ts +69 -0
- package/src/modes/orchestrate.ts +43 -0
- package/src/modes/print-mode.ts +282 -0
- package/src/modes/prompt-action-autocomplete.ts +322 -0
- package/src/modes/queue-input.ts +132 -0
- package/src/modes/rpc/host-tools.ts +204 -0
- package/src/modes/rpc/host-uris.ts +241 -0
- package/src/modes/rpc/rpc-client.ts +1213 -0
- package/src/modes/rpc/rpc-frame.ts +316 -0
- package/src/modes/rpc/rpc-input.ts +38 -0
- package/src/modes/rpc/rpc-messages.ts +127 -0
- package/src/modes/rpc/rpc-mode.ts +1519 -0
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +544 -0
- package/src/modes/running-subagent-badge.ts +13 -0
- package/src/modes/runtime-init.ts +144 -0
- package/src/modes/session-observer-registry.ts +223 -0
- package/src/modes/session-teardown.ts +82 -0
- package/src/modes/setup-version.ts +11 -0
- package/src/modes/setup-wizard/index.ts +103 -0
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +103 -0
- package/src/modes/setup-wizard/scenes/model.ts +132 -0
- package/src/modes/setup-wizard/scenes/outro.ts +35 -0
- package/src/modes/setup-wizard/scenes/providers.ts +105 -0
- package/src/modes/setup-wizard/scenes/sign-in.ts +312 -0
- package/src/modes/setup-wizard/scenes/splash.ts +201 -0
- package/src/modes/setup-wizard/scenes/theme.ts +330 -0
- package/src/modes/setup-wizard/scenes/types.ts +65 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +153 -0
- package/src/modes/setup-wizard/startup-splash.ts +107 -0
- package/src/modes/setup-wizard/wizard-overlay.ts +335 -0
- package/src/modes/shared.ts +49 -0
- package/src/modes/skill-command.ts +91 -0
- package/src/modes/theme/dark.json +95 -0
- package/src/modes/theme/defaults/alabaster.json +93 -0
- package/src/modes/theme/defaults/amethyst.json +96 -0
- package/src/modes/theme/defaults/anthracite.json +93 -0
- package/src/modes/theme/defaults/basalt.json +91 -0
- package/src/modes/theme/defaults/birch.json +95 -0
- package/src/modes/theme/defaults/dark-abyss.json +91 -0
- package/src/modes/theme/defaults/dark-arctic.json +104 -0
- package/src/modes/theme/defaults/dark-aurora.json +95 -0
- package/src/modes/theme/defaults/dark-catppuccin.json +107 -0
- package/src/modes/theme/defaults/dark-cavern.json +91 -0
- package/src/modes/theme/defaults/dark-copper.json +95 -0
- package/src/modes/theme/defaults/dark-cosmos.json +90 -0
- package/src/modes/theme/defaults/dark-cyberpunk.json +102 -0
- package/src/modes/theme/defaults/dark-dracula.json +98 -0
- package/src/modes/theme/defaults/dark-eclipse.json +91 -0
- package/src/modes/theme/defaults/dark-ember.json +95 -0
- package/src/modes/theme/defaults/dark-equinox.json +90 -0
- package/src/modes/theme/defaults/dark-forest.json +96 -0
- package/src/modes/theme/defaults/dark-github.json +105 -0
- package/src/modes/theme/defaults/dark-gruvbox.json +112 -0
- package/src/modes/theme/defaults/dark-lavender.json +95 -0
- package/src/modes/theme/defaults/dark-lunar.json +89 -0
- package/src/modes/theme/defaults/dark-midnight.json +95 -0
- package/src/modes/theme/defaults/dark-monochrome.json +94 -0
- package/src/modes/theme/defaults/dark-monokai.json +98 -0
- package/src/modes/theme/defaults/dark-nebula.json +90 -0
- package/src/modes/theme/defaults/dark-nord.json +97 -0
- package/src/modes/theme/defaults/dark-ocean.json +101 -0
- package/src/modes/theme/defaults/dark-one.json +100 -0
- package/src/modes/theme/defaults/dark-poimandres.json +143 -0
- package/src/modes/theme/defaults/dark-rainforest.json +91 -0
- package/src/modes/theme/defaults/dark-reef.json +91 -0
- package/src/modes/theme/defaults/dark-retro.json +92 -0
- package/src/modes/theme/defaults/dark-rose-pine.json +96 -0
- package/src/modes/theme/defaults/dark-sakura.json +95 -0
- package/src/modes/theme/defaults/dark-slate.json +95 -0
- package/src/modes/theme/defaults/dark-solarized.json +97 -0
- package/src/modes/theme/defaults/dark-solstice.json +90 -0
- package/src/modes/theme/defaults/dark-starfall.json +91 -0
- package/src/modes/theme/defaults/dark-sunset.json +99 -0
- package/src/modes/theme/defaults/dark-swamp.json +90 -0
- package/src/modes/theme/defaults/dark-synthwave.json +103 -0
- package/src/modes/theme/defaults/dark-taiga.json +91 -0
- package/src/modes/theme/defaults/dark-terminal.json +95 -0
- package/src/modes/theme/defaults/dark-tokyo-night.json +101 -0
- package/src/modes/theme/defaults/dark-tundra.json +91 -0
- package/src/modes/theme/defaults/dark-twilight.json +91 -0
- package/src/modes/theme/defaults/dark-volcanic.json +91 -0
- package/src/modes/theme/defaults/graphite.json +92 -0
- package/src/modes/theme/defaults/index.ts +199 -0
- package/src/modes/theme/defaults/light-arctic.json +107 -0
- package/src/modes/theme/defaults/light-aurora-day.json +91 -0
- package/src/modes/theme/defaults/light-canyon.json +91 -0
- package/src/modes/theme/defaults/light-catppuccin.json +106 -0
- package/src/modes/theme/defaults/light-cirrus.json +90 -0
- package/src/modes/theme/defaults/light-coral.json +95 -0
- package/src/modes/theme/defaults/light-cyberpunk.json +96 -0
- package/src/modes/theme/defaults/light-dawn.json +90 -0
- package/src/modes/theme/defaults/light-dunes.json +91 -0
- package/src/modes/theme/defaults/light-eucalyptus.json +95 -0
- package/src/modes/theme/defaults/light-forest.json +100 -0
- package/src/modes/theme/defaults/light-frost.json +95 -0
- package/src/modes/theme/defaults/light-github.json +115 -0
- package/src/modes/theme/defaults/light-glacier.json +91 -0
- package/src/modes/theme/defaults/light-gruvbox.json +108 -0
- package/src/modes/theme/defaults/light-haze.json +90 -0
- package/src/modes/theme/defaults/light-honeycomb.json +95 -0
- package/src/modes/theme/defaults/light-lagoon.json +91 -0
- package/src/modes/theme/defaults/light-lavender.json +95 -0
- package/src/modes/theme/defaults/light-meadow.json +91 -0
- package/src/modes/theme/defaults/light-mint.json +95 -0
- package/src/modes/theme/defaults/light-monochrome.json +101 -0
- package/src/modes/theme/defaults/light-ocean.json +99 -0
- package/src/modes/theme/defaults/light-one.json +99 -0
- package/src/modes/theme/defaults/light-opal.json +91 -0
- package/src/modes/theme/defaults/light-orchard.json +91 -0
- package/src/modes/theme/defaults/light-paper.json +95 -0
- package/src/modes/theme/defaults/light-poimandres.json +143 -0
- package/src/modes/theme/defaults/light-prism.json +90 -0
- package/src/modes/theme/defaults/light-retro.json +98 -0
- package/src/modes/theme/defaults/light-sand.json +95 -0
- package/src/modes/theme/defaults/light-savanna.json +91 -0
- package/src/modes/theme/defaults/light-solarized.json +102 -0
- package/src/modes/theme/defaults/light-soleil.json +90 -0
- package/src/modes/theme/defaults/light-sunset.json +99 -0
- package/src/modes/theme/defaults/light-synthwave.json +98 -0
- package/src/modes/theme/defaults/light-tokyo-night.json +111 -0
- package/src/modes/theme/defaults/light-wetland.json +91 -0
- package/src/modes/theme/defaults/light-zenith.json +89 -0
- package/src/modes/theme/defaults/limestone.json +94 -0
- package/src/modes/theme/defaults/mahogany.json +97 -0
- package/src/modes/theme/defaults/marble.json +93 -0
- package/src/modes/theme/defaults/obsidian.json +91 -0
- package/src/modes/theme/defaults/onyx.json +91 -0
- package/src/modes/theme/defaults/pearl.json +93 -0
- package/src/modes/theme/defaults/porcelain.json +91 -0
- package/src/modes/theme/defaults/quartz.json +96 -0
- package/src/modes/theme/defaults/sandstone.json +95 -0
- package/src/modes/theme/defaults/titanium.json +90 -0
- package/src/modes/theme/light.json +93 -0
- package/src/modes/theme/mermaid-cache.ts +92 -0
- package/src/modes/theme/shimmer.ts +305 -0
- package/src/modes/theme/theme-schema.json +463 -0
- package/src/modes/theme/theme.ts +3171 -0
- package/src/modes/turn-budget.ts +31 -0
- package/src/modes/types.ts +494 -0
- package/src/modes/ultrathink.ts +42 -0
- package/src/modes/utils/context-usage.ts +518 -0
- package/src/modes/utils/copy-targets.ts +378 -0
- package/src/modes/utils/hotkeys-markdown.ts +65 -0
- package/src/modes/utils/interactive-context-helpers.ts +31 -0
- package/src/modes/utils/keybinding-matchers.ts +86 -0
- package/src/modes/utils/tools-markdown.ts +31 -0
- package/src/modes/utils/transcript-render-helpers.ts +253 -0
- package/src/modes/utils/ui-helpers.ts +976 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/modes/workflow.ts +55 -0
- package/src/plan-mode/approved-plan.ts +194 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/plan-mode/plan-handoff.ts +37 -0
- package/src/plan-mode/plan-protection.ts +31 -0
- package/src/plan-mode/state.ts +6 -0
- package/src/priority.json +60 -0
- package/src/prompts/advisor/active-repo-watchdog.md +6 -0
- package/src/prompts/advisor/advise-tool.md +3 -0
- package/src/prompts/advisor/context-files.md +8 -0
- package/src/prompts/advisor/system.md +98 -0
- package/src/prompts/agents/designer.md +74 -0
- package/src/prompts/agents/frontmatter.md +12 -0
- package/src/prompts/agents/init.md +33 -0
- package/src/prompts/agents/librarian.md +119 -0
- package/src/prompts/agents/reviewer.md +139 -0
- package/src/prompts/agents/scout.md +58 -0
- package/src/prompts/agents/security-reviewer.md +75 -0
- package/src/prompts/agents/task.md +17 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/bench.md +6 -0
- package/src/prompts/ci-green-request.md +36 -0
- package/src/prompts/dry-balance-bench.md +8 -0
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/goals/goal-mode-context.md +4 -0
- package/src/prompts/goals/goal-todo-context.md +12 -0
- package/src/prompts/goals/guided-goal-interview.md +43 -0
- package/src/prompts/memories/consolidation.md +30 -0
- package/src/prompts/memories/consolidation_system.md +4 -0
- package/src/prompts/memories/read-path.md +17 -0
- package/src/prompts/memories/stage_one_input.md +6 -0
- package/src/prompts/memories/stage_one_system.md +21 -0
- package/src/prompts/review-custom-request.md +21 -0
- package/src/prompts/review-headless-request.md +16 -0
- package/src/prompts/review-request.md +68 -0
- package/src/prompts/security/scan-coordinator.md +7 -0
- package/src/prompts/security/scan-request.md +21 -0
- package/src/prompts/security/validate-request.md +8 -0
- package/src/prompts/session/launch-completion.md +1 -0
- package/src/prompts/skills/autoload.md +8 -0
- package/src/prompts/skills/user-invocation.md +11 -0
- package/src/prompts/steering/parent-irc.md +5 -0
- package/src/prompts/steering/user-interjection.md +6 -0
- package/src/prompts/system/active-repo-context.md +4 -0
- package/src/prompts/system/agent-creation-architect.md +50 -0
- package/src/prompts/system/agent-creation-user.md +6 -0
- package/src/prompts/system/auto-continue.md +1 -0
- package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
- package/src/prompts/system/auto-thinking-difficulty.md +14 -0
- package/src/prompts/system/autolearn-guidance-learn.md +1 -0
- package/src/prompts/system/autolearn-guidance.md +7 -0
- package/src/prompts/system/autolearn-nudge-autocontinue.md +5 -0
- package/src/prompts/system/background-tan-dispatch.md +8 -0
- package/src/prompts/system/btw-user.md +8 -0
- package/src/prompts/system/commit-message-system.md +14 -0
- package/src/prompts/system/computer-safety.md +14 -0
- package/src/prompts/system/custom-system-prompt.md +64 -0
- package/src/prompts/system/eager-task.md +7 -0
- package/src/prompts/system/eager-todo.md +18 -0
- package/src/prompts/system/empty-stop-retry.md +4 -0
- package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
- package/src/prompts/system/interrupted-thinking.md +7 -0
- package/src/prompts/system/irc-autoreply.md +6 -0
- package/src/prompts/system/irc-incoming.md +9 -0
- package/src/prompts/system/manual-continue.md +7 -0
- package/src/prompts/system/mcp-xdev-guidance.md +11 -0
- package/src/prompts/system/memory-consolidation-system.md +8 -0
- package/src/prompts/system/memory-extraction-system.md +26 -0
- package/src/prompts/system/mid-run-todo-nudge.md +3 -0
- package/src/prompts/system/omfg-user.md +50 -0
- package/src/prompts/system/orchestrate-notice.md +40 -0
- package/src/prompts/system/personalities/default.md +18 -0
- package/src/prompts/system/personalities/friendly.md +17 -0
- package/src/prompts/system/personalities/pragmatic.md +15 -0
- package/src/prompts/system/plan-mode-active.md +125 -0
- package/src/prompts/system/plan-mode-approved.md +22 -0
- package/src/prompts/system/plan-mode-compact-instructions.md +17 -0
- package/src/prompts/system/plan-mode-reference.md +10 -0
- package/src/prompts/system/plan-mode-subagent.md +33 -0
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
- package/src/prompts/system/plan-yolo-handoff.md +5 -0
- package/src/prompts/system/prewalk-checklist.md +7 -0
- package/src/prompts/system/prewalk-continue.md +1 -0
- package/src/prompts/system/prewalk-plan.md +13 -0
- package/src/prompts/system/project-prompt.md +61 -0
- package/src/prompts/system/recap-user.md +9 -0
- package/src/prompts/system/resolve-device-reminder.md +3 -0
- package/src/prompts/system/rewind-report.md +6 -0
- package/src/prompts/system/side-channel-no-tools.md +3 -0
- package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-context-stub.md +1 -0
- package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-system-stub.md +1 -0
- package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
- package/src/prompts/system/speech-rewrite.md +15 -0
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/subagent-system-prompt.md +73 -0
- package/src/prompts/system/subagent-user-prompt.md +3 -0
- package/src/prompts/system/subagent-yield-reminder.md +23 -0
- package/src/prompts/system/system-prompt.md +263 -0
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/system/task-label.md +23 -0
- package/src/prompts/system/thinking-loop-redirect.md +10 -0
- package/src/prompts/system/title-marker-instruction.md +1 -0
- package/src/prompts/system/title-system.md +16 -0
- package/src/prompts/system/tool-call-loop-redirect.md +8 -0
- package/src/prompts/system/ttsr-interrupt.md +7 -0
- package/src/prompts/system/ttsr-tool-reminder.md +5 -0
- package/src/prompts/system/ultrathink-notice.md +3 -0
- package/src/prompts/system/unexpected-stop-classifier.md +17 -0
- package/src/prompts/system/unexpected-stop-retry.md +4 -0
- package/src/prompts/system/vibe-mode-active.md +26 -0
- package/src/prompts/system/web-search.md +25 -0
- package/src/prompts/system/workflow-notice.md +112 -0
- package/src/prompts/system/xdev-mount-notice.md +20 -0
- package/src/prompts/tools/apply-patch.md +65 -0
- package/src/prompts/tools/ask.md +22 -0
- package/src/prompts/tools/ast-edit.md +11 -0
- package/src/prompts/tools/ast-grep.md +19 -0
- package/src/prompts/tools/async-result.md +8 -0
- package/src/prompts/tools/bash.md +23 -0
- package/src/prompts/tools/browser.md +29 -0
- package/src/prompts/tools/checkpoint.md +15 -0
- package/src/prompts/tools/computer.md +26 -0
- package/src/prompts/tools/debug.md +3 -0
- package/src/prompts/tools/eval.md +45 -0
- package/src/prompts/tools/github.md +22 -0
- package/src/prompts/tools/glob.md +16 -0
- package/src/prompts/tools/goal.md +11 -0
- package/src/prompts/tools/grep.md +13 -0
- package/src/prompts/tools/hub.md +34 -0
- package/src/prompts/tools/image-attachment-describe-system.md +8 -0
- package/src/prompts/tools/image-attachment-describe.md +10 -0
- package/src/prompts/tools/image-gen.md +7 -0
- package/src/prompts/tools/inspect-image-system.md +20 -0
- package/src/prompts/tools/inspect-image.md +22 -0
- package/src/prompts/tools/learn.md +7 -0
- package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
- package/src/prompts/tools/lsp.md +19 -0
- package/src/prompts/tools/manage-skill.md +9 -0
- package/src/prompts/tools/memory-edit.md +12 -0
- package/src/prompts/tools/patch.md +57 -0
- package/src/prompts/tools/read.md +27 -0
- package/src/prompts/tools/recall.md +7 -0
- package/src/prompts/tools/reflect.md +5 -0
- package/src/prompts/tools/replace.md +30 -0
- package/src/prompts/tools/retain.md +6 -0
- package/src/prompts/tools/rewind.md +14 -0
- package/src/prompts/tools/security-publish.md +1 -0
- package/src/prompts/tools/security-scan.md +1 -0
- package/src/prompts/tools/task-async-contract.md +1 -0
- package/src/prompts/tools/task-summary.md +20 -0
- package/src/prompts/tools/task.md +83 -0
- package/src/prompts/tools/todo.md +42 -0
- package/src/prompts/tools/vibe-kill.md +3 -0
- package/src/prompts/tools/vibe-list.md +3 -0
- package/src/prompts/tools/vibe-send.md +9 -0
- package/src/prompts/tools/vibe-spawn.md +10 -0
- package/src/prompts/tools/vibe-turn-result.md +19 -0
- package/src/prompts/tools/vibe-wait.md +8 -0
- package/src/prompts/tools/web-search.md +8 -0
- package/src/prompts/tools/write.md +14 -0
- package/src/registry/agent-lifecycle.ts +505 -0
- package/src/registry/agent-registry.ts +287 -0
- package/src/registry/persisted-agents.ts +423 -0
- package/src/sdk.ts +3820 -0
- package/src/secrets/index.ts +375 -0
- package/src/secrets/obfuscator.ts +2629 -0
- package/src/secrets/regex.ts +21 -0
- package/src/security/auth.ts +98 -0
- package/src/security/cloud.ts +686 -0
- package/src/security/comparison.ts +255 -0
- package/src/security/contracts/ids.ts +111 -0
- package/src/security/contracts/index.ts +4 -0
- package/src/security/contracts/schemas.ts +219 -0
- package/src/security/contracts/types.ts +254 -0
- package/src/security/contracts/validation.ts +69 -0
- package/src/security/coordinator.ts +708 -0
- package/src/security/importers/codex-security.ts +387 -0
- package/src/security/importers/index.ts +2 -0
- package/src/security/importers/sarif.ts +357 -0
- package/src/security/index.ts +13 -0
- package/src/security/preflight.ts +405 -0
- package/src/security/provenance.ts +106 -0
- package/src/security/publication.ts +326 -0
- package/src/security/remediation.ts +93 -0
- package/src/security/resource-output.ts +50 -0
- package/src/security/sarif.ts +78 -0
- package/src/security/store.ts +430 -0
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-events.ts +67 -0
- package/src/session/agent-session-types.ts +407 -0
- package/src/session/agent-session.ts +9000 -0
- package/src/session/agent-storage.ts +807 -0
- package/src/session/artifacts.ts +154 -0
- package/src/session/async-job-delivery.ts +82 -0
- package/src/session/auth-broker-config.ts +92 -0
- package/src/session/auth-storage.ts +25 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.ts +295 -0
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/claude-session-store.ts +426 -0
- package/src/session/client-bridge.ts +85 -0
- package/src/session/codex-auto-reset.ts +673 -0
- package/src/session/codex-session-store.ts +673 -0
- package/src/session/compact-modes.ts +105 -0
- package/src/session/credential-pin.ts +93 -0
- package/src/session/eval-runner.ts +219 -0
- package/src/session/exit-diagnostics.ts +310 -0
- package/src/session/foreign-session-import.ts +52 -0
- package/src/session/foreign-session-jsonl.ts +29 -0
- package/src/session/foreign-session-store.ts +26 -0
- package/src/session/history-storage.ts +329 -0
- package/src/session/indexed-session-storage.ts +553 -0
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/launch-completion.ts +37 -0
- package/src/session/messages.ts +1304 -0
- package/src/session/model-controls.ts +757 -0
- package/src/session/prewalk.ts +279 -0
- package/src/session/provider-image-budget.ts +86 -0
- package/src/session/queued-messages.ts +99 -0
- package/src/session/redis-session-storage.ts +257 -0
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1896 -0
- package/src/session/session-context.ts +584 -0
- package/src/session/session-dump-format.ts +216 -0
- package/src/session/session-entries.ts +306 -0
- package/src/session/session-handoff.ts +325 -0
- package/src/session/session-history-format.ts +459 -0
- package/src/session/session-listing.ts +715 -0
- package/src/session/session-loader.ts +362 -0
- package/src/session/session-maintenance.ts +3115 -0
- package/src/session/session-manager.ts +2747 -0
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-metadata.ts +53 -0
- package/src/session/session-migrations.ts +78 -0
- package/src/session/session-paths.ts +280 -0
- package/src/session/session-persistence.ts +293 -0
- package/src/session/session-provider-boundary.ts +306 -0
- package/src/session/session-stats.ts +349 -0
- package/src/session/session-storage.ts +774 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/session-tools.ts +1385 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/settings-stream-fn.ts +79 -0
- package/src/session/shake-types.ts +43 -0
- package/src/session/snapcompact-inline.ts +545 -0
- package/src/session/snapcompact-savings-journal.ts +113 -0
- package/src/session/sql-session-storage.ts +374 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/streaming-output.ts +1459 -0
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/tool-choice-queue.ts +305 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-persistence.ts +142 -0
- package/src/session/turn-recovery.ts +2005 -0
- package/src/session/unexpected-stop-classifier.ts +141 -0
- package/src/session/yield-queue.ts +288 -0
- package/src/slash-commands/acp-builtins.ts +70 -0
- package/src/slash-commands/available-commands.ts +105 -0
- package/src/slash-commands/builtin-registry.ts +3149 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +80 -0
- package/src/slash-commands/helpers/collab-qrcode.ts +28 -0
- package/src/slash-commands/helpers/context-report.ts +66 -0
- package/src/slash-commands/helpers/format.ts +46 -0
- package/src/slash-commands/helpers/logout.ts +108 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +533 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/reset-usage.ts +68 -0
- package/src/slash-commands/helpers/security.ts +451 -0
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/slash-commands/helpers/ssh.ts +196 -0
- package/src/slash-commands/helpers/stats-dashboard.ts +86 -0
- package/src/slash-commands/helpers/todo.ts +285 -0
- package/src/slash-commands/helpers/usage-report.ts +198 -0
- package/src/slash-commands/marketplace-install-parser.ts +99 -0
- package/src/slash-commands/types.ts +139 -0
- package/src/ssh/config-writer.ts +183 -0
- package/src/ssh/connection-manager.ts +667 -0
- package/src/ssh/file-transfer.ts +214 -0
- package/src/ssh/sshfs-mount.ts +163 -0
- package/src/ssh/utils.ts +51 -0
- package/src/startup-splash.ts +19 -0
- package/src/stt/asr-client.ts +401 -0
- package/src/stt/asr-protocol.ts +65 -0
- package/src/stt/asr-worker.ts +603 -0
- package/src/stt/downloader.ts +142 -0
- package/src/stt/endpointer.ts +259 -0
- package/src/stt/index.ts +6 -0
- package/src/stt/models.ts +150 -0
- package/src/stt/sherpa-runtime.ts +71 -0
- package/src/stt/stt-controller.ts +320 -0
- package/src/stt/submit-trigger.ts +74 -0
- package/src/subprocess/worker-client.ts +463 -0
- package/src/subprocess/worker-runtime.ts +494 -0
- package/src/system-prompt.ts +921 -0
- package/src/task/agents.ts +170 -0
- package/src/task/commands.ts +132 -0
- package/src/task/discovery.ts +145 -0
- package/src/task/executor.ts +3431 -0
- package/src/task/index.ts +1515 -0
- package/src/task/isolation-ownership.ts +106 -0
- package/src/task/isolation-runner.ts +444 -0
- package/src/task/label.ts +40 -0
- package/src/task/name-generator.ts +1577 -0
- package/src/task/omp-command.ts +26 -0
- package/src/task/output-manager.ts +115 -0
- package/src/task/parallel.ts +221 -0
- package/src/task/persisted-revive.ts +173 -0
- package/src/task/prewalk.ts +6 -0
- package/src/task/prompt-policy.ts +8 -0
- package/src/task/provider-concurrency.ts +100 -0
- package/src/task/read-only-policy.ts +27 -0
- package/src/task/render.ts +1830 -0
- package/src/task/renderer.ts +14 -0
- package/src/task/repair-args.ts +118 -0
- package/src/task/spawn-policy.ts +72 -0
- package/src/task/structured-subagent.ts +676 -0
- package/src/task/subprocess-tool-registry.ts +88 -0
- package/src/task/types.ts +555 -0
- package/src/task/worktree.ts +967 -0
- package/src/task/yield-assembly.ts +198 -0
- package/src/telemetry-export.ts +504 -0
- package/src/thinking.ts +377 -0
- package/src/tiny/device.ts +111 -0
- package/src/tiny/dtype.ts +101 -0
- package/src/tiny/message-preproc.ts +155 -0
- package/src/tiny/models.ts +268 -0
- package/src/tiny/text.ts +290 -0
- package/src/tiny/title-client.ts +460 -0
- package/src/tiny/title-protocol.ts +56 -0
- package/src/tiny/worker.ts +354 -0
- package/src/tools/acp-bridge.ts +125 -0
- package/src/tools/approval.ts +245 -0
- package/src/tools/ask.ts +1459 -0
- package/src/tools/ast-edit.ts +718 -0
- package/src/tools/ast-grep.ts +526 -0
- package/src/tools/auto-generated-guard.ts +335 -0
- package/src/tools/bash-interactive.ts +435 -0
- package/src/tools/bash-interceptor.ts +148 -0
- package/src/tools/bash-pty-selection.ts +14 -0
- package/src/tools/bash-skill-urls.ts +350 -0
- package/src/tools/bash.ts +1826 -0
- package/src/tools/browser/aria/aria-snapshot.bundle.txt +7 -0
- package/src/tools/browser/aria/aria-snapshot.ts +131 -0
- package/src/tools/browser/attach.ts +219 -0
- package/src/tools/browser/cmux/cmux-tab.ts +1531 -0
- package/src/tools/browser/cmux/rpc.ts +200 -0
- package/src/tools/browser/cmux/socket-client.ts +445 -0
- package/src/tools/browser/launch.ts +939 -0
- package/src/tools/browser/readable.ts +112 -0
- package/src/tools/browser/registry.ts +447 -0
- 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 +229 -0
- package/src/tools/browser/run-output.ts +76 -0
- package/src/tools/browser/shared-daemon.ts +139 -0
- package/src/tools/browser/tab-protocol.ts +123 -0
- package/src/tools/browser/tab-supervisor.ts +1077 -0
- package/src/tools/browser/tab-worker-entry.ts +29 -0
- package/src/tools/browser/tab-worker.ts +1982 -0
- package/src/tools/browser.ts +487 -0
- package/src/tools/builtin-names.ts +67 -0
- package/src/tools/checkpoint.ts +137 -0
- package/src/tools/computer/exposure.ts +14 -0
- package/src/tools/computer/protocol.ts +69 -0
- package/src/tools/computer/supervisor.ts +409 -0
- package/src/tools/computer/worker-entry.ts +39 -0
- package/src/tools/computer/worker.ts +745 -0
- package/src/tools/computer-renderer.ts +147 -0
- package/src/tools/computer.ts +222 -0
- package/src/tools/conflict-detect.ts +815 -0
- package/src/tools/context.ts +49 -0
- package/src/tools/debug.ts +1121 -0
- package/src/tools/default-renderer.ts +154 -0
- package/src/tools/essential-tools.ts +46 -0
- package/src/tools/eval-backends.ts +34 -0
- package/src/tools/eval-format/index.ts +24 -0
- package/src/tools/eval-format/javascript.ts +952 -0
- package/src/tools/eval-format/julia.ts +446 -0
- package/src/tools/eval-format/python.ts +544 -0
- package/src/tools/eval-format/ruby.ts +380 -0
- package/src/tools/eval-render.ts +784 -0
- package/src/tools/eval.ts +774 -0
- package/src/tools/fetch.ts +1889 -0
- package/src/tools/file-recorder.ts +35 -0
- package/src/tools/fs-cache-invalidation.ts +28 -0
- package/src/tools/gh-cache-invalidation.ts +175 -0
- package/src/tools/gh-format.ts +12 -0
- package/src/tools/gh-renderer.ts +484 -0
- package/src/tools/gh.ts +3958 -0
- package/src/tools/github-cache.ts +663 -0
- package/src/tools/glob.ts +691 -0
- package/src/tools/grep.ts +1920 -0
- package/src/tools/grouped-file-output.ts +210 -0
- package/src/tools/hub/index.ts +579 -0
- package/src/tools/hub/jobs.ts +714 -0
- package/src/tools/hub/launch.ts +690 -0
- package/src/tools/hub/messaging.ts +735 -0
- package/src/tools/hub/types.ts +117 -0
- package/src/tools/image-gen.ts +1689 -0
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +736 -0
- package/src/tools/inspect-image-renderer.ts +133 -0
- package/src/tools/inspect-image.ts +316 -0
- package/src/tools/json-tree.ts +260 -0
- package/src/tools/jtd-to-json-schema.ts +219 -0
- package/src/tools/jtd-to-typescript.ts +136 -0
- package/src/tools/jtd-utils.ts +102 -0
- package/src/tools/learn.ts +141 -0
- package/src/tools/list-limit.ts +40 -0
- package/src/tools/manage-skill.ts +102 -0
- package/src/tools/match-line-format.ts +20 -0
- package/src/tools/memory-edit.ts +61 -0
- package/src/tools/memory-recall.ts +102 -0
- package/src/tools/memory-reflect.ts +88 -0
- package/src/tools/memory-render.ts +211 -0
- package/src/tools/memory-retain.ts +89 -0
- package/src/tools/output-meta.ts +860 -0
- package/src/tools/output-schema-validator.ts +307 -0
- package/src/tools/path-utils.ts +1489 -0
- package/src/tools/plan-mode-guard.ts +155 -0
- package/src/tools/puppeteer/00_stealth_tampering.txt +44 -0
- package/src/tools/puppeteer/01_stealth_activity.txt +80 -0
- package/src/tools/puppeteer/02_stealth_hairline.txt +57 -0
- package/src/tools/puppeteer/03_stealth_botd.txt +380 -0
- package/src/tools/puppeteer/04_stealth_iframe.txt +174 -0
- package/src/tools/puppeteer/05_stealth_webgl.txt +233 -0
- package/src/tools/puppeteer/06_stealth_screen.txt +260 -0
- package/src/tools/puppeteer/07_stealth_fonts.txt +99 -0
- package/src/tools/puppeteer/08_stealth_audio.txt +63 -0
- package/src/tools/puppeteer/09_stealth_locale.txt +51 -0
- package/src/tools/puppeteer/10_stealth_plugins.txt +212 -0
- package/src/tools/puppeteer/11_stealth_hardware.txt +59 -0
- package/src/tools/puppeteer/12_stealth_codecs.txt +42 -0
- package/src/tools/puppeteer/13_stealth_worker.txt +235 -0
- package/src/tools/read.ts +3762 -0
- package/src/tools/render-utils.ts +926 -0
- package/src/tools/renderers.ts +133 -0
- package/src/tools/report-tool-issue.ts +568 -0
- package/src/tools/resolve.ts +423 -0
- package/src/tools/review.ts +103 -0
- package/src/tools/run-scope.ts +417 -0
- package/src/tools/security-scan.ts +287 -0
- package/src/tools/shell-tokenize.ts +213 -0
- package/src/tools/sqlite-reader.ts +884 -0
- package/src/tools/terminal-output.ts +141 -0
- package/src/tools/todo.ts +1226 -0
- package/src/tools/tool-errors.ts +62 -0
- package/src/tools/tool-result.ts +102 -0
- package/src/tools/tool-timeouts.ts +38 -0
- package/src/tools/tts.ts +266 -0
- package/src/tools/vibe.ts +608 -0
- package/src/tools/write.ts +1646 -0
- package/src/tools/xdev.ts +560 -0
- package/src/tools/yield.ts +486 -0
- package/src/tts/downloader.ts +64 -0
- package/src/tts/index.ts +10 -0
- package/src/tts/models.ts +137 -0
- package/src/tts/runtime.ts +21 -0
- package/src/tts/speakable.ts +392 -0
- package/src/tts/speech-enhancer.ts +206 -0
- package/src/tts/streaming-player.ts +120 -0
- package/src/tts/tts-client.ts +475 -0
- package/src/tts/tts-protocol.ts +69 -0
- package/src/tts/tts-worker.ts +434 -0
- package/src/tts/vocalizer.ts +419 -0
- package/src/tts/wav.ts +58 -0
- package/src/tui/code-cell.ts +268 -0
- package/src/tui/file-list.ts +55 -0
- package/src/tui/hyperlink.ts +178 -0
- package/src/tui/index.ts +13 -0
- package/src/tui/output-block.ts +268 -0
- package/src/tui/status-line.ts +54 -0
- package/src/tui/tree-list.ts +172 -0
- package/src/tui/types.ts +15 -0
- package/src/tui/utils.ts +103 -0
- package/src/tui/width-aware-text.ts +58 -0
- package/src/utils/active-repo-context.ts +143 -0
- package/src/utils/block-context.ts +312 -0
- package/src/utils/changelog.ts +373 -0
- package/src/utils/clipboard.ts +361 -0
- package/src/utils/command-args.ts +74 -0
- package/src/utils/commit-message-generator.ts +148 -0
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/edit-mode.ts +61 -0
- package/src/utils/enhanced-paste.ts +230 -0
- package/src/utils/event-bus.ts +33 -0
- package/src/utils/external-editor.ts +80 -0
- package/src/utils/fetch-timeout.ts +10 -0
- package/src/utils/file-display-mode.ts +44 -0
- package/src/utils/file-mentions.ts +293 -0
- package/src/utils/git.ts +2472 -0
- package/src/utils/image-loading.ts +236 -0
- package/src/utils/image-resize.ts +420 -0
- package/src/utils/image-vision-fallback.ts +196 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/ipc.ts +38 -0
- package/src/utils/jj.ts +422 -0
- package/src/utils/lang-from-path.ts +251 -0
- package/src/utils/late-cleanup.ts +17 -0
- package/src/utils/local-date.ts +7 -0
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/markit-cache.ts +166 -0
- package/src/utils/markit.ts +223 -0
- package/src/utils/mupdf-wasm-embed.ts +12 -0
- package/src/utils/open.ts +126 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/prompt-path.ts +3 -0
- package/src/utils/qrcode.ts +535 -0
- package/src/utils/sample-profile.ts +437 -0
- package/src/utils/session-color.ts +142 -0
- package/src/utils/shell-snapshot-fn-env.sh +63 -0
- package/src/utils/shell-snapshot.ts +326 -0
- package/src/utils/sixel.ts +69 -0
- package/src/utils/thinking-display.ts +163 -0
- package/src/utils/title-generator.ts +597 -0
- package/src/utils/token-rate.ts +72 -0
- package/src/utils/tool-choice.ts +50 -0
- package/src/utils/tools-manager.ts +411 -0
- package/src/utils/turndown.ts +82 -0
- package/src/utils/zip.ts +1106 -0
- package/src/vibe/runtime.ts +1684 -0
- package/src/vibe/state.ts +4 -0
- package/src/web/kagi.ts +305 -0
- package/src/web/parallel.ts +354 -0
- package/src/web/scrapers/artifacthub.ts +207 -0
- package/src/web/scrapers/arxiv.ts +83 -0
- package/src/web/scrapers/aur.ts +162 -0
- package/src/web/scrapers/biorxiv.ts +133 -0
- package/src/web/scrapers/bluesky.ts +262 -0
- package/src/web/scrapers/brew.ts +172 -0
- package/src/web/scrapers/cheatsh.ts +68 -0
- package/src/web/scrapers/chocolatey.ts +196 -0
- package/src/web/scrapers/choosealicense.ts +95 -0
- package/src/web/scrapers/cisa-kev.ts +87 -0
- package/src/web/scrapers/clojars.ts +154 -0
- package/src/web/scrapers/coingecko.ts +177 -0
- package/src/web/scrapers/crates-io.ts +97 -0
- package/src/web/scrapers/crossref.ts +136 -0
- package/src/web/scrapers/devto.ts +147 -0
- package/src/web/scrapers/discogs.ts +306 -0
- package/src/web/scrapers/discourse.ts +197 -0
- package/src/web/scrapers/dockerhub.ts +138 -0
- package/src/web/scrapers/docs-rs.ts +663 -0
- package/src/web/scrapers/fdroid.ts +134 -0
- package/src/web/scrapers/firefox-addons.ts +191 -0
- package/src/web/scrapers/flathub.ts +223 -0
- package/src/web/scrapers/github-gist.ts +58 -0
- package/src/web/scrapers/github.ts +800 -0
- package/src/web/scrapers/gitlab.ts +401 -0
- package/src/web/scrapers/go-pkg.ts +266 -0
- package/src/web/scrapers/hackage.ts +129 -0
- package/src/web/scrapers/hackernews.ts +189 -0
- package/src/web/scrapers/hex.ts +105 -0
- package/src/web/scrapers/huggingface.ts +321 -0
- package/src/web/scrapers/iacr.ts +89 -0
- package/src/web/scrapers/index.ts +252 -0
- package/src/web/scrapers/jetbrains-marketplace.ts +159 -0
- package/src/web/scrapers/lemmy.ts +203 -0
- package/src/web/scrapers/lobsters.ts +175 -0
- package/src/web/scrapers/mastodon.ts +292 -0
- package/src/web/scrapers/maven.ts +138 -0
- package/src/web/scrapers/mdn.ts +173 -0
- package/src/web/scrapers/metacpan.ts +222 -0
- package/src/web/scrapers/musicbrainz.ts +250 -0
- package/src/web/scrapers/npm.ts +98 -0
- package/src/web/scrapers/nuget.ts +183 -0
- package/src/web/scrapers/nvd.ts +222 -0
- package/src/web/scrapers/ollama.ts +239 -0
- package/src/web/scrapers/open-vsx.ts +106 -0
- package/src/web/scrapers/opencorporates.ts +292 -0
- package/src/web/scrapers/openlibrary.ts +336 -0
- package/src/web/scrapers/orcid.ts +286 -0
- package/src/web/scrapers/osv.ts +176 -0
- package/src/web/scrapers/packagist.ts +160 -0
- package/src/web/scrapers/pub-dev.ts +143 -0
- package/src/web/scrapers/pubmed.ts +211 -0
- package/src/web/scrapers/pypi.ts +112 -0
- package/src/web/scrapers/rawg.ts +110 -0
- package/src/web/scrapers/readthedocs.ts +121 -0
- package/src/web/scrapers/reddit.ts +95 -0
- package/src/web/scrapers/repology.ts +251 -0
- package/src/web/scrapers/rfc.ts +201 -0
- package/src/web/scrapers/rubygems.ts +103 -0
- package/src/web/scrapers/searchcode.ts +189 -0
- package/src/web/scrapers/sec-edgar.ts +261 -0
- package/src/web/scrapers/semantic-scholar.ts +171 -0
- package/src/web/scrapers/snapcraft.ts +187 -0
- package/src/web/scrapers/sourcegraph.ts +336 -0
- package/src/web/scrapers/spdx.ts +108 -0
- package/src/web/scrapers/spotify.ts +198 -0
- package/src/web/scrapers/stackoverflow.ts +120 -0
- package/src/web/scrapers/terraform.ts +277 -0
- package/src/web/scrapers/tldr.ts +47 -0
- package/src/web/scrapers/twitter.ts +94 -0
- package/src/web/scrapers/types.ts +354 -0
- package/src/web/scrapers/utils.ts +109 -0
- package/src/web/scrapers/vimeo.ts +133 -0
- package/src/web/scrapers/vscode-marketplace.ts +187 -0
- package/src/web/scrapers/w3c.ts +156 -0
- package/src/web/scrapers/wikidata.ts +344 -0
- package/src/web/scrapers/wikipedia.ts +84 -0
- package/src/web/scrapers/youtube.ts +325 -0
- package/src/web/search/index.ts +393 -0
- package/src/web/search/provider.ts +272 -0
- package/src/web/search/providers/anthropic.ts +406 -0
- package/src/web/search/providers/base.ts +112 -0
- package/src/web/search/providers/brave.ts +181 -0
- package/src/web/search/providers/browser-headers.ts +82 -0
- package/src/web/search/providers/browser-page.ts +125 -0
- package/src/web/search/providers/codex.ts +788 -0
- package/src/web/search/providers/duckduckgo.ts +382 -0
- package/src/web/search/providers/ecosia.ts +183 -0
- package/src/web/search/providers/exa.ts +513 -0
- package/src/web/search/providers/firecrawl.ts +213 -0
- package/src/web/search/providers/gemini.ts +634 -0
- package/src/web/search/providers/google.ts +195 -0
- package/src/web/search/providers/jina.ts +144 -0
- package/src/web/search/providers/kagi.ts +98 -0
- package/src/web/search/providers/kimi.ts +221 -0
- package/src/web/search/providers/mojeek.ts +220 -0
- package/src/web/search/providers/parallel.ts +186 -0
- package/src/web/search/providers/perplexity-auth.ts +142 -0
- package/src/web/search/providers/perplexity.ts +997 -0
- package/src/web/search/providers/public.ts +199 -0
- package/src/web/search/providers/searxng.ts +467 -0
- package/src/web/search/providers/startpage.ts +225 -0
- package/src/web/search/providers/synthetic.ts +126 -0
- package/src/web/search/providers/tavily.ts +244 -0
- package/src/web/search/providers/tinyfish.ts +166 -0
- package/src/web/search/providers/utils.ts +130 -0
- package/src/web/search/providers/xai.ts +372 -0
- package/src/web/search/providers/zai.ts +452 -0
- package/src/web/search/query.ts +850 -0
- package/src/web/search/render.ts +262 -0
- package/src/web/search/types.ts +518 -0
- package/src/web/search/utils.ts +17 -0
- package/src/workspace-tree.ts +326 -0
|
@@ -0,0 +1,2747 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
ImageContent,
|
|
5
|
+
Message,
|
|
6
|
+
MessageAttribution,
|
|
7
|
+
ServiceTierByFamily,
|
|
8
|
+
TextContent,
|
|
9
|
+
Usage,
|
|
10
|
+
} from "@oh-my-pi/pi-ai";
|
|
11
|
+
import {
|
|
12
|
+
directoryExists,
|
|
13
|
+
getBlobsDir,
|
|
14
|
+
getProjectDir,
|
|
15
|
+
getSessionsDir,
|
|
16
|
+
isEnoent,
|
|
17
|
+
logger,
|
|
18
|
+
stringifyJson,
|
|
19
|
+
toError,
|
|
20
|
+
} from "@oh-my-pi/pi-utils";
|
|
21
|
+
import type { StructuredSubagentSchemaMode } from "../task/types";
|
|
22
|
+
import { ArtifactManager } from "./artifacts";
|
|
23
|
+
import { type BlobPutOptions, type BlobPutResult, BlobStore } from "./blob-store";
|
|
24
|
+
import {
|
|
25
|
+
type BashExecutionMessage,
|
|
26
|
+
type CustomMessage,
|
|
27
|
+
type FileMentionMessage,
|
|
28
|
+
type HookMessage,
|
|
29
|
+
normalizeCustomMessagePayload,
|
|
30
|
+
type PythonExecutionMessage,
|
|
31
|
+
sanitizeRehydratedOpenAIResponsesAssistantMessage,
|
|
32
|
+
stripInternalDetailsFields,
|
|
33
|
+
} from "./messages";
|
|
34
|
+
import { type BuildSessionContextOptions, buildSessionContext, type SessionContext } from "./session-context";
|
|
35
|
+
import {
|
|
36
|
+
type BranchSummaryEntry,
|
|
37
|
+
type CompactionEntry,
|
|
38
|
+
type CredentialPinEntry,
|
|
39
|
+
CURRENT_SESSION_VERSION,
|
|
40
|
+
type CustomEntry,
|
|
41
|
+
type CustomMessageEntry,
|
|
42
|
+
type FileEntry,
|
|
43
|
+
type LabelEntry,
|
|
44
|
+
type ModeChangeEntry,
|
|
45
|
+
type ModelChangeEntry,
|
|
46
|
+
type NewSessionOptions,
|
|
47
|
+
type ResetBoundaryEntry,
|
|
48
|
+
type ServiceTierChangeEntry,
|
|
49
|
+
type SessionEntry,
|
|
50
|
+
type SessionHeader,
|
|
51
|
+
type SessionInitEntry,
|
|
52
|
+
type SessionMessageEntry,
|
|
53
|
+
type SessionTitleSource,
|
|
54
|
+
type SessionTreeNode,
|
|
55
|
+
type ThinkingLevelChangeEntry,
|
|
56
|
+
TITLE_CHANGE_ENTRY_TYPE,
|
|
57
|
+
type TitleChangeEntry,
|
|
58
|
+
type TtsrInjectionEntry,
|
|
59
|
+
type UsageStatistics,
|
|
60
|
+
} from "./session-entries";
|
|
61
|
+
import { findMostRecentSession, listAllSessions, listSessions, type SessionInfo } from "./session-listing";
|
|
62
|
+
import { loadEntriesFromFile, readTitleSlotFromFile, resolveBlobRefsInEntries } from "./session-loader";
|
|
63
|
+
import { generateId, migrateToCurrentVersion } from "./session-migrations";
|
|
64
|
+
import {
|
|
65
|
+
computeDefaultSessionDir,
|
|
66
|
+
readTerminalBreadcrumbEntry,
|
|
67
|
+
resolveManagedSessionRoot,
|
|
68
|
+
writeTerminalBreadcrumb,
|
|
69
|
+
} from "./session-paths";
|
|
70
|
+
import { prepareEntryForPersistence } from "./session-persistence";
|
|
71
|
+
import {
|
|
72
|
+
FileSessionStorage,
|
|
73
|
+
MemorySessionStorage,
|
|
74
|
+
type SessionStorage,
|
|
75
|
+
type SessionStorageWriter,
|
|
76
|
+
} from "./session-storage";
|
|
77
|
+
import { type SessionTitleUpdate, serializeTitleSlot } from "./session-title-slot";
|
|
78
|
+
import {
|
|
79
|
+
additionalWorkspaceDirectories,
|
|
80
|
+
normalizeSessionWorkspace,
|
|
81
|
+
normalizeWorkspaceDirectory,
|
|
82
|
+
} from "./session-workspace";
|
|
83
|
+
|
|
84
|
+
const JSONL_SUFFIX_LENGTH = ".jsonl".length;
|
|
85
|
+
const DRAFT_ONLY_SESSION_MARKER = ".draft-only-session";
|
|
86
|
+
|
|
87
|
+
function mintSessionId(): string {
|
|
88
|
+
return Bun.randomUUIDv7();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function nowIso(): string {
|
|
92
|
+
return new Date().toISOString();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function fileSafeTimestamp(iso: string): string {
|
|
96
|
+
return iso.replace(/[:.]/g, "-");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function artifactsDirectoryFor(sessionFile: string | undefined): string | null {
|
|
100
|
+
return sessionFile ? sessionFile.slice(0, -JSONL_SUFFIX_LENGTH) : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve a breadcrumb's recorded session file to its interactive root. Subagent
|
|
105
|
+
* (and other artifact) sessions live inside a parent session's artifacts dir —
|
|
106
|
+
* `<parent>.jsonl` strips its suffix to `<parent>/`, and a child writes
|
|
107
|
+
* `<parent>/<agentId>.jsonl`. A breadcrumb that points at such a child — a
|
|
108
|
+
* pre-fix poisoned crumb left by a subagent that opened in the parent's TTY, or
|
|
109
|
+
* any nested artifact — must resolve back up to the top-level session so
|
|
110
|
+
* `--continue` resumes the real conversation instead of a subagent transcript.
|
|
111
|
+
*/
|
|
112
|
+
function resolveBreadcrumbToInteractiveRoot(sessionFile: string): string {
|
|
113
|
+
let current = path.resolve(sessionFile);
|
|
114
|
+
// Walk up while the containing dir is itself a session's artifacts dir
|
|
115
|
+
// (`<dir>.jsonl` exists). Capped to defend against pathological layouts.
|
|
116
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
117
|
+
const parentSessionFile = `${path.dirname(current)}.jsonl`;
|
|
118
|
+
if (!fs.existsSync(parentSessionFile)) return current;
|
|
119
|
+
current = parentSessionFile;
|
|
120
|
+
}
|
|
121
|
+
return current;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function emptyUsageStatistics(): UsageStatistics {
|
|
125
|
+
return {
|
|
126
|
+
input: 0,
|
|
127
|
+
output: 0,
|
|
128
|
+
cacheRead: 0,
|
|
129
|
+
cacheWrite: 0,
|
|
130
|
+
totalTokens: 0,
|
|
131
|
+
orchestrationInput: 0,
|
|
132
|
+
orchestrationOutput: 0,
|
|
133
|
+
orchestrationCacheRead: 0,
|
|
134
|
+
premiumRequests: 0,
|
|
135
|
+
cost: 0,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function taskUsageFrom(details: unknown): Usage | undefined {
|
|
140
|
+
if (details === null || typeof details !== "object") return undefined;
|
|
141
|
+
const maybeUsage = (details as Record<string, unknown>).usage;
|
|
142
|
+
return maybeUsage !== null && typeof maybeUsage === "object" ? (maybeUsage as Usage) : undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function entryUsage(entry: SessionEntry): Usage | undefined {
|
|
146
|
+
if (entry.type !== "message") return undefined;
|
|
147
|
+
const message = entry.message;
|
|
148
|
+
if (message.role === "assistant") return message.usage;
|
|
149
|
+
if (message.role === "toolResult" && message.toolName === "task") return taskUsageFrom(message.details);
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function addUsage(target: UsageStatistics, usage: Usage | undefined): void {
|
|
154
|
+
if (!usage) return;
|
|
155
|
+
target.input += usage.input;
|
|
156
|
+
target.output += usage.output;
|
|
157
|
+
target.cacheRead += usage.cacheRead;
|
|
158
|
+
target.cacheWrite += usage.cacheWrite;
|
|
159
|
+
target.totalTokens += usage.totalTokens;
|
|
160
|
+
target.orchestrationInput += usage.orchestration?.input ?? 0;
|
|
161
|
+
target.orchestrationOutput += usage.orchestration?.output ?? 0;
|
|
162
|
+
target.orchestrationCacheRead += usage.orchestration?.cacheRead ?? 0;
|
|
163
|
+
target.premiumRequests += usage.premiumRequests ?? 0;
|
|
164
|
+
target.cost += usage.cost.total;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isAssistantEntry(entry: SessionEntry): boolean {
|
|
168
|
+
return entry.type === "message" && entry.message.role === "assistant";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isDraftOnlyMetadataEntry(entry: SessionEntry): boolean {
|
|
172
|
+
// Startup-recorded selector state that does not survive as user intent
|
|
173
|
+
// once the draft is cleared. `mode_change` covers the `plan.defaultOnStartup`
|
|
174
|
+
// path (interactive-mode.ts enters plan mode before draft restoration) and
|
|
175
|
+
// `/plan` toggles that leave the session otherwise empty; entries carrying
|
|
176
|
+
// real conversation state — messages, compactions, branch summaries,
|
|
177
|
+
// custom/custom_message, session_init, labels, title/tool selection — never
|
|
178
|
+
// reach this branch and always keep the file resumable.
|
|
179
|
+
switch (entry.type) {
|
|
180
|
+
case "model_change":
|
|
181
|
+
case "thinking_level_change":
|
|
182
|
+
case "service_tier_change":
|
|
183
|
+
case "mode_change":
|
|
184
|
+
case "credential_pin":
|
|
185
|
+
return true;
|
|
186
|
+
default:
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function orderedByTimestamp(a: SessionTreeNode, b: SessionTreeNode): number {
|
|
192
|
+
return new Date(a.entry.timestamp).getTime() - new Date(b.entry.timestamp).getTime();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Maintains the derived views over a session's entry list: id lookup, the
|
|
197
|
+
* parent→children adjacency, the resolved label map, the active leaf, and the
|
|
198
|
+
* running usage totals. Kept in lockstep with the manager's `#entries` so reads
|
|
199
|
+
* stay O(1)/O(children) instead of rescanning the whole journal.
|
|
200
|
+
*/
|
|
201
|
+
class SessionEntryIndex {
|
|
202
|
+
#entriesById = new Map<string, SessionEntry>();
|
|
203
|
+
#children = new Map<string | null, SessionEntry[]>();
|
|
204
|
+
#labels = new Map<string, string>();
|
|
205
|
+
#leaf: string | null = null;
|
|
206
|
+
#usage = emptyUsageStatistics();
|
|
207
|
+
|
|
208
|
+
clear(): void {
|
|
209
|
+
this.#entriesById.clear();
|
|
210
|
+
this.#children.clear();
|
|
211
|
+
this.#labels.clear();
|
|
212
|
+
this.#leaf = null;
|
|
213
|
+
this.#usage = emptyUsageStatistics();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
rebuild(entries: readonly SessionEntry[]): void {
|
|
217
|
+
this.clear();
|
|
218
|
+
for (const entry of entries) this.insert(entry);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
insert(entry: SessionEntry): void {
|
|
222
|
+
this.#entriesById.set(entry.id, entry);
|
|
223
|
+
this.#leaf = entry.id;
|
|
224
|
+
|
|
225
|
+
const bucket = this.#children.get(entry.parentId);
|
|
226
|
+
if (bucket) bucket.push(entry);
|
|
227
|
+
else this.#children.set(entry.parentId, [entry]);
|
|
228
|
+
|
|
229
|
+
if (entry.type === "label") {
|
|
230
|
+
if (entry.label) this.#labels.set(entry.targetId, entry.label);
|
|
231
|
+
else this.#labels.delete(entry.targetId);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
addUsage(this.#usage, entryUsage(entry));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
has(id: string): boolean {
|
|
238
|
+
return this.#entriesById.has(id);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
get(id: string): SessionEntry | undefined {
|
|
242
|
+
return this.#entriesById.get(id);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The live id→entry map. Read-only for callers (lookups + `generateId`
|
|
247
|
+
* collision checks); never mutate it directly — go through `insert`/`rebuild`.
|
|
248
|
+
*/
|
|
249
|
+
entriesById(): Map<string, SessionEntry> {
|
|
250
|
+
return this.#entriesById;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
leafId(): string | null {
|
|
254
|
+
return this.#leaf;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
leafEntry(): SessionEntry | undefined {
|
|
258
|
+
return this.#leaf ? this.#entriesById.get(this.#leaf) : undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
setLeaf(id: string | null): void {
|
|
262
|
+
this.#leaf = id;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
childrenOf(parentId: string): SessionEntry[] {
|
|
266
|
+
return [...(this.#children.get(parentId) ?? [])];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
labelFor(id: string): string | undefined {
|
|
270
|
+
return this.#labels.get(id);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
labelsInEffect(): IterableIterator<[string, string]> {
|
|
274
|
+
return this.#labels.entries();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
usageSnapshot(): UsageStatistics {
|
|
278
|
+
return { ...this.#usage };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
pathTo(id: string | null | undefined = this.#leaf): SessionEntry[] {
|
|
282
|
+
const branch: SessionEntry[] = [];
|
|
283
|
+
const seen = new Set<string>();
|
|
284
|
+
let cursor = id ? this.#entriesById.get(id) : undefined;
|
|
285
|
+
|
|
286
|
+
while (cursor && !seen.has(cursor.id)) {
|
|
287
|
+
seen.add(cursor.id);
|
|
288
|
+
branch.push(cursor);
|
|
289
|
+
cursor = cursor.parentId ? this.#entriesById.get(cursor.parentId) : undefined;
|
|
290
|
+
}
|
|
291
|
+
branch.reverse();
|
|
292
|
+
return branch;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
tree(entries: readonly SessionEntry[]): SessionTreeNode[] {
|
|
296
|
+
const nodes = new Map<string, SessionTreeNode>();
|
|
297
|
+
const roots: SessionTreeNode[] = [];
|
|
298
|
+
|
|
299
|
+
for (const entry of entries) {
|
|
300
|
+
nodes.set(entry.id, { entry, children: [], label: this.#labels.get(entry.id) });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
for (const entry of entries) {
|
|
304
|
+
const node = nodes.get(entry.id)!;
|
|
305
|
+
const parentId = entry.parentId;
|
|
306
|
+
if (parentId === null || parentId === entry.id) {
|
|
307
|
+
roots.push(node);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const parent = nodes.get(parentId);
|
|
312
|
+
if (parent) parent.children.push(node);
|
|
313
|
+
else roots.push(node);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const stack = [...roots];
|
|
317
|
+
while (stack.length > 0) {
|
|
318
|
+
const node = stack.pop()!;
|
|
319
|
+
node.children.sort(orderedByTimestamp);
|
|
320
|
+
stack.push(...node.children);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return roots;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export type ReadonlySessionManager = Pick<
|
|
328
|
+
SessionManager,
|
|
329
|
+
| "getCwd"
|
|
330
|
+
| "getSessionDir"
|
|
331
|
+
| "getSessionId"
|
|
332
|
+
| "getSessionFile"
|
|
333
|
+
| "getSessionName"
|
|
334
|
+
| "getArtifactsDir"
|
|
335
|
+
| "getArtifactManager"
|
|
336
|
+
| "allocateArtifactPath"
|
|
337
|
+
| "saveArtifact"
|
|
338
|
+
| "getArtifactPath"
|
|
339
|
+
| "getLeafId"
|
|
340
|
+
| "getLeafEntry"
|
|
341
|
+
| "getEntry"
|
|
342
|
+
| "getLabel"
|
|
343
|
+
| "getBranch"
|
|
344
|
+
| "getHeader"
|
|
345
|
+
| "getEntries"
|
|
346
|
+
| "getTree"
|
|
347
|
+
| "getUsageStatistics"
|
|
348
|
+
| "putBlob"
|
|
349
|
+
| "putBlobSync"
|
|
350
|
+
>;
|
|
351
|
+
|
|
352
|
+
interface SessionManagerStateSnapshot {
|
|
353
|
+
cwd: string;
|
|
354
|
+
sessionDir: string;
|
|
355
|
+
sessionId: string;
|
|
356
|
+
sessionName: string | undefined;
|
|
357
|
+
titleSource: SessionTitleSource | undefined;
|
|
358
|
+
sessionFile: string | undefined;
|
|
359
|
+
titleUpdatedAt: string;
|
|
360
|
+
hasTitleSlot: boolean;
|
|
361
|
+
onDisk: boolean;
|
|
362
|
+
needsRewrite: boolean;
|
|
363
|
+
draftOnlySessionCleanupArmed: boolean;
|
|
364
|
+
header: SessionHeader;
|
|
365
|
+
entries: SessionEntry[];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
interface DiskQueueOptions {
|
|
369
|
+
ignorePriorError?: boolean;
|
|
370
|
+
ignoreEpoch?: boolean;
|
|
371
|
+
epoch?: number;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
interface AtomicEntryBatch {
|
|
375
|
+
collecting: boolean;
|
|
376
|
+
entryIds: Set<string>;
|
|
377
|
+
deferredNotifications: SessionEntry[];
|
|
378
|
+
preBatchLeafId: string | null;
|
|
379
|
+
externalLeafChanged: boolean;
|
|
380
|
+
externalLeafId: string | null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The storage may have published a write that rejected, and an authoritative
|
|
385
|
+
* repair could not be proven durable. Callers must fail closed until recovery.
|
|
386
|
+
*/
|
|
387
|
+
export class SessionPersistenceIndeterminateError extends AggregateError {
|
|
388
|
+
readonly operationError: Error;
|
|
389
|
+
readonly recoveryErrors: readonly Error[];
|
|
390
|
+
|
|
391
|
+
constructor(operationError: Error, recoveryErrors: readonly Error[]) {
|
|
392
|
+
super(
|
|
393
|
+
[operationError, ...recoveryErrors],
|
|
394
|
+
`Session persistence is indeterminate after "${operationError.message}" and authoritative repair failed.`,
|
|
395
|
+
);
|
|
396
|
+
this.name = "SessionPersistenceIndeterminateError";
|
|
397
|
+
this.operationError = operationError;
|
|
398
|
+
this.recoveryErrors = [...recoveryErrors];
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Stores and navigates an append-only conversation journal.
|
|
404
|
+
*
|
|
405
|
+
* A session is a JSONL file: one header line followed by entries. Entries form a
|
|
406
|
+
* tree by `(id, parentId)`, and the mutable leaf pointer selects which path is
|
|
407
|
+
* active for future appends and for LLM context construction.
|
|
408
|
+
*
|
|
409
|
+
* Durability is software-crash safe but not power-loss safe: completed entries
|
|
410
|
+
* (user/assistant/toolResult messages, tool_execution_start markers, custom
|
|
411
|
+
* entries) are handed to the OS synchronously in-body on append and never
|
|
412
|
+
* `fsync`'d. In-flight streaming text is intentionally not durable until
|
|
413
|
+
* `message_end` persists the finished message.
|
|
414
|
+
*
|
|
415
|
+
* While an in-place atomic rewrite is publishing, a concurrent completed append
|
|
416
|
+
* supersedes that publish with a synchronous full-body rewrite so the entry is
|
|
417
|
+
* software-crash durable before the append returns; the abandoned atomic's
|
|
418
|
+
* `commitGuard` then refuses to clobber the fresher body.
|
|
419
|
+
*
|
|
420
|
+
* During {@link moveTo}, appends write a full body to the live relocation path
|
|
421
|
+
* (source until rename, destination once the rename has landed) so a crash mid-
|
|
422
|
+
* move still preserves completed entries without recreating a vacated source.
|
|
423
|
+
* A trailing atomic rewrite still rewrites the header cwd after the path is
|
|
424
|
+
* repointed.
|
|
425
|
+
*/
|
|
426
|
+
export class SessionManager {
|
|
427
|
+
#cwd: string;
|
|
428
|
+
/** Additional workspace directories beyond cwd (multi-root). Normalized absolute, deduped, excludes cwd. */
|
|
429
|
+
#additionalDirectories: string[] = [];
|
|
430
|
+
#sessionDir: string;
|
|
431
|
+
readonly #persist: boolean;
|
|
432
|
+
readonly #storage: SessionStorage;
|
|
433
|
+
readonly #blobs: BlobStore;
|
|
434
|
+
|
|
435
|
+
#sessionId = "";
|
|
436
|
+
#sessionName: string | undefined;
|
|
437
|
+
#titleSource: SessionTitleSource | undefined;
|
|
438
|
+
#sessionFile: string | undefined;
|
|
439
|
+
#header!: SessionHeader;
|
|
440
|
+
#titleUpdatedAt = "";
|
|
441
|
+
#hasTitleSlot = true;
|
|
442
|
+
#entries: SessionEntry[] = [];
|
|
443
|
+
#index = new SessionEntryIndex();
|
|
444
|
+
|
|
445
|
+
/** File reflects all current entries; appends can go incrementally. */
|
|
446
|
+
#fileIsCurrent = false;
|
|
447
|
+
/** In-memory entries diverged from disk (load-migration/sanitize) → next persist must full-rewrite. */
|
|
448
|
+
#rewriteRequired = false;
|
|
449
|
+
/** Lazy gate crossed (ensureOnDisk / loaded file): every entry must persist from now on. */
|
|
450
|
+
#forceFileCreation = false;
|
|
451
|
+
/**
|
|
452
|
+
* Armed only when this manager observed a draft sidecar lifecycle that
|
|
453
|
+
* materialized an otherwise metadata-only session file. Explicit
|
|
454
|
+
* ensureOnDisk() callers (ACP session/new, handoff) must survive close().
|
|
455
|
+
*/
|
|
456
|
+
#draftOnlySessionCleanupArmed = false;
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Collab replication tap: invoked for every appended entry with the
|
|
460
|
+
* in-memory (pre-blob-externalization) entry, so inline images survive.
|
|
461
|
+
*/
|
|
462
|
+
onEntryAppended?: (entry: SessionEntry) => void;
|
|
463
|
+
|
|
464
|
+
#turnBudgetTotal: number | null = null;
|
|
465
|
+
#turnBudgetHard = false;
|
|
466
|
+
#turnOutputBaseline = 0;
|
|
467
|
+
#turnEvalOutput = 0;
|
|
468
|
+
|
|
469
|
+
/** The single open append writer; the manager only ever writes one file at a time. */
|
|
470
|
+
#writer: SessionStorageWriter | undefined;
|
|
471
|
+
/** Serializes async disk work (flush/close/atomic rewrite). Appends are synchronous and bypass it. */
|
|
472
|
+
#diskTail: Promise<void> = Promise.resolve();
|
|
473
|
+
#diskFailure: Error | undefined;
|
|
474
|
+
#diskFailureLogged = false;
|
|
475
|
+
/** FIFO reservation for atomic batches and authoritative recovery. */
|
|
476
|
+
#atomicPersistenceTail: Promise<void> = Promise.resolve();
|
|
477
|
+
/** Observer notifications withheld until their entries are proven durable. */
|
|
478
|
+
#pendingDurabilityNotifications: SessionEntry[] = [];
|
|
479
|
+
/** Bumped on every sync rewrite / chain reset so stale queued tasks become no-ops. */
|
|
480
|
+
#diskEpoch = 0;
|
|
481
|
+
/**
|
|
482
|
+
* Epoch of the in-flight atomic rewrite, or `null` when no rewrite is running.
|
|
483
|
+
* The fence in {@link #appendToSessionFile} only applies while this matches
|
|
484
|
+
* `#diskEpoch`: once a synchronous rewrite (`flushSync` → `#rewriteSynchronously`)
|
|
485
|
+
* bumps the epoch, the pending atomic publish is guaranteed to abandon via
|
|
486
|
+
* its `commitGuard`, and appends can safely take the hot path against the
|
|
487
|
+
* freshly-published file.
|
|
488
|
+
*/
|
|
489
|
+
#atomicRewriteFenceEpoch: number | null = null;
|
|
490
|
+
/** Set by synchronous appends that land while an atomic replacement is active. */
|
|
491
|
+
#atomicRewriteDirty = false;
|
|
492
|
+
/**
|
|
493
|
+
* Active {@link moveTo} relocation. Concurrent completed appends write a
|
|
494
|
+
* full body to the live path: source while it still exists, destination
|
|
495
|
+
* once rename has landed (source gone). Never recreates a vacated source.
|
|
496
|
+
* `null` outside an active relocation.
|
|
497
|
+
*/
|
|
498
|
+
#sessionFileRelocating: { source: string; dest: string } | null = null;
|
|
499
|
+
/** Atomic entry batch currently staged for a full-file commit. */
|
|
500
|
+
#atomicEntryBatch: AtomicEntryBatch | undefined;
|
|
501
|
+
|
|
502
|
+
#artifactManager: ArtifactManager | null = null;
|
|
503
|
+
#artifactManagerSessionFile: string | null = null;
|
|
504
|
+
#adoptedArtifactManager: ArtifactManager | null = null;
|
|
505
|
+
#inMemoryArtifacts: Map<string, string> | null = null;
|
|
506
|
+
#inMemoryArtifactCounter = 0;
|
|
507
|
+
|
|
508
|
+
#suppressBreadcrumb = false;
|
|
509
|
+
/**
|
|
510
|
+
* The last breadcrumb this manager wrote marked a lazy `/new` boundary whose
|
|
511
|
+
* JSONL is not yet on disk. Cleared (and the crumb re-stamped non-fresh) once
|
|
512
|
+
* the session materializes, so a materialized-then-deleted session still falls
|
|
513
|
+
* back to the most-recent session instead of being treated as a fresh crumb.
|
|
514
|
+
*/
|
|
515
|
+
#breadcrumbFresh = false;
|
|
516
|
+
#sessionNameChangedCallbacks = new Set<() => void>();
|
|
517
|
+
|
|
518
|
+
private constructor(cwd: string, sessionDir: string, persist: boolean, storage: SessionStorage) {
|
|
519
|
+
this.#cwd = cwd;
|
|
520
|
+
this.#sessionDir = sessionDir;
|
|
521
|
+
this.#persist = persist;
|
|
522
|
+
this.#storage = storage;
|
|
523
|
+
this.#blobs = new BlobStore(getBlobsDir());
|
|
524
|
+
|
|
525
|
+
if (persist && sessionDir) this.#storage.ensureDirSync(sessionDir);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
#rememberBreadcrumb(cwd: string, sessionFile: string, fresh = false): void {
|
|
529
|
+
this.#breadcrumbFresh = fresh;
|
|
530
|
+
if (!this.#suppressBreadcrumb) writeTerminalBreadcrumb(cwd, sessionFile, fresh);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Re-stamp a fresh `/new` breadcrumb as non-fresh once the session has
|
|
535
|
+
* materialized on disk. A no-op unless the current breadcrumb is still fresh.
|
|
536
|
+
*/
|
|
537
|
+
#materializeBreadcrumb(): void {
|
|
538
|
+
if (!this.#breadcrumbFresh || !this.#sessionFile) return;
|
|
539
|
+
this.#rememberBreadcrumb(this.#cwd, this.#sessionFile, false);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
#clearDiskError(): void {
|
|
543
|
+
this.#diskFailure = undefined;
|
|
544
|
+
this.#diskFailureLogged = false;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
#noteDiskFailure(errorLike: unknown): Error {
|
|
548
|
+
const error = toError(errorLike);
|
|
549
|
+
if (!this.#diskFailure) this.#diskFailure = error;
|
|
550
|
+
|
|
551
|
+
if (!this.#diskFailureLogged) {
|
|
552
|
+
this.#diskFailureLogged = true;
|
|
553
|
+
logger.error("Session persistence error.", {
|
|
554
|
+
sessionFile: this.#sessionFile,
|
|
555
|
+
error: error.message,
|
|
556
|
+
stack: error.stack,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return this.#diskFailure;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
#scheduleDiskWork(work: () => Promise<void>, options: DiskQueueOptions = {}): Promise<void> {
|
|
564
|
+
const epoch = options.epoch ?? this.#diskEpoch;
|
|
565
|
+
const scheduled = this.#diskTail
|
|
566
|
+
.catch(() => undefined)
|
|
567
|
+
.then(async () => {
|
|
568
|
+
if (!options.ignoreEpoch && epoch !== this.#diskEpoch) return;
|
|
569
|
+
if (this.#diskFailure && !options.ignorePriorError) throw this.#diskFailure;
|
|
570
|
+
await work();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
const reported = scheduled.catch(err => {
|
|
574
|
+
throw this.#noteDiskFailure(err);
|
|
575
|
+
});
|
|
576
|
+
this.#diskTail = reported.catch(() => undefined);
|
|
577
|
+
return reported;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async #withAtomicPersistenceLock<T>(operation: () => Promise<T>): Promise<T> {
|
|
581
|
+
const predecessor = this.#atomicPersistenceTail;
|
|
582
|
+
const turn = Promise.withResolvers<void>();
|
|
583
|
+
this.#atomicPersistenceTail = predecessor.catch(() => undefined).then(() => turn.promise);
|
|
584
|
+
await predecessor.catch(() => undefined);
|
|
585
|
+
try {
|
|
586
|
+
return await operation();
|
|
587
|
+
} finally {
|
|
588
|
+
turn.resolve();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async #drainAndCloseWriter(): Promise<void> {
|
|
593
|
+
try {
|
|
594
|
+
await this.#scheduleDiskWork(
|
|
595
|
+
async () => {
|
|
596
|
+
await this.#closeWriterHandle();
|
|
597
|
+
},
|
|
598
|
+
{ ignorePriorError: true, ignoreEpoch: true },
|
|
599
|
+
);
|
|
600
|
+
} finally {
|
|
601
|
+
this.#writer = undefined;
|
|
602
|
+
this.#diskTail = Promise.resolve();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
#closeWriterEventually(): void {
|
|
607
|
+
const writer = this.#writer;
|
|
608
|
+
this.#writer = undefined;
|
|
609
|
+
if (writer) void writer.close().catch(() => undefined);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async #closeWriterHandle(): Promise<void> {
|
|
613
|
+
const writer = this.#writer;
|
|
614
|
+
if (!writer) return;
|
|
615
|
+
this.#writer = undefined;
|
|
616
|
+
await writer.close();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
#latchIndeterminate(operationError: Error, recoveryErrors: readonly Error[]): SessionPersistenceIndeterminateError {
|
|
620
|
+
const error = new SessionPersistenceIndeterminateError(operationError, recoveryErrors);
|
|
621
|
+
this.#diskFailure = error;
|
|
622
|
+
if (!this.#diskFailureLogged) {
|
|
623
|
+
this.#diskFailureLogged = true;
|
|
624
|
+
logger.error("Session persistence became indeterminate.", {
|
|
625
|
+
sessionFile: this.#sessionFile,
|
|
626
|
+
error: error.message,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
return error;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
#notifyDurableEntries(entries: readonly SessionEntry[] = []): void {
|
|
633
|
+
const notifications = [...this.#pendingDurabilityNotifications, ...entries];
|
|
634
|
+
this.#pendingDurabilityNotifications = [];
|
|
635
|
+
const seen = new Set<string>();
|
|
636
|
+
for (const entry of notifications) {
|
|
637
|
+
if (seen.has(entry.id)) continue;
|
|
638
|
+
seen.add(entry.id);
|
|
639
|
+
this.#notifyEntryAppended(entry);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async #authoritativelyRewriteCurrentStateLocked(operationError: Error): Promise<void> {
|
|
644
|
+
if (!this.#persist || !this.#sessionFile) return;
|
|
645
|
+
const previousDiskTail = this.#diskTail;
|
|
646
|
+
const writer = this.#writer;
|
|
647
|
+
this.#diskEpoch++;
|
|
648
|
+
const epoch = this.#diskEpoch;
|
|
649
|
+
this.#writer = undefined;
|
|
650
|
+
this.#diskTail = Promise.resolve();
|
|
651
|
+
this.#forceFileCreation = true;
|
|
652
|
+
this.#fileIsCurrent = false;
|
|
653
|
+
this.#rewriteRequired = true;
|
|
654
|
+
this.#atomicRewriteFenceEpoch = epoch;
|
|
655
|
+
if (!this.#diskFailure) this.#diskFailure = operationError;
|
|
656
|
+
try {
|
|
657
|
+
await previousDiskTail.catch(() => undefined);
|
|
658
|
+
let closeError: Error | undefined;
|
|
659
|
+
if (writer) {
|
|
660
|
+
try {
|
|
661
|
+
await writer.close();
|
|
662
|
+
} catch (error) {
|
|
663
|
+
closeError = toError(error);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
let drainError: Error | undefined;
|
|
667
|
+
try {
|
|
668
|
+
await this.#storage.drain();
|
|
669
|
+
} catch (error) {
|
|
670
|
+
drainError = toError(error);
|
|
671
|
+
}
|
|
672
|
+
if (writer?.isOpen()) {
|
|
673
|
+
throw this.#latchIndeterminate(operationError, [
|
|
674
|
+
closeError ?? new Error("Failed to close session writer before authoritative repair."),
|
|
675
|
+
...(drainError ? [drainError] : []),
|
|
676
|
+
]);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
do {
|
|
680
|
+
this.#atomicRewriteDirty = false;
|
|
681
|
+
const sessionFile = this.#sessionFile;
|
|
682
|
+
if (!sessionFile) {
|
|
683
|
+
throw this.#latchIndeterminate(operationError, [
|
|
684
|
+
new Error("Session file disappeared during authoritative repair."),
|
|
685
|
+
]);
|
|
686
|
+
}
|
|
687
|
+
const body = this.#fileBody();
|
|
688
|
+
try {
|
|
689
|
+
await this.#storage.writeTextAtomic(sessionFile, body, {
|
|
690
|
+
commitGuard: () => this.#diskEpoch === epoch,
|
|
691
|
+
});
|
|
692
|
+
} catch (error) {
|
|
693
|
+
const recoveryErrors = [toError(error)];
|
|
694
|
+
try {
|
|
695
|
+
await this.#storage.drain();
|
|
696
|
+
} catch (drainFailure) {
|
|
697
|
+
recoveryErrors.push(toError(drainFailure));
|
|
698
|
+
}
|
|
699
|
+
let actual: string;
|
|
700
|
+
try {
|
|
701
|
+
actual = await this.#storage.readText(sessionFile);
|
|
702
|
+
} catch (readFailure) {
|
|
703
|
+
recoveryErrors.push(toError(readFailure));
|
|
704
|
+
throw this.#latchIndeterminate(operationError, recoveryErrors);
|
|
705
|
+
}
|
|
706
|
+
if (actual !== body) {
|
|
707
|
+
recoveryErrors.push(new Error("Authoritative session repair did not match durable storage."));
|
|
708
|
+
throw this.#latchIndeterminate(operationError, recoveryErrors);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (this.#diskEpoch !== epoch) {
|
|
712
|
+
throw this.#latchIndeterminate(operationError, [
|
|
713
|
+
new Error("Authoritative session repair was superseded before verification."),
|
|
714
|
+
]);
|
|
715
|
+
}
|
|
716
|
+
} while (this.#atomicRewriteDirty);
|
|
717
|
+
|
|
718
|
+
this.#fileIsCurrent = true;
|
|
719
|
+
this.#rewriteRequired = false;
|
|
720
|
+
this.#hasTitleSlot = true;
|
|
721
|
+
this.#clearDiskError();
|
|
722
|
+
} catch (error) {
|
|
723
|
+
if (error instanceof SessionPersistenceIndeterminateError) throw error;
|
|
724
|
+
throw this.#latchIndeterminate(operationError, [toError(error)]);
|
|
725
|
+
} finally {
|
|
726
|
+
if (this.#atomicRewriteFenceEpoch === epoch) this.#atomicRewriteFenceEpoch = null;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
#appendWriter(): SessionStorageWriter {
|
|
731
|
+
if (!this.#sessionFile) throw new Error("Cannot open a session writer before a session file exists");
|
|
732
|
+
|
|
733
|
+
if (this.#writer?.isOpen()) return this.#writer;
|
|
734
|
+
|
|
735
|
+
this.#writer = this.#storage.openWriter(this.#sessionFile, {
|
|
736
|
+
flags: "a",
|
|
737
|
+
onError: err => this.#noteDiskFailure(err),
|
|
738
|
+
});
|
|
739
|
+
return this.#writer;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
#lineFor(entry: FileEntry): string {
|
|
743
|
+
return `${stringifyJson(prepareEntryForPersistence(entry, this.#blobs)) ?? "null"}\n`;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
#titleSlotLine(): string {
|
|
747
|
+
return serializeTitleSlot({
|
|
748
|
+
title: this.#sessionName,
|
|
749
|
+
source: this.#titleSource,
|
|
750
|
+
updatedAt: this.#titleUpdatedAt || this.#header.timestamp,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
#fileBody(): string {
|
|
755
|
+
let body = this.#titleSlotLine();
|
|
756
|
+
body += this.#lineFor(this.#header);
|
|
757
|
+
for (const entry of this.#entries) body += this.#lineFor(entry);
|
|
758
|
+
return body;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
#historyContainsAssistantMessage(): boolean {
|
|
762
|
+
return this.#entries.some(isAssistantEntry);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
#shouldHaveSessionFile(): boolean {
|
|
766
|
+
return this.#forceFileCreation || this.#fileIsCurrent || this.#historyContainsAssistantMessage();
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Live path for concurrent completed appends during {@link moveTo}.
|
|
771
|
+
* Prefers destination once rename has landed (source gone); otherwise
|
|
772
|
+
* source. Never invents a path that does not already exist.
|
|
773
|
+
*/
|
|
774
|
+
#liveRelocationWritePath(): string | null {
|
|
775
|
+
const relocating = this.#sessionFileRelocating;
|
|
776
|
+
if (!relocating) return null;
|
|
777
|
+
if (this.#storage.existsSync(relocating.dest)) return relocating.dest;
|
|
778
|
+
if (this.#storage.existsSync(relocating.source)) return relocating.source;
|
|
779
|
+
// Rename in flight with neither path visible (rare cross-device edge):
|
|
780
|
+
// fall back to destination so we do not recreate a vacated source.
|
|
781
|
+
return relocating.dest;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Synchronously rewrite the whole file (header + entries) and keep no open
|
|
786
|
+
* writer; the next append re-opens one. `writeTextSync` returns with the
|
|
787
|
+
* bytes in the kernel page cache, so the file is software-crash durable.
|
|
788
|
+
*
|
|
789
|
+
* During {@link moveTo}, writes to the live relocation path (source pre-
|
|
790
|
+
* rename, destination post-rename) rather than always `#sessionFile`, so
|
|
791
|
+
* concurrent completed entries are durable without recreating a vacated source.
|
|
792
|
+
*/
|
|
793
|
+
#rewriteSynchronously(): void {
|
|
794
|
+
if (!this.#persist || !this.#shouldHaveSessionFile()) return;
|
|
795
|
+
const targetPath = this.#liveRelocationWritePath() ?? this.#sessionFile;
|
|
796
|
+
if (!targetPath) return;
|
|
797
|
+
|
|
798
|
+
try {
|
|
799
|
+
const body = this.#fileBody();
|
|
800
|
+
this.#diskEpoch++;
|
|
801
|
+
this.#diskTail = Promise.resolve();
|
|
802
|
+
this.#closeWriterEventually();
|
|
803
|
+
this.#storage.writeTextSync(targetPath, body);
|
|
804
|
+
// Only mark the manager current when writing the active session path.
|
|
805
|
+
// Mid-move writes update the live relocation path; `#sessionFile` is
|
|
806
|
+
// still the pre-repoint source until moveTo repoints it.
|
|
807
|
+
if (!this.#sessionFileRelocating || targetPath === this.#sessionFile) {
|
|
808
|
+
this.#fileIsCurrent = true;
|
|
809
|
+
this.#materializeBreadcrumb();
|
|
810
|
+
this.#rewriteRequired = false;
|
|
811
|
+
this.#hasTitleSlot = true;
|
|
812
|
+
} else {
|
|
813
|
+
// Destination body is current on disk; in-memory still needs a
|
|
814
|
+
// header-cwd rewrite after repoint, but entries are durable.
|
|
815
|
+
this.#fileIsCurrent = false;
|
|
816
|
+
this.#rewriteRequired = true;
|
|
817
|
+
this.#hasTitleSlot = true;
|
|
818
|
+
}
|
|
819
|
+
} catch (err) {
|
|
820
|
+
this.#noteDiskFailure(err);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Rewrite the whole file atomically (temp-write + rename, EPERM-safe) on the
|
|
826
|
+
* disk chain. The body is serialized after the writer is closed. The fence
|
|
827
|
+
* is enabled BEFORE `#closeWriterHandle()` and stays active until the last
|
|
828
|
+
* atomic publish returns, so a sync append landing in the close-yield window
|
|
829
|
+
* cannot open a fresh writer that the pending replacement would then detach
|
|
830
|
+
* from the current JSONL path. A `commitGuard` also prevents a superseding
|
|
831
|
+
* synchronous rewrite from being overwritten by the stale body serialized
|
|
832
|
+
* before it ran.
|
|
833
|
+
*/
|
|
834
|
+
async #rewriteAtomically(): Promise<void> {
|
|
835
|
+
if (!this.#persist || !this.#sessionFile) return;
|
|
836
|
+
|
|
837
|
+
const startEpoch = this.#diskEpoch;
|
|
838
|
+
await this.#scheduleDiskWork(
|
|
839
|
+
async () => {
|
|
840
|
+
if (await this.#runFencedAtomicRewrite(startEpoch)) {
|
|
841
|
+
this.#fileIsCurrent = true;
|
|
842
|
+
this.#materializeBreadcrumb();
|
|
843
|
+
this.#rewriteRequired = false;
|
|
844
|
+
this.#hasTitleSlot = true;
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
{ epoch: startEpoch },
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Shared fenced atomic-rewrite loop used by `#rewriteAtomically` and the
|
|
853
|
+
* `#persistTitleChangeEntry` fallback. Holds `#atomicRewriteActive` across
|
|
854
|
+
* the writer close and the full-file replace, and loops on
|
|
855
|
+
* `#atomicRewriteDirty` so any fenced append that lands during the rewrite
|
|
856
|
+
* is captured before the task resolves. Returns `false` when the disk epoch
|
|
857
|
+
* moved (a superseding synchronous rewrite has taken over) so callers skip
|
|
858
|
+
* their post-publish state updates.
|
|
859
|
+
*/
|
|
860
|
+
async #runFencedAtomicRewrite(epoch: number): Promise<boolean> {
|
|
861
|
+
this.#atomicRewriteFenceEpoch = epoch;
|
|
862
|
+
try {
|
|
863
|
+
do {
|
|
864
|
+
this.#atomicRewriteDirty = false;
|
|
865
|
+
await this.#closeWriterHandle();
|
|
866
|
+
const sessionFile = this.#sessionFile;
|
|
867
|
+
if (!sessionFile) return false;
|
|
868
|
+
if (this.#diskEpoch !== epoch) return false;
|
|
869
|
+
await this.#storage.writeTextAtomic(sessionFile, this.#fileBody(), {
|
|
870
|
+
commitGuard: () => this.#diskEpoch === epoch,
|
|
871
|
+
});
|
|
872
|
+
if (this.#diskEpoch !== epoch) return false;
|
|
873
|
+
} while (this.#atomicRewriteDirty);
|
|
874
|
+
return true;
|
|
875
|
+
} finally {
|
|
876
|
+
// Only relinquish the fence if we still own it. A superseding
|
|
877
|
+
// synchronous rewrite (`flushSync` → `#rewriteSynchronously`) may
|
|
878
|
+
// have reset `#diskTail`, scheduled a fresh atomic task at the new
|
|
879
|
+
// epoch, and that task may have taken ownership of the fence while
|
|
880
|
+
// this stale rewrite was still awaiting storage. Clearing it here
|
|
881
|
+
// unconditionally would strand appends during the newer publish.
|
|
882
|
+
if (this.#atomicRewriteFenceEpoch === epoch) this.#atomicRewriteFenceEpoch = null;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
#appendToSessionFile(entry: SessionEntry): void {
|
|
887
|
+
if (!this.#persist || !this.#sessionFile) return;
|
|
888
|
+
if (this.#atomicEntryBatch) {
|
|
889
|
+
this.#fileIsCurrent = false;
|
|
890
|
+
this.#rewriteRequired = true;
|
|
891
|
+
this.#atomicRewriteDirty = true;
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (this.#diskFailure) throw this.#diskFailure;
|
|
895
|
+
|
|
896
|
+
// Lazy gate: a brand-new session is not written until it has an assistant
|
|
897
|
+
// message (or someone forced creation), so sessions that never produce
|
|
898
|
+
// output never create a file.
|
|
899
|
+
if (!this.#shouldHaveSessionFile()) {
|
|
900
|
+
this.#fileIsCurrent = false;
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Atomic replacement / move window: do not open a fresh append writer that
|
|
905
|
+
// a Windows EPERM replace could detach from the current JSONL path.
|
|
906
|
+
// - moveTo: write a full body to the live relocation path (source pre-
|
|
907
|
+
// rename, destination post-rename) so completed entries are durable
|
|
908
|
+
// without recreating a vacated source.
|
|
909
|
+
// - in-place atomic fence: supersede the pending publish with a
|
|
910
|
+
// synchronous full-body rewrite; bumping `#diskEpoch` abandons the
|
|
911
|
+
// in-flight atomic via its `commitGuard`.
|
|
912
|
+
if (this.#sessionFileRelocating) {
|
|
913
|
+
this.#rewriteSynchronously();
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (this.#atomicRewriteFenceEpoch !== null && this.#atomicRewriteFenceEpoch === this.#diskEpoch) {
|
|
917
|
+
this.#atomicRewriteDirty = true;
|
|
918
|
+
this.#rewriteSynchronously();
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
// Cold/divergent: not on disk yet, or in-memory entries diverged from the
|
|
922
|
+
// file → rewrite the whole file synchronously and keep going.
|
|
923
|
+
if (!this.#fileIsCurrent || this.#rewriteRequired) {
|
|
924
|
+
this.#rewriteSynchronously();
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Hot path: write the entry directly on the writer, outside the async disk
|
|
929
|
+
// chain. Prefer appendSync so write failures latch `#diskFailure` before
|
|
930
|
+
// this call returns (not via a discarded rejected Promise after a later
|
|
931
|
+
// microtask). Callers stay non-throwing here — the core turn loop invokes
|
|
932
|
+
// appendMessage/appendCustomEntry without try/catch; flushSync/close and
|
|
933
|
+
// subsequent appends still throw the latched error. File writers apply
|
|
934
|
+
// each line to the OS page cache before return.
|
|
935
|
+
// A mid-close writer leaves `#writer` undefined, so `#appendWriter` simply
|
|
936
|
+
// opens a fresh append handle and the entry still lands.
|
|
937
|
+
try {
|
|
938
|
+
const writer = this.#appendWriter();
|
|
939
|
+
const line = this.#lineFor(entry);
|
|
940
|
+
if (writer.appendSync) {
|
|
941
|
+
writer.appendSync(line);
|
|
942
|
+
} else {
|
|
943
|
+
void writer.append(line).catch(err => this.#noteDiskFailure(err));
|
|
944
|
+
}
|
|
945
|
+
} catch (err) {
|
|
946
|
+
this.#noteDiskFailure(err);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async #persistTitleChangeEntry(entry: TitleChangeEntry, update: SessionTitleUpdate): Promise<void> {
|
|
951
|
+
if (!this.#persist || !this.#sessionFile) return;
|
|
952
|
+
if (this.#diskFailure) throw this.#diskFailure;
|
|
953
|
+
|
|
954
|
+
if (!this.#shouldHaveSessionFile()) {
|
|
955
|
+
this.#fileIsCurrent = false;
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Title changes use their own asynchronous append path rather than
|
|
960
|
+
// #appendToSessionFile. During move, write the full body (including the
|
|
961
|
+
// title entry) to the live relocation path so a crash mid-move still
|
|
962
|
+
// keeps the title change; the trailing rewrite still updates header cwd.
|
|
963
|
+
if (this.#sessionFileRelocating) {
|
|
964
|
+
this.#rewriteSynchronously();
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (
|
|
969
|
+
!this.#fileIsCurrent ||
|
|
970
|
+
this.#rewriteRequired ||
|
|
971
|
+
!this.#hasTitleSlot ||
|
|
972
|
+
!this.#storage.existsSync(this.#sessionFile)
|
|
973
|
+
) {
|
|
974
|
+
await this.#rewriteAtomically();
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const epoch = this.#diskEpoch;
|
|
979
|
+
const line = this.#lineFor(entry);
|
|
980
|
+
await this.#scheduleDiskWork(
|
|
981
|
+
async () => {
|
|
982
|
+
const sessionFile = this.#sessionFile;
|
|
983
|
+
if (!sessionFile) return;
|
|
984
|
+
try {
|
|
985
|
+
await this.#appendWriter().append(line);
|
|
986
|
+
await this.#storage.updateSessionTitle(sessionFile, update);
|
|
987
|
+
if (this.#diskEpoch === epoch) this.#fileIsCurrent = true;
|
|
988
|
+
} catch {
|
|
989
|
+
if (!(await this.#runFencedAtomicRewrite(epoch))) return;
|
|
990
|
+
this.#clearDiskError();
|
|
991
|
+
this.#fileIsCurrent = true;
|
|
992
|
+
this.#rewriteRequired = false;
|
|
993
|
+
this.#hasTitleSlot = true;
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
{ epoch },
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
#notifyEntryAppended(entry: SessionEntry): void {
|
|
1001
|
+
const callback = this.onEntryAppended;
|
|
1002
|
+
if (callback) {
|
|
1003
|
+
try {
|
|
1004
|
+
callback(entry);
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
logger.warn("collab entry hook failed", { error: String(err) });
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
#resetToNewSession(options?: NewSessionOptions, forcedSessionFile?: string): string | undefined {
|
|
1012
|
+
this.#diskTail = Promise.resolve();
|
|
1013
|
+
this.#clearDiskError();
|
|
1014
|
+
this.#sessionId = mintSessionId();
|
|
1015
|
+
this.#sessionName = undefined;
|
|
1016
|
+
this.#titleSource = undefined;
|
|
1017
|
+
this.#titleUpdatedAt = "";
|
|
1018
|
+
this.#hasTitleSlot = true;
|
|
1019
|
+
|
|
1020
|
+
const timestamp = nowIso();
|
|
1021
|
+
this.#header = {
|
|
1022
|
+
type: "session",
|
|
1023
|
+
version: CURRENT_SESSION_VERSION,
|
|
1024
|
+
id: this.#sessionId,
|
|
1025
|
+
timestamp,
|
|
1026
|
+
cwd: this.#cwd,
|
|
1027
|
+
parentSession: options?.parentSession,
|
|
1028
|
+
providerPromptCacheKey: options?.providerPromptCacheKey,
|
|
1029
|
+
};
|
|
1030
|
+
const workspace = normalizeSessionWorkspace({
|
|
1031
|
+
cwd: this.#cwd,
|
|
1032
|
+
directories: options?.additionalDirectories ?? [],
|
|
1033
|
+
});
|
|
1034
|
+
this.#additionalDirectories = additionalWorkspaceDirectories(workspace);
|
|
1035
|
+
if (this.#additionalDirectories.length > 0) {
|
|
1036
|
+
this.#header.additionalDirectories = [...this.#additionalDirectories];
|
|
1037
|
+
}
|
|
1038
|
+
this.#titleUpdatedAt = timestamp;
|
|
1039
|
+
|
|
1040
|
+
this.#entries = [];
|
|
1041
|
+
this.#index.clear();
|
|
1042
|
+
this.#fileIsCurrent = false;
|
|
1043
|
+
this.#rewriteRequired = false;
|
|
1044
|
+
this.#forceFileCreation = false;
|
|
1045
|
+
this.#draftOnlySessionCleanupArmed = false;
|
|
1046
|
+
this.#turnBudgetTotal = null;
|
|
1047
|
+
this.#turnBudgetHard = false;
|
|
1048
|
+
this.#turnOutputBaseline = 0;
|
|
1049
|
+
this.#turnEvalOutput = 0;
|
|
1050
|
+
this.#artifactManager = null;
|
|
1051
|
+
this.#artifactManagerSessionFile = null;
|
|
1052
|
+
this.#adoptedArtifactManager = null;
|
|
1053
|
+
this.#inMemoryArtifacts = null;
|
|
1054
|
+
this.#inMemoryArtifactCounter = 0;
|
|
1055
|
+
|
|
1056
|
+
if (this.#persist) {
|
|
1057
|
+
this.#sessionFile =
|
|
1058
|
+
forcedSessionFile ??
|
|
1059
|
+
path.join(this.#sessionDir, `${fileSafeTimestamp(timestamp)}_${this.#sessionId}.jsonl`);
|
|
1060
|
+
this.#rememberBreadcrumb(this.#cwd, this.#sessionFile, true);
|
|
1061
|
+
} else {
|
|
1062
|
+
this.#sessionFile = undefined;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return this.#sessionFile;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
#applyEntries(header: SessionHeader, entries: SessionEntry[]): void {
|
|
1069
|
+
this.#header = header;
|
|
1070
|
+
this.#entries = entries;
|
|
1071
|
+
this.#sessionId = header.id;
|
|
1072
|
+
this.#sessionName = header.title;
|
|
1073
|
+
this.#titleSource = header.titleSource;
|
|
1074
|
+
this.#titleUpdatedAt = header.timestamp;
|
|
1075
|
+
this.#index.rebuild(entries);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
#freshEntryFields(): { id: string; parentId: string | null; timestamp: string } {
|
|
1079
|
+
return {
|
|
1080
|
+
id: generateId(this.#index),
|
|
1081
|
+
parentId: this.#index.leafId(),
|
|
1082
|
+
timestamp: nowIso(),
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
#setLeaf(id: string | null): void {
|
|
1087
|
+
this.#index.setLeaf(id);
|
|
1088
|
+
const batch = this.#atomicEntryBatch;
|
|
1089
|
+
if (batch && !batch.collecting) {
|
|
1090
|
+
batch.externalLeafChanged = true;
|
|
1091
|
+
batch.externalLeafId = id;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
#recordEntry(entry: SessionEntry): void {
|
|
1096
|
+
this.#entries.push(entry);
|
|
1097
|
+
this.#index.insert(entry);
|
|
1098
|
+
const batch = this.#atomicEntryBatch;
|
|
1099
|
+
if (batch?.collecting) batch.entryIds.add(entry.id);
|
|
1100
|
+
if (batch && !batch.collecting) {
|
|
1101
|
+
batch.externalLeafChanged = true;
|
|
1102
|
+
batch.externalLeafId = entry.id;
|
|
1103
|
+
}
|
|
1104
|
+
this.#appendToSessionFile(entry);
|
|
1105
|
+
if (batch) batch.deferredNotifications.push(entry);
|
|
1106
|
+
else this.#notifyEntryAppended(entry);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
#rollbackAtomicEntryBatch(batch: AtomicEntryBatch): void {
|
|
1110
|
+
const retainedAncestor = (id: string | null): string | null => {
|
|
1111
|
+
const seen = new Set<string>();
|
|
1112
|
+
while (id && batch.entryIds.has(id) && !seen.has(id)) {
|
|
1113
|
+
seen.add(id);
|
|
1114
|
+
id = this.#index.get(id)?.parentId ?? null;
|
|
1115
|
+
}
|
|
1116
|
+
return id;
|
|
1117
|
+
};
|
|
1118
|
+
const retained = this.#entries.filter(entry => !batch.entryIds.has(entry.id));
|
|
1119
|
+
for (const entry of retained) entry.parentId = retainedAncestor(entry.parentId);
|
|
1120
|
+
const restoredLeaf = retainedAncestor(batch.externalLeafChanged ? batch.externalLeafId : batch.preBatchLeafId);
|
|
1121
|
+
this.#entries = retained;
|
|
1122
|
+
this.#index.rebuild(retained);
|
|
1123
|
+
this.#index.setLeaf(restoredLeaf && this.#index.has(restoredLeaf) ? restoredLeaf : null);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
#draftPath(): string | null {
|
|
1127
|
+
const artifactsDir = this.getArtifactsDir();
|
|
1128
|
+
return artifactsDir ? path.join(artifactsDir, "draft.txt") : null;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
#draftOnlySessionMarkerPath(): string | null {
|
|
1132
|
+
const artifactsDir = this.getArtifactsDir();
|
|
1133
|
+
return artifactsDir ? path.join(artifactsDir, DRAFT_ONLY_SESSION_MARKER) : null;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
#hasDraftOnlySessionMarker(): boolean {
|
|
1137
|
+
const markerPath = this.#draftOnlySessionMarkerPath();
|
|
1138
|
+
return markerPath !== null && this.#storage.existsSync(markerPath);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
async #writeDraftOnlySessionMarker(): Promise<void> {
|
|
1142
|
+
const markerPath = this.#draftOnlySessionMarkerPath();
|
|
1143
|
+
if (!markerPath) return;
|
|
1144
|
+
await this.#storage.writeText(markerPath, "");
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
async #clearDraftOnlySessionMarker(): Promise<void> {
|
|
1148
|
+
const markerPath = this.#draftOnlySessionMarkerPath();
|
|
1149
|
+
if (!markerPath) return;
|
|
1150
|
+
try {
|
|
1151
|
+
await this.#storage.unlink(markerPath);
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
if (!isEnoent(err)) throw err;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
#artifactManagerForSession(): ArtifactManager | null {
|
|
1158
|
+
if (this.#adoptedArtifactManager) return this.#adoptedArtifactManager;
|
|
1159
|
+
|
|
1160
|
+
const sessionFile = this.#sessionFile;
|
|
1161
|
+
if (!sessionFile) {
|
|
1162
|
+
this.#artifactManager = null;
|
|
1163
|
+
this.#artifactManagerSessionFile = null;
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (this.#artifactManager && this.#artifactManagerSessionFile === sessionFile) return this.#artifactManager;
|
|
1168
|
+
|
|
1169
|
+
this.#artifactManager = new ArtifactManager(sessionFile.slice(0, -JSONL_SUFFIX_LENGTH));
|
|
1170
|
+
this.#artifactManagerSessionFile = sessionFile;
|
|
1171
|
+
return this.#artifactManager;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
#notifySessionNameListeners(): void {
|
|
1175
|
+
for (const callback of [...this.#sessionNameChangedCallbacks]) {
|
|
1176
|
+
try {
|
|
1177
|
+
callback();
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
logger.warn("SessionManager: session name change hook failed", { error: String(err) });
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
static #cleanTitle(raw: string): string {
|
|
1185
|
+
return raw
|
|
1186
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
|
|
1187
|
+
.replace(/ +/g, " ")
|
|
1188
|
+
.trim();
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/** Puts a binary blob into the blob store and returns the blob reference. */
|
|
1192
|
+
async putBlob(data: Buffer, options?: BlobPutOptions): Promise<BlobPutResult> {
|
|
1193
|
+
return this.#blobs.put(data, options);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/** Synchronous variant of {@link putBlob} for rebuild-only render paths. */
|
|
1197
|
+
putBlobSync(data: Buffer, options?: BlobPutOptions): BlobPutResult {
|
|
1198
|
+
return this.#blobs.putSync(data, options);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
captureState(): SessionManagerStateSnapshot {
|
|
1202
|
+
return {
|
|
1203
|
+
cwd: this.#cwd,
|
|
1204
|
+
sessionDir: this.#sessionDir,
|
|
1205
|
+
sessionId: this.#sessionId,
|
|
1206
|
+
sessionName: this.#sessionName,
|
|
1207
|
+
titleSource: this.#titleSource,
|
|
1208
|
+
titleUpdatedAt: this.#titleUpdatedAt,
|
|
1209
|
+
hasTitleSlot: this.#hasTitleSlot,
|
|
1210
|
+
sessionFile: this.#sessionFile,
|
|
1211
|
+
onDisk: this.#fileIsCurrent,
|
|
1212
|
+
needsRewrite: this.#rewriteRequired,
|
|
1213
|
+
draftOnlySessionCleanupArmed: this.#draftOnlySessionCleanupArmed,
|
|
1214
|
+
// Snapshot header + entries by reference: switch/reload replaces the
|
|
1215
|
+
// active header/array wholesale, so rollback needs no deep clone.
|
|
1216
|
+
header: this.#header,
|
|
1217
|
+
entries: [...this.#entries],
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Create an independent manager for the current logical session and branch.
|
|
1223
|
+
* The clone shares the storage backend but owns its entry index and writer, so
|
|
1224
|
+
* callers can finish session-owned work after this manager switches elsewhere.
|
|
1225
|
+
* Set `persist` false when the original session is intentionally being dropped.
|
|
1226
|
+
*/
|
|
1227
|
+
cloneCurrentSession(options?: { persist?: boolean }): SessionManager {
|
|
1228
|
+
const persist = options?.persist ?? this.#persist;
|
|
1229
|
+
const clone = new SessionManager(this.#cwd, this.#sessionDir, persist, this.#storage);
|
|
1230
|
+
clone.#suppressBreadcrumb = true;
|
|
1231
|
+
clone.restoreState(this.captureState());
|
|
1232
|
+
if (!persist) {
|
|
1233
|
+
clone.#sessionFile = undefined;
|
|
1234
|
+
clone.#fileIsCurrent = false;
|
|
1235
|
+
clone.#rewriteRequired = false;
|
|
1236
|
+
clone.#forceFileCreation = false;
|
|
1237
|
+
}
|
|
1238
|
+
return clone;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
restoreState(snapshot: SessionManagerStateSnapshot): void {
|
|
1242
|
+
this.#closeWriterEventually();
|
|
1243
|
+
this.#diskTail = Promise.resolve();
|
|
1244
|
+
this.#clearDiskError();
|
|
1245
|
+
|
|
1246
|
+
this.#cwd = snapshot.cwd;
|
|
1247
|
+
this.#sessionDir = snapshot.sessionDir;
|
|
1248
|
+
this.#sessionFile = snapshot.sessionFile;
|
|
1249
|
+
this.#fileIsCurrent = snapshot.onDisk;
|
|
1250
|
+
this.#rewriteRequired = snapshot.needsRewrite;
|
|
1251
|
+
this.#forceFileCreation = snapshot.onDisk;
|
|
1252
|
+
this.#draftOnlySessionCleanupArmed = snapshot.draftOnlySessionCleanupArmed;
|
|
1253
|
+
this.#applyEntries(snapshot.header, [...snapshot.entries]);
|
|
1254
|
+
this.#additionalDirectories = snapshot.header.additionalDirectories ?? [];
|
|
1255
|
+
this.#sessionName = snapshot.sessionName;
|
|
1256
|
+
this.#titleSource = snapshot.titleSource;
|
|
1257
|
+
this.#titleUpdatedAt = snapshot.titleUpdatedAt;
|
|
1258
|
+
this.#hasTitleSlot = snapshot.hasTitleSlot;
|
|
1259
|
+
this.#artifactManager = null;
|
|
1260
|
+
this.#artifactManagerSessionFile = null;
|
|
1261
|
+
this.#adoptedArtifactManager = null;
|
|
1262
|
+
|
|
1263
|
+
if (this.#sessionFile) this.#rememberBreadcrumb(this.#cwd, this.#sessionFile);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/** Switch to a different session file (resume / branch). */
|
|
1267
|
+
async setSessionFile(sessionFile: string): Promise<void> {
|
|
1268
|
+
await this.#drainAndCloseWriter();
|
|
1269
|
+
this.#clearDiskError();
|
|
1270
|
+
this.#draftOnlySessionCleanupArmed = false;
|
|
1271
|
+
|
|
1272
|
+
const resolvedSessionFile = path.resolve(sessionFile);
|
|
1273
|
+
this.#sessionFile = resolvedSessionFile;
|
|
1274
|
+
this.#rememberBreadcrumb(this.#cwd, resolvedSessionFile);
|
|
1275
|
+
|
|
1276
|
+
const titleSlot = await readTitleSlotFromFile(resolvedSessionFile, this.#storage);
|
|
1277
|
+
const fileEntries = await loadEntriesFromFile(resolvedSessionFile, this.#storage);
|
|
1278
|
+
if (fileEntries.length === 0) {
|
|
1279
|
+
// Explicit but empty/missing path (e.g. --session flag): start fresh but
|
|
1280
|
+
// keep the requested path and materialize the header immediately.
|
|
1281
|
+
this.#resetToNewSession(undefined, resolvedSessionFile);
|
|
1282
|
+
this.#forceFileCreation = true;
|
|
1283
|
+
await this.#rewriteAtomically();
|
|
1284
|
+
this.#fileIsCurrent = true;
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const migrated = migrateToCurrentVersion(fileEntries);
|
|
1289
|
+
await resolveBlobRefsInEntries(fileEntries, this.#blobs);
|
|
1290
|
+
// loadEntriesFromFile guarantees entries[0] is a valid session header.
|
|
1291
|
+
const header = fileEntries[0] as SessionHeader;
|
|
1292
|
+
|
|
1293
|
+
// Adopt the loaded session's working directory. Sessions live in a dir
|
|
1294
|
+
// keyed by their cwd, so resuming a session from another project must
|
|
1295
|
+
// re-point cwd/sessionDir at that project — unless that project directory
|
|
1296
|
+
// no longer exists on disk, in which case adopting it (and the process
|
|
1297
|
+
// chdir interactive mode then performs) would fail with ENOENT. Keep the
|
|
1298
|
+
// current cwd so the resumed session stays where the user already is.
|
|
1299
|
+
const headerCwd = header.cwd ? path.resolve(header.cwd) : undefined;
|
|
1300
|
+
if (headerCwd && headerCwd !== path.resolve(this.#cwd) && (await directoryExists(headerCwd))) {
|
|
1301
|
+
this.#cwd = headerCwd;
|
|
1302
|
+
this.#sessionDir = path.dirname(resolvedSessionFile);
|
|
1303
|
+
this.#rememberBreadcrumb(this.#cwd, resolvedSessionFile);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
this.#applyEntries(header, fileEntries.slice(1) as SessionEntry[]);
|
|
1307
|
+
this.#additionalDirectories = header.additionalDirectories ?? [];
|
|
1308
|
+
this.#titleUpdatedAt = titleSlot?.updatedAt ?? header.timestamp;
|
|
1309
|
+
this.#hasTitleSlot = titleSlot !== undefined;
|
|
1310
|
+
this.#fileIsCurrent = true;
|
|
1311
|
+
this.#rewriteRequired = migrated;
|
|
1312
|
+
this.#forceFileCreation = true;
|
|
1313
|
+
this.#artifactManager = null;
|
|
1314
|
+
this.#artifactManagerSessionFile = null;
|
|
1315
|
+
|
|
1316
|
+
if (this.sanitizeLoadedOpenAIResponsesReplayMetadata()) this.#rewriteRequired = true;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/** Start a new session. Drains and closes any existing writer first. */
|
|
1320
|
+
async newSession(options?: NewSessionOptions): Promise<string | undefined> {
|
|
1321
|
+
await this.#drainAndCloseWriter();
|
|
1322
|
+
return this.#resetToNewSession(options);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/** Delete a session file and its artifact directory. ENOENT is treated as success. */
|
|
1326
|
+
async dropSession(sessionPath: string): Promise<void> {
|
|
1327
|
+
await this.#drainAndCloseWriter();
|
|
1328
|
+
try {
|
|
1329
|
+
await this.#storage.deleteSessionWithArtifacts(sessionPath);
|
|
1330
|
+
} catch (err) {
|
|
1331
|
+
if (!isEnoent(err)) throw err;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* Fork the current session into a new file with the same entries.
|
|
1337
|
+
* @returns the old and new session file paths, or undefined when not persisting.
|
|
1338
|
+
*/
|
|
1339
|
+
async fork(): Promise<{ oldSessionFile: string; newSessionFile: string } | undefined> {
|
|
1340
|
+
if (!this.#persist || !this.#sessionFile) return undefined;
|
|
1341
|
+
|
|
1342
|
+
const oldSessionFile = this.#sessionFile;
|
|
1343
|
+
const parentSessionId = this.#sessionId;
|
|
1344
|
+
await this.#drainAndCloseWriter();
|
|
1345
|
+
this.#clearDiskError();
|
|
1346
|
+
|
|
1347
|
+
const timestamp = nowIso();
|
|
1348
|
+
this.#sessionId = mintSessionId();
|
|
1349
|
+
this.#sessionFile = path.join(this.#sessionDir, `${fileSafeTimestamp(timestamp)}_${this.#sessionId}.jsonl`);
|
|
1350
|
+
this.#header = {
|
|
1351
|
+
type: "session",
|
|
1352
|
+
version: CURRENT_SESSION_VERSION,
|
|
1353
|
+
id: this.#sessionId,
|
|
1354
|
+
title: this.#header.title ?? this.#sessionName,
|
|
1355
|
+
titleSource: this.#header.titleSource ?? this.#titleSource,
|
|
1356
|
+
timestamp,
|
|
1357
|
+
cwd: this.#cwd,
|
|
1358
|
+
additionalDirectories: this.#additionalDirectories.length > 0 ? [...this.#additionalDirectories] : undefined,
|
|
1359
|
+
parentSession: parentSessionId,
|
|
1360
|
+
providerPromptCacheKey: this.#header.providerPromptCacheKey ?? parentSessionId,
|
|
1361
|
+
};
|
|
1362
|
+
this.#sessionName = this.#header.title;
|
|
1363
|
+
this.#titleSource = this.#header.titleSource;
|
|
1364
|
+
this.#titleUpdatedAt = timestamp;
|
|
1365
|
+
this.#hasTitleSlot = true;
|
|
1366
|
+
this.#fileIsCurrent = false;
|
|
1367
|
+
this.#rewriteRequired = false;
|
|
1368
|
+
this.#forceFileCreation = true;
|
|
1369
|
+
this.#draftOnlySessionCleanupArmed = false;
|
|
1370
|
+
this.#artifactManager = null;
|
|
1371
|
+
this.#artifactManagerSessionFile = null;
|
|
1372
|
+
this.#rememberBreadcrumb(this.#cwd, this.#sessionFile);
|
|
1373
|
+
|
|
1374
|
+
await this.#rewriteAtomically();
|
|
1375
|
+
return { oldSessionFile, newSessionFile: this.#sessionFile };
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/**
|
|
1379
|
+
* Move the session to a new working directory: relocate the session file and
|
|
1380
|
+
* artifacts on disk, update internal references, and rewrite the header cwd.
|
|
1381
|
+
*/
|
|
1382
|
+
async moveTo(newCwd: string, targetSessionDir?: string): Promise<void> {
|
|
1383
|
+
const resolvedCwd = path.resolve(newCwd);
|
|
1384
|
+
const resolvedTargetDir = targetSessionDir ? path.resolve(targetSessionDir) : undefined;
|
|
1385
|
+
if (
|
|
1386
|
+
resolvedCwd === path.resolve(this.#cwd) &&
|
|
1387
|
+
(!resolvedTargetDir || resolvedTargetDir === path.resolve(this.#sessionDir))
|
|
1388
|
+
) {
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
const managedRoot = resolveManagedSessionRoot(this.#sessionDir, this.#cwd);
|
|
1393
|
+
const nextSessionDir =
|
|
1394
|
+
resolvedTargetDir ??
|
|
1395
|
+
(managedRoot
|
|
1396
|
+
? computeDefaultSessionDir(resolvedCwd, this.#storage, managedRoot)
|
|
1397
|
+
: computeDefaultSessionDir(resolvedCwd, this.#storage));
|
|
1398
|
+
|
|
1399
|
+
let sessionFileExisted = false;
|
|
1400
|
+
// Track source+dest for concurrent completed appends during relocation
|
|
1401
|
+
// (see `#sessionFileRelocating`). Existence of either path decides the
|
|
1402
|
+
// live write target — not a `#diskEpoch` bump, which would cancel any
|
|
1403
|
+
// disk task already queued at the current epoch (e.g. a header-only
|
|
1404
|
+
// `ensureOnDisk()` materializing rewrite) before the drain below runs it.
|
|
1405
|
+
if (this.#persist && this.#sessionFile) {
|
|
1406
|
+
const source = this.#sessionFile;
|
|
1407
|
+
const dest = path.join(nextSessionDir, path.basename(source));
|
|
1408
|
+
this.#sessionFileRelocating = { source, dest };
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
try {
|
|
1412
|
+
if (this.#persist && this.#sessionFile) {
|
|
1413
|
+
this.#storage.ensureDirSync(nextSessionDir);
|
|
1414
|
+
await this.#drainAndCloseWriter();
|
|
1415
|
+
this.#clearDiskError();
|
|
1416
|
+
|
|
1417
|
+
const oldSessionFile = this.#sessionFile;
|
|
1418
|
+
const newSessionFile = path.join(nextSessionDir, path.basename(oldSessionFile));
|
|
1419
|
+
const oldArtifactsDir = artifactsDirectoryFor(oldSessionFile)!;
|
|
1420
|
+
const newArtifactsDir = artifactsDirectoryFor(newSessionFile)!;
|
|
1421
|
+
const sessionPathChanged = path.resolve(oldSessionFile) !== path.resolve(newSessionFile);
|
|
1422
|
+
const artifactPathChanged = path.resolve(oldArtifactsDir) !== path.resolve(newArtifactsDir);
|
|
1423
|
+
sessionFileExisted = this.#storage.existsSync(oldSessionFile);
|
|
1424
|
+
|
|
1425
|
+
let sessionMoved = false;
|
|
1426
|
+
let artifactsMoved = false;
|
|
1427
|
+
|
|
1428
|
+
try {
|
|
1429
|
+
if (sessionFileExisted && sessionPathChanged) {
|
|
1430
|
+
await fs.promises.rename(oldSessionFile, newSessionFile);
|
|
1431
|
+
sessionMoved = true;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
if (artifactPathChanged) {
|
|
1435
|
+
try {
|
|
1436
|
+
const artifactStat = await fs.promises.stat(oldArtifactsDir);
|
|
1437
|
+
if (artifactStat.isDirectory()) {
|
|
1438
|
+
await fs.promises.rename(oldArtifactsDir, newArtifactsDir);
|
|
1439
|
+
artifactsMoved = true;
|
|
1440
|
+
}
|
|
1441
|
+
} catch (err) {
|
|
1442
|
+
if (!isEnoent(err)) throw err;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
} catch (err) {
|
|
1446
|
+
if (artifactsMoved) {
|
|
1447
|
+
try {
|
|
1448
|
+
await fs.promises.rename(newArtifactsDir, oldArtifactsDir);
|
|
1449
|
+
} catch (rollbackErr) {
|
|
1450
|
+
throw new Error(
|
|
1451
|
+
`Failed to move artifacts and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (sessionMoved) {
|
|
1457
|
+
try {
|
|
1458
|
+
await fs.promises.rename(newSessionFile, oldSessionFile);
|
|
1459
|
+
} catch (rollbackErr) {
|
|
1460
|
+
throw new Error(
|
|
1461
|
+
`Failed to move session file and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
throw err;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
if (sessionFileExisted && sessionPathChanged) {
|
|
1470
|
+
this.#header.previousSessionFiles = [
|
|
1471
|
+
...new Set([...(this.#header.previousSessionFiles ?? []), path.resolve(oldSessionFile)]),
|
|
1472
|
+
];
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
this.#sessionFile = newSessionFile;
|
|
1476
|
+
this.#artifactManager = null;
|
|
1477
|
+
this.#artifactManagerSessionFile = null;
|
|
1478
|
+
// Path is repointed; hot-path appends may use `#sessionFile` again.
|
|
1479
|
+
this.#sessionFileRelocating = null;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
this.#cwd = resolvedCwd;
|
|
1483
|
+
this.#sessionDir = nextSessionDir;
|
|
1484
|
+
this.#header.cwd = resolvedCwd;
|
|
1485
|
+
// Re-filter additional roots: the new cwd may have been an additional root,
|
|
1486
|
+
// or it may now contain/subsume one. Re-normalize to keep the invariant
|
|
1487
|
+
// that cwd is never also listed as an additional directory.
|
|
1488
|
+
if (this.#additionalDirectories.length > 0) {
|
|
1489
|
+
this.#additionalDirectories = this.#additionalDirectories.filter(d => d !== resolvedCwd);
|
|
1490
|
+
this.#header.additionalDirectories =
|
|
1491
|
+
this.#additionalDirectories.length > 0 ? this.#additionalDirectories : undefined;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// Rewrite at the new location when the file already existed (update cwd) or
|
|
1495
|
+
// there is in-memory output worth materializing; otherwise stay lazy.
|
|
1496
|
+
const hasAssistant = this.#historyContainsAssistantMessage();
|
|
1497
|
+
if (this.#persist && this.#sessionFile && (sessionFileExisted || hasAssistant)) {
|
|
1498
|
+
this.#forceFileCreation = true;
|
|
1499
|
+
await this.#rewriteAtomically();
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
if (this.#sessionFile) this.#rememberBreadcrumb(resolvedCwd, this.#sessionFile);
|
|
1503
|
+
} finally {
|
|
1504
|
+
this.#sessionFileRelocating = null;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Force the session onto disk even with no assistant message yet (ACP
|
|
1510
|
+
* session/new must create a discoverable file immediately).
|
|
1511
|
+
*/
|
|
1512
|
+
async ensureOnDisk(): Promise<void> {
|
|
1513
|
+
if (!this.#persist || !this.#sessionFile) return;
|
|
1514
|
+
this.#forceFileCreation = true;
|
|
1515
|
+
if (this.#fileIsCurrent && !this.#rewriteRequired) return;
|
|
1516
|
+
await this.#rewriteAtomically();
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/** Persist this session's transcript as a newly identified OMP session. */
|
|
1520
|
+
async persistCopy(
|
|
1521
|
+
options?: { sessionDir?: string; suppressBreadcrumb?: boolean },
|
|
1522
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
1523
|
+
): Promise<SessionManager> {
|
|
1524
|
+
const sessionDir = options?.sessionDir ?? SessionManager.getDefaultSessionDir(this.#cwd, undefined, storage);
|
|
1525
|
+
const manager = new SessionManager(this.#cwd, sessionDir, true, storage);
|
|
1526
|
+
manager.#suppressBreadcrumb = options?.suppressBreadcrumb === true;
|
|
1527
|
+
manager.#resetToNewSession();
|
|
1528
|
+
manager.#sessionName = this.#sessionName;
|
|
1529
|
+
manager.#titleSource = this.#titleSource;
|
|
1530
|
+
manager.#titleUpdatedAt = this.#titleUpdatedAt;
|
|
1531
|
+
manager.#header.title = this.#sessionName;
|
|
1532
|
+
manager.#header.titleSource = this.#titleSource;
|
|
1533
|
+
manager.#additionalDirectories = [...this.#additionalDirectories];
|
|
1534
|
+
manager.#header.additionalDirectories =
|
|
1535
|
+
manager.#additionalDirectories.length > 0 ? [...manager.#additionalDirectories] : undefined;
|
|
1536
|
+
manager.#entries = structuredClone(this.#entries);
|
|
1537
|
+
manager.#index.rebuild(manager.#entries);
|
|
1538
|
+
manager.#forceFileCreation = true;
|
|
1539
|
+
await manager.#rewriteAtomically();
|
|
1540
|
+
return manager;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
/**
|
|
1544
|
+
* Stage a synchronous group of entry appends and publish the resulting full
|
|
1545
|
+
* journal with one atomic replace. A failed publish removes only the staged
|
|
1546
|
+
* entries, preserves/reparents entries appended concurrently, restores the
|
|
1547
|
+
* prior durable file view, and clears the failed writer latch for retry.
|
|
1548
|
+
*
|
|
1549
|
+
* The callback MUST be synchronous.
|
|
1550
|
+
*/
|
|
1551
|
+
appendEntriesAtomically<T>(append: () => T): Promise<T> {
|
|
1552
|
+
return this.#withAtomicPersistenceLock(() => this.#appendEntriesAtomicallyLocked(append));
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
async #appendEntriesAtomicallyLocked<T>(append: () => T): Promise<T> {
|
|
1556
|
+
if (!this.#persist || !this.#sessionFile) return append();
|
|
1557
|
+
if (this.#atomicEntryBatch) throw new Error("Atomic persistence lock ownership was violated.");
|
|
1558
|
+
try {
|
|
1559
|
+
await this.ensureOnDisk();
|
|
1560
|
+
await this.flush();
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
const operationError = toError(error);
|
|
1563
|
+
await this.#authoritativelyRewriteCurrentStateLocked(operationError);
|
|
1564
|
+
this.#notifyDurableEntries();
|
|
1565
|
+
throw error;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
const batch: AtomicEntryBatch = {
|
|
1569
|
+
collecting: true,
|
|
1570
|
+
entryIds: new Set(),
|
|
1571
|
+
deferredNotifications: [],
|
|
1572
|
+
preBatchLeafId: this.#index.leafId(),
|
|
1573
|
+
externalLeafChanged: false,
|
|
1574
|
+
externalLeafId: null,
|
|
1575
|
+
};
|
|
1576
|
+
this.#atomicEntryBatch = batch;
|
|
1577
|
+
let result!: T;
|
|
1578
|
+
try {
|
|
1579
|
+
try {
|
|
1580
|
+
result = append();
|
|
1581
|
+
} finally {
|
|
1582
|
+
batch.collecting = false;
|
|
1583
|
+
}
|
|
1584
|
+
await this.#rewriteAtomically();
|
|
1585
|
+
if (!this.#fileIsCurrent || this.#rewriteRequired) {
|
|
1586
|
+
throw new Error("Atomic session batch was superseded before commit.");
|
|
1587
|
+
}
|
|
1588
|
+
this.#atomicEntryBatch = undefined;
|
|
1589
|
+
this.#notifyDurableEntries(batch.deferredNotifications);
|
|
1590
|
+
return result;
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
batch.collecting = false;
|
|
1593
|
+
const operationError = toError(error);
|
|
1594
|
+
this.#rollbackAtomicEntryBatch(batch);
|
|
1595
|
+
try {
|
|
1596
|
+
await this.#authoritativelyRewriteCurrentStateLocked(operationError);
|
|
1597
|
+
} catch (repairError) {
|
|
1598
|
+
const retainedNotifications = batch.deferredNotifications.filter(entry => !batch.entryIds.has(entry.id));
|
|
1599
|
+
this.#pendingDurabilityNotifications.push(...retainedNotifications);
|
|
1600
|
+
this.#atomicEntryBatch = undefined;
|
|
1601
|
+
this.#fileIsCurrent = false;
|
|
1602
|
+
this.#rewriteRequired = true;
|
|
1603
|
+
if (repairError instanceof SessionPersistenceIndeterminateError) throw repairError;
|
|
1604
|
+
throw this.#latchIndeterminate(operationError, [toError(repairError)]);
|
|
1605
|
+
}
|
|
1606
|
+
const retainedNotifications = batch.deferredNotifications.filter(entry => !batch.entryIds.has(entry.id));
|
|
1607
|
+
this.#atomicEntryBatch = undefined;
|
|
1608
|
+
this.#notifyDurableEntries(retainedNotifications);
|
|
1609
|
+
throw error;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* Replace an uncertain append tail with the authoritative in-memory journal.
|
|
1615
|
+
* Callers must only use this for monotonic recovery where every retained
|
|
1616
|
+
* entry remains intended (for example, an explicit terminal tombstone).
|
|
1617
|
+
*/
|
|
1618
|
+
recoverPersistenceFromCurrentState(): Promise<void> {
|
|
1619
|
+
return this.#withAtomicPersistenceLock(async () => {
|
|
1620
|
+
if (!this.#persist || !this.#sessionFile) return;
|
|
1621
|
+
if (this.#atomicEntryBatch) throw new Error("Atomic persistence lock ownership was violated.");
|
|
1622
|
+
const operationError =
|
|
1623
|
+
this.#diskFailure ?? new Error("Authoritative session persistence recovery was requested.");
|
|
1624
|
+
await this.#authoritativelyRewriteCurrentStateLocked(operationError);
|
|
1625
|
+
this.#notifyDurableEntries();
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/** Flush pending writes. Call before switching sessions or on shutdown. */
|
|
1630
|
+
async flush(): Promise<void> {
|
|
1631
|
+
if (!this.#persist || !this.#sessionFile) return;
|
|
1632
|
+
await this.#scheduleDiskWork(async () => {
|
|
1633
|
+
if (this.#writer?.isOpen()) await this.#writer.flush();
|
|
1634
|
+
});
|
|
1635
|
+
// Drain any fire-and-forget backing writes (e.g. `writeTextSync` queued
|
|
1636
|
+
// on IndexedSessionStorage during `flushSync`) so callers relying on
|
|
1637
|
+
// flush() see the write durably visible to readers.
|
|
1638
|
+
await this.#storage.drain();
|
|
1639
|
+
if (this.#diskFailure) throw this.#diskFailure;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/**
|
|
1643
|
+
* Synchronously makes the current append-only session durable. Avoid rewriting
|
|
1644
|
+
* an already-current file: large restored sessions can contain GiB of compacted
|
|
1645
|
+
* history, and Ctrl+C must not rebuild the whole JSONL string just to flush.
|
|
1646
|
+
*/
|
|
1647
|
+
flushSync(): void {
|
|
1648
|
+
if (!this.#persist || !this.#sessionFile) return;
|
|
1649
|
+
if (this.#atomicEntryBatch) throw new Error("Cannot synchronously flush during an atomic session batch.");
|
|
1650
|
+
if (this.#diskFailure) throw this.#diskFailure;
|
|
1651
|
+
if (this.#fileIsCurrent && !this.#rewriteRequired) {
|
|
1652
|
+
this.#writer?.flushSync?.();
|
|
1653
|
+
const writerError = this.#writer?.getError();
|
|
1654
|
+
if (writerError) throw writerError;
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
this.#rewriteSynchronously();
|
|
1658
|
+
if (this.#diskFailure) throw this.#diskFailure;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* Drop only session files that this manager saw materialized for a draft and
|
|
1663
|
+
* that still contain no durable conversation or extension state. Explicit
|
|
1664
|
+
* ensureOnDisk() records (ACP session/new, handoff) stay resumable.
|
|
1665
|
+
*/
|
|
1666
|
+
async #dropIfEmptyAndNoDraft(): Promise<void> {
|
|
1667
|
+
if (!this.#draftOnlySessionCleanupArmed) return;
|
|
1668
|
+
const sessionFile = this.#sessionFile;
|
|
1669
|
+
if (!sessionFile || !this.#storage.existsSync(sessionFile)) {
|
|
1670
|
+
this.#draftOnlySessionCleanupArmed = false;
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
const draftPath = this.#draftPath();
|
|
1674
|
+
if (draftPath && this.#storage.existsSync(draftPath)) return;
|
|
1675
|
+
if (!this.#entries.every(isDraftOnlyMetadataEntry)) {
|
|
1676
|
+
await this.#clearDraftOnlySessionMarker();
|
|
1677
|
+
this.#draftOnlySessionCleanupArmed = false;
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
try {
|
|
1681
|
+
await this.#storage.deleteSessionWithArtifacts(sessionFile);
|
|
1682
|
+
this.#fileIsCurrent = false;
|
|
1683
|
+
this.#forceFileCreation = false;
|
|
1684
|
+
this.#hasTitleSlot = false;
|
|
1685
|
+
this.#draftOnlySessionCleanupArmed = false;
|
|
1686
|
+
} catch (err) {
|
|
1687
|
+
if (!isEnoent(err)) {
|
|
1688
|
+
logger.warn("Failed to drop empty session on close", { sessionFile, error: String(err) });
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/** Flush, then close the append writer. */
|
|
1694
|
+
async close(): Promise<void> {
|
|
1695
|
+
if (!this.#persist) return;
|
|
1696
|
+
await this.#scheduleDiskWork(async () => {
|
|
1697
|
+
const hadWriter = this.#writer !== undefined;
|
|
1698
|
+
await this.#closeWriterHandle();
|
|
1699
|
+
if (hadWriter || (this.#sessionFile && this.#storage.existsSync(this.#sessionFile)))
|
|
1700
|
+
this.#fileIsCurrent = true;
|
|
1701
|
+
});
|
|
1702
|
+
await this.#dropIfEmptyAndNoDraft();
|
|
1703
|
+
// Wait for any queued backing writes (IndexedSessionStorage per-path
|
|
1704
|
+
// tail) to become durable so a graceful shutdown does not exit while
|
|
1705
|
+
// a fire-and-forget publish is still on the wire.
|
|
1706
|
+
await this.#storage.drain();
|
|
1707
|
+
if (this.#diskFailure) throw this.#diskFailure;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
getCwd(): string {
|
|
1711
|
+
return this.#cwd;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
/** Additional workspace directories beyond cwd (multi-root), absolute and normalized. */
|
|
1715
|
+
getAdditionalDirectories(): string[] {
|
|
1716
|
+
return [...this.#additionalDirectories];
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* Persist a workspace-directory change to the session header. Respects the
|
|
1721
|
+
* lazy-persistence gate: a session with no durable output yet keeps the
|
|
1722
|
+
* change in memory (the header lands with the first real write), so seeding
|
|
1723
|
+
* roots at launch never materializes an empty resumable session file.
|
|
1724
|
+
*/
|
|
1725
|
+
async #persistWorkspaceDirectoriesChange(): Promise<void> {
|
|
1726
|
+
if (!this.#persist || !this.#sessionFile || !this.#shouldHaveSessionFile()) return;
|
|
1727
|
+
this.#rewriteRequired = true;
|
|
1728
|
+
await this.#rewriteAtomically();
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/**
|
|
1732
|
+
* Add a workspace directory. Normalizes (relative to cwd), dedupes, rejects
|
|
1733
|
+
* the cwd itself, persists to the session header, and triggers an atomic
|
|
1734
|
+
* rewrite so the change survives a crash. Returns the resolved absolute
|
|
1735
|
+
* path or `null` when the directory was already present (no-op).
|
|
1736
|
+
*/
|
|
1737
|
+
async addWorkspaceDirectory(directory: string): Promise<string | null> {
|
|
1738
|
+
const resolved = normalizeWorkspaceDirectory(directory, this.#cwd);
|
|
1739
|
+
if (resolved === path.resolve(this.#cwd)) {
|
|
1740
|
+
throw new Error("The current working directory is already the primary workspace root.");
|
|
1741
|
+
}
|
|
1742
|
+
if (this.#additionalDirectories.includes(resolved)) return null;
|
|
1743
|
+
this.#additionalDirectories = [...this.#additionalDirectories, resolved];
|
|
1744
|
+
this.#header.additionalDirectories = this.#additionalDirectories;
|
|
1745
|
+
await this.#persistWorkspaceDirectoriesChange();
|
|
1746
|
+
return resolved;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
/**
|
|
1750
|
+
* Remove a workspace directory by absolute or cwd-relative path. Persists
|
|
1751
|
+
* the trimmed header. Returns the resolved path that was removed, or
|
|
1752
|
+
* `null` when the directory was not an additional root (no-op).
|
|
1753
|
+
*/
|
|
1754
|
+
async removeWorkspaceDirectory(directory: string): Promise<string | null> {
|
|
1755
|
+
const resolved = normalizeWorkspaceDirectory(directory, this.#cwd);
|
|
1756
|
+
const idx = this.#additionalDirectories.findIndex(p => path.resolve(p) === resolved);
|
|
1757
|
+
if (idx === -1) return null;
|
|
1758
|
+
this.#additionalDirectories = this.#additionalDirectories.filter((_, i) => i !== idx);
|
|
1759
|
+
if (this.#additionalDirectories.length === 0) {
|
|
1760
|
+
this.#header.additionalDirectories = undefined;
|
|
1761
|
+
} else {
|
|
1762
|
+
this.#header.additionalDirectories = this.#additionalDirectories;
|
|
1763
|
+
}
|
|
1764
|
+
await this.#persistWorkspaceDirectoriesChange();
|
|
1765
|
+
return resolved;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/** Seed additional directories from settings or a passed list. Also called on resumed sessions with --add-dir; persists the updated header when the session file is already durable. No-op when the normalized list is unchanged (avoids rewriting large session files on every startup). */
|
|
1769
|
+
async setAdditionalDirectories(directories: string[]): Promise<void> {
|
|
1770
|
+
const workspace = normalizeSessionWorkspace({ cwd: this.#cwd, directories });
|
|
1771
|
+
const next = additionalWorkspaceDirectories(workspace);
|
|
1772
|
+
if (
|
|
1773
|
+
next.length === this.#additionalDirectories.length &&
|
|
1774
|
+
next.every((d, i) => d === this.#additionalDirectories[i])
|
|
1775
|
+
) {
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
this.#additionalDirectories = next;
|
|
1779
|
+
if (this.#additionalDirectories.length > 0) {
|
|
1780
|
+
this.#header.additionalDirectories = this.#additionalDirectories;
|
|
1781
|
+
} else {
|
|
1782
|
+
this.#header.additionalDirectories = undefined;
|
|
1783
|
+
}
|
|
1784
|
+
await this.#persistWorkspaceDirectoriesChange();
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
getUsageStatistics(): UsageStatistics {
|
|
1788
|
+
return this.#index.usageSnapshot();
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* Open a new per-turn budget window: snapshot the cumulative output baseline,
|
|
1793
|
+
* reset the eval-subagent counter, and set the (optional) ceiling.
|
|
1794
|
+
*/
|
|
1795
|
+
beginTurnBudget(total: number | null, hard: boolean): void {
|
|
1796
|
+
this.#turnBudgetTotal = total;
|
|
1797
|
+
this.#turnBudgetHard = hard;
|
|
1798
|
+
this.#turnOutputBaseline = this.#index.usageSnapshot().output;
|
|
1799
|
+
this.#turnEvalOutput = 0;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
recordEvalSubagentOutput(output: number): void {
|
|
1803
|
+
if (Number.isFinite(output) && output > 0) this.#turnEvalOutput += output;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
getTurnBudget(): { total: number | null; spent: number; hard: boolean } {
|
|
1807
|
+
const mainOutput = Math.max(0, this.#index.usageSnapshot().output - this.#turnOutputBaseline);
|
|
1808
|
+
return { total: this.#turnBudgetTotal, spent: mainOutput + this.#turnEvalOutput, hard: this.#turnBudgetHard };
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
getSessionDir(): string {
|
|
1812
|
+
return this.#sessionDir;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
getSessionId(): string {
|
|
1816
|
+
return this.#sessionId;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
getSessionFile(): string | undefined {
|
|
1820
|
+
return this.#sessionFile;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
getArtifactsDir(): string | null {
|
|
1824
|
+
if (this.#adoptedArtifactManager) return this.#adoptedArtifactManager.dir;
|
|
1825
|
+
return artifactsDirectoryFor(this.#sessionFile);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
adoptArtifactManager(manager: ArtifactManager): void {
|
|
1829
|
+
this.#adoptedArtifactManager = manager;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
getArtifactManager(): ArtifactManager | null {
|
|
1833
|
+
return this.#artifactManagerForSession();
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
async allocateArtifactPath(toolType: string): Promise<{ id?: string; path?: string }> {
|
|
1837
|
+
return (await this.#artifactManagerForSession()?.allocatePath(toolType)) ?? {};
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
async saveArtifact(content: string, toolType: string): Promise<string | undefined> {
|
|
1841
|
+
const manager = this.#artifactManagerForSession();
|
|
1842
|
+
if (manager) return manager.save(content, toolType);
|
|
1843
|
+
|
|
1844
|
+
// Non-persistent session: keep an in-memory copy so spill truncation works.
|
|
1845
|
+
this.#inMemoryArtifacts ??= new Map();
|
|
1846
|
+
const id = String(this.#inMemoryArtifactCounter++);
|
|
1847
|
+
this.#inMemoryArtifacts.set(id, content);
|
|
1848
|
+
return id;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
async getArtifactPath(id: string): Promise<string | null> {
|
|
1852
|
+
return (await this.#artifactManagerForSession()?.getPath(id)) ?? null;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
async saveDraft(text: string): Promise<void> {
|
|
1856
|
+
const draftPath = this.#draftPath();
|
|
1857
|
+
if (!draftPath || !this.#persist) return;
|
|
1858
|
+
|
|
1859
|
+
if (text.length === 0) {
|
|
1860
|
+
try {
|
|
1861
|
+
await this.#storage.unlink(draftPath);
|
|
1862
|
+
} catch (err) {
|
|
1863
|
+
if (!isEnoent(err)) throw err;
|
|
1864
|
+
}
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
const sessionFile = this.#sessionFile;
|
|
1869
|
+
const draftWillMaterializeMetadataOnlyFile =
|
|
1870
|
+
sessionFile !== undefined &&
|
|
1871
|
+
!this.#storage.existsSync(sessionFile) &&
|
|
1872
|
+
this.#entries.every(isDraftOnlyMetadataEntry);
|
|
1873
|
+
// Force the header onto disk so resume can find the file this draft attaches to.
|
|
1874
|
+
await this.ensureOnDisk();
|
|
1875
|
+
if (draftWillMaterializeMetadataOnlyFile) {
|
|
1876
|
+
await this.#writeDraftOnlySessionMarker();
|
|
1877
|
+
this.#draftOnlySessionCleanupArmed = true;
|
|
1878
|
+
}
|
|
1879
|
+
await this.#storage.writeText(draftPath, text);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
async consumeDraft(): Promise<string | null> {
|
|
1883
|
+
const draftPath = this.#draftPath();
|
|
1884
|
+
if (!draftPath) return null;
|
|
1885
|
+
|
|
1886
|
+
let draft: string;
|
|
1887
|
+
try {
|
|
1888
|
+
draft = await this.#storage.readText(draftPath);
|
|
1889
|
+
} catch (err) {
|
|
1890
|
+
if (isEnoent(err)) return null;
|
|
1891
|
+
throw err;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
try {
|
|
1895
|
+
await this.#storage.unlink(draftPath);
|
|
1896
|
+
} catch (err) {
|
|
1897
|
+
if (!isEnoent(err)) throw err;
|
|
1898
|
+
}
|
|
1899
|
+
if (this.#entries.every(isDraftOnlyMetadataEntry) && this.#hasDraftOnlySessionMarker())
|
|
1900
|
+
this.#draftOnlySessionCleanupArmed = true;
|
|
1901
|
+
|
|
1902
|
+
return draft;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/** The source that set the session name: "user" (manual/RPC) or "auto" (generated title). */
|
|
1906
|
+
get titleSource(): SessionTitleSource | undefined {
|
|
1907
|
+
return this.#titleSource;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
getSessionName(): string | undefined {
|
|
1911
|
+
return this.#sessionName;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
onSessionNameChanged(cb: () => void): () => void {
|
|
1915
|
+
this.#sessionNameChangedCallbacks.add(cb);
|
|
1916
|
+
return () => {
|
|
1917
|
+
this.#sessionNameChangedCallbacks.delete(cb);
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
/**
|
|
1922
|
+
* Set the session display name.
|
|
1923
|
+
* @param source "user" for explicit renames; "auto" for generated titles.
|
|
1924
|
+
* Auto titles are ignored once the user has set a name.
|
|
1925
|
+
*/
|
|
1926
|
+
async setSessionName(name: string, source: SessionTitleSource = "auto", trigger?: string): Promise<boolean> {
|
|
1927
|
+
if (this.#titleSource === "user" && source === "auto") return false;
|
|
1928
|
+
|
|
1929
|
+
const title = SessionManager.#cleanTitle(name);
|
|
1930
|
+
if (!title) return false;
|
|
1931
|
+
|
|
1932
|
+
const previousTitle = this.#sessionName;
|
|
1933
|
+
const timestamp = nowIso();
|
|
1934
|
+
this.#sessionName = title;
|
|
1935
|
+
this.#titleSource = source;
|
|
1936
|
+
this.#titleUpdatedAt = timestamp;
|
|
1937
|
+
this.#header.title = title;
|
|
1938
|
+
this.#header.titleSource = source;
|
|
1939
|
+
|
|
1940
|
+
const entry: TitleChangeEntry = {
|
|
1941
|
+
type: TITLE_CHANGE_ENTRY_TYPE,
|
|
1942
|
+
...this.#freshEntryFields(),
|
|
1943
|
+
timestamp,
|
|
1944
|
+
title,
|
|
1945
|
+
source,
|
|
1946
|
+
};
|
|
1947
|
+
if (previousTitle) entry.previousTitle = previousTitle;
|
|
1948
|
+
if (trigger) entry.trigger = trigger;
|
|
1949
|
+
this.#entries.push(entry);
|
|
1950
|
+
this.#index.insert(entry);
|
|
1951
|
+
this.#notifyEntryAppended(entry);
|
|
1952
|
+
await this.#persistTitleChangeEntry(entry, { title, source, updatedAt: timestamp });
|
|
1953
|
+
|
|
1954
|
+
this.#notifySessionNameListeners();
|
|
1955
|
+
return true;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
/**
|
|
1959
|
+
* Append a foreign (host-authored) entry verbatim, preserving its
|
|
1960
|
+
* `id`/`parentId`. Used by collab guests to mirror the host session.
|
|
1961
|
+
*/
|
|
1962
|
+
ingestReplicatedEntry(entry: SessionEntry): void {
|
|
1963
|
+
this.#recordEntry(entry);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
/**
|
|
1967
|
+
* Snapshot the session for collab replication: the live header plus a deep
|
|
1968
|
+
* copy of every entry (the host mutates entries in place on rewrite paths, so
|
|
1969
|
+
* guests must not share references).
|
|
1970
|
+
*/
|
|
1971
|
+
snapshotForReplication(): { header: SessionHeader; entries: SessionEntry[] } {
|
|
1972
|
+
return { header: structuredClone(this.#header), entries: structuredClone(this.#entries) as SessionEntry[] };
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
/**
|
|
1976
|
+
* Append a message as a child of the current leaf, then advance the leaf.
|
|
1977
|
+
* CompactionSummaryMessage / BranchSummaryMessage are rejected here — they are
|
|
1978
|
+
* top-level entries via appendCompaction()/branchWithSummary().
|
|
1979
|
+
*/
|
|
1980
|
+
appendMessage(
|
|
1981
|
+
message:
|
|
1982
|
+
| Message
|
|
1983
|
+
| CustomMessage
|
|
1984
|
+
| HookMessage
|
|
1985
|
+
| BashExecutionMessage
|
|
1986
|
+
| PythonExecutionMessage
|
|
1987
|
+
| FileMentionMessage,
|
|
1988
|
+
): string {
|
|
1989
|
+
const entry: SessionMessageEntry = { type: "message", ...this.#freshEntryFields(), message };
|
|
1990
|
+
this.#recordEntry(entry);
|
|
1991
|
+
return entry.id;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
/**
|
|
1995
|
+
* Append to a non-active branch without changing the current leaf.
|
|
1996
|
+
* Used by work that retains ownership of a branch across tree navigation.
|
|
1997
|
+
*/
|
|
1998
|
+
appendMessageToBranch(
|
|
1999
|
+
message:
|
|
2000
|
+
| Message
|
|
2001
|
+
| CustomMessage
|
|
2002
|
+
| HookMessage
|
|
2003
|
+
| BashExecutionMessage
|
|
2004
|
+
| PythonExecutionMessage
|
|
2005
|
+
| FileMentionMessage,
|
|
2006
|
+
parentId: string | null,
|
|
2007
|
+
): string {
|
|
2008
|
+
if (parentId !== null && !this.#index.has(parentId)) throw new Error(`Entry ${parentId} not found`);
|
|
2009
|
+
const activeLeafId = this.#index.leafId();
|
|
2010
|
+
const entry: SessionMessageEntry = {
|
|
2011
|
+
type: "message",
|
|
2012
|
+
id: generateId(this.#index),
|
|
2013
|
+
parentId,
|
|
2014
|
+
timestamp: nowIso(),
|
|
2015
|
+
message,
|
|
2016
|
+
};
|
|
2017
|
+
this.#recordEntry(entry);
|
|
2018
|
+
this.#index.setLeaf(activeLeafId);
|
|
2019
|
+
return entry.id;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
/** Append a thinking level change as child of current leaf, then advance leaf. Returns entry id. */
|
|
2023
|
+
appendThinkingLevelChange(thinkingLevel?: string, configured?: string): string {
|
|
2024
|
+
const entry: ThinkingLevelChangeEntry = {
|
|
2025
|
+
type: "thinking_level_change",
|
|
2026
|
+
...this.#freshEntryFields(),
|
|
2027
|
+
thinkingLevel: thinkingLevel ?? null,
|
|
2028
|
+
configured: configured ?? null,
|
|
2029
|
+
};
|
|
2030
|
+
this.#recordEntry(entry);
|
|
2031
|
+
return entry.id;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
appendServiceTierChange(serviceTier: ServiceTierByFamily | null): string {
|
|
2035
|
+
const entry: ServiceTierChangeEntry = { type: "service_tier_change", ...this.#freshEntryFields(), serviceTier };
|
|
2036
|
+
this.#recordEntry(entry);
|
|
2037
|
+
return entry.id;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
appendModeChange(mode: string, data?: Record<string, unknown>): string {
|
|
2041
|
+
const entry: ModeChangeEntry = { type: "mode_change", ...this.#freshEntryFields(), mode, data };
|
|
2042
|
+
this.#recordEntry(entry);
|
|
2043
|
+
return entry.id;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
/**
|
|
2047
|
+
* Append a model change as a child of the current leaf, then advance the leaf.
|
|
2048
|
+
* @param model Model in "provider/modelId" format
|
|
2049
|
+
* @param role Optional role (default: "default")
|
|
2050
|
+
* @param resolvedModelIsFallback Whether this transition selected a retry-fallback model
|
|
2051
|
+
*/
|
|
2052
|
+
appendModelChange(model: string, role?: string, resolvedModelIsFallback = false): string {
|
|
2053
|
+
const entry: ModelChangeEntry = {
|
|
2054
|
+
type: "model_change",
|
|
2055
|
+
...this.#freshEntryFields(),
|
|
2056
|
+
model,
|
|
2057
|
+
role,
|
|
2058
|
+
resolvedModelIsFallback,
|
|
2059
|
+
};
|
|
2060
|
+
this.#recordEntry(entry);
|
|
2061
|
+
return entry.id;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
appendSessionInit(init: {
|
|
2065
|
+
systemPrompt: string;
|
|
2066
|
+
task: string;
|
|
2067
|
+
tools: string[];
|
|
2068
|
+
agent?: string;
|
|
2069
|
+
modelRole?: string;
|
|
2070
|
+
resolvedModel?: string;
|
|
2071
|
+
readOnly?: boolean;
|
|
2072
|
+
outputSchema?: unknown;
|
|
2073
|
+
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
2074
|
+
restrictToolNames?: boolean;
|
|
2075
|
+
spawns?: string;
|
|
2076
|
+
readSummarize?: boolean;
|
|
2077
|
+
}): string {
|
|
2078
|
+
const entry: SessionInitEntry = { type: "session_init", ...this.#freshEntryFields(), ...init };
|
|
2079
|
+
this.#recordEntry(entry);
|
|
2080
|
+
return entry.id;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
appendCompaction<T = unknown>(
|
|
2084
|
+
summary: string,
|
|
2085
|
+
shortSummary: string | undefined,
|
|
2086
|
+
firstKeptEntryId: string,
|
|
2087
|
+
tokensBefore: number,
|
|
2088
|
+
details?: T,
|
|
2089
|
+
fromExtension?: boolean,
|
|
2090
|
+
preserveData?: Record<string, unknown>,
|
|
2091
|
+
): string {
|
|
2092
|
+
const entry: CompactionEntry<T> = {
|
|
2093
|
+
type: "compaction",
|
|
2094
|
+
...this.#freshEntryFields(),
|
|
2095
|
+
summary,
|
|
2096
|
+
shortSummary,
|
|
2097
|
+
firstKeptEntryId,
|
|
2098
|
+
tokensBefore,
|
|
2099
|
+
details,
|
|
2100
|
+
fromExtension,
|
|
2101
|
+
preserveData,
|
|
2102
|
+
};
|
|
2103
|
+
this.#recordEntry(entry);
|
|
2104
|
+
return entry.id;
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
/**
|
|
2108
|
+
* Append the durable conversation boundary recorded by `/clear`. The
|
|
2109
|
+
* collapsed live transcript and the model-context rebuild start after the
|
|
2110
|
+
* latest one, while the full history stays on disk (the plain
|
|
2111
|
+
* `transcript:true` export walks it unchanged).
|
|
2112
|
+
*/
|
|
2113
|
+
appendResetBoundary(): string {
|
|
2114
|
+
const entry: ResetBoundaryEntry = { type: "reset_boundary", ...this.#freshEntryFields() };
|
|
2115
|
+
this.#recordEntry(entry);
|
|
2116
|
+
return entry.id;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
appendCustomEntry(customType: string, data?: unknown): string {
|
|
2120
|
+
const entry: CustomEntry = { type: "custom", customType, data, ...this.#freshEntryFields() };
|
|
2121
|
+
this.#recordEntry(entry);
|
|
2122
|
+
return entry.id;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
/**
|
|
2126
|
+
* Rewrite the session file after in-place entry updates (e.g. pruning old tool
|
|
2127
|
+
* outputs). Use sparingly.
|
|
2128
|
+
*/
|
|
2129
|
+
async rewriteEntries(): Promise<void> {
|
|
2130
|
+
if (!this.#persist || !this.#sessionFile) return;
|
|
2131
|
+
await this.#rewriteAtomically();
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
/**
|
|
2135
|
+
* Append a custom message entry (for extensions) that participates in LLM context.
|
|
2136
|
+
* @param customType Hook identifier for filtering on reload
|
|
2137
|
+
* @param content Message content (string or TextContent/ImageContent array)
|
|
2138
|
+
* @param display Whether to show in TUI (true = styled display, false = hidden)
|
|
2139
|
+
* @param details Optional extension-specific metadata (not sent to LLM)
|
|
2140
|
+
* @param attribution Who initiated this message for billing/attribution semantics
|
|
2141
|
+
*/
|
|
2142
|
+
appendCustomMessageEntry<T = unknown>(
|
|
2143
|
+
customType: string | undefined,
|
|
2144
|
+
content: string | (TextContent | ImageContent)[] | undefined,
|
|
2145
|
+
display: boolean | undefined,
|
|
2146
|
+
details?: T,
|
|
2147
|
+
attribution: MessageAttribution | undefined = "agent",
|
|
2148
|
+
): string {
|
|
2149
|
+
const normalized = normalizeCustomMessagePayload<T>({ customType, content, display, details, attribution });
|
|
2150
|
+
const entry: CustomMessageEntry<T> = {
|
|
2151
|
+
type: "custom_message",
|
|
2152
|
+
customType: normalized.customType,
|
|
2153
|
+
content: normalized.content,
|
|
2154
|
+
display: normalized.display,
|
|
2155
|
+
// Drop AgentSession-internal transient fields before disk persistence.
|
|
2156
|
+
details: stripInternalDetailsFields(normalized.details),
|
|
2157
|
+
attribution: normalized.attribution,
|
|
2158
|
+
...this.#freshEntryFields(),
|
|
2159
|
+
};
|
|
2160
|
+
this.#recordEntry(entry);
|
|
2161
|
+
return entry.id;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
/** Append a TTSR injection entry recording which rules were injected. */
|
|
2165
|
+
appendTtsrInjection(ruleNames: string[]): string {
|
|
2166
|
+
const entry: TtsrInjectionEntry = {
|
|
2167
|
+
type: "ttsr_injection",
|
|
2168
|
+
...this.#freshEntryFields(),
|
|
2169
|
+
injectedRules: [...ruleNames],
|
|
2170
|
+
};
|
|
2171
|
+
this.#recordEntry(entry);
|
|
2172
|
+
return entry.id;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
/** All unique TTSR rule names injected on the current branch (root → leaf). */
|
|
2176
|
+
getInjectedTtsrRules(): string[] {
|
|
2177
|
+
const names = new Set<string>();
|
|
2178
|
+
for (const entry of this.getBranch()) {
|
|
2179
|
+
if (entry.type !== "ttsr_injection") continue;
|
|
2180
|
+
for (const name of entry.injectedRules) names.add(name);
|
|
2181
|
+
}
|
|
2182
|
+
return [...names];
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
/** Append a credential pin recording which OAuth account served `provider`. */
|
|
2186
|
+
appendCredentialPin(provider: string, hash: string): string {
|
|
2187
|
+
const entry: CredentialPinEntry = {
|
|
2188
|
+
type: "credential_pin",
|
|
2189
|
+
...this.#freshEntryFields(),
|
|
2190
|
+
provider,
|
|
2191
|
+
hash,
|
|
2192
|
+
};
|
|
2193
|
+
this.#recordEntry(entry);
|
|
2194
|
+
return entry.id;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
/**
|
|
2198
|
+
* Latest credential pin per provider on the current branch (root → leaf),
|
|
2199
|
+
* with the effective last-use time of the pinned account.
|
|
2200
|
+
*
|
|
2201
|
+
* Pins are appended only when the serving account *changes*, so a long
|
|
2202
|
+
* session on one account carries a single old pin entry. Any assistant turn
|
|
2203
|
+
* for the same provider after that pin was necessarily served by the pinned
|
|
2204
|
+
* account, so its timestamp advances `lastUsedAt` — a resume seconds after
|
|
2205
|
+
* the last turn seeds a warm sticky instead of a stale one.
|
|
2206
|
+
*/
|
|
2207
|
+
getCredentialPins(): Map<string, { hash: string; lastUsedAt: number }> {
|
|
2208
|
+
const pins = new Map<string, { hash: string; lastUsedAt: number }>();
|
|
2209
|
+
for (const entry of this.getBranch()) {
|
|
2210
|
+
if (entry.type === "credential_pin") {
|
|
2211
|
+
pins.set(entry.provider, { hash: entry.hash, lastUsedAt: new Date(entry.timestamp).getTime() });
|
|
2212
|
+
} else if (entry.type === "message" && entry.message.role === "assistant") {
|
|
2213
|
+
const pin = pins.get(entry.message.provider);
|
|
2214
|
+
if (pin) pin.lastUsedAt = Math.max(pin.lastUsedAt, entry.message.timestamp);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
return pins;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
getLeafId(): string | null {
|
|
2221
|
+
return this.#index.leafId();
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
getLeafEntry(): SessionEntry | undefined {
|
|
2225
|
+
return this.#index.leafEntry();
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
/**
|
|
2229
|
+
* The most recent model role on the current branch, or undefined when no
|
|
2230
|
+
* model change has been recorded.
|
|
2231
|
+
*/
|
|
2232
|
+
getLastModelChangeRole(): string | undefined {
|
|
2233
|
+
const branch = this.getBranch();
|
|
2234
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
2235
|
+
const entry = branch[index];
|
|
2236
|
+
if (entry.type === "model_change") return entry.role ?? "default";
|
|
2237
|
+
}
|
|
2238
|
+
return undefined;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
getEntry(id: string): SessionEntry | undefined {
|
|
2242
|
+
return this.#index.get(id);
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
/** All direct children of an entry. */
|
|
2246
|
+
getChildren(parentId: string): SessionEntry[] {
|
|
2247
|
+
return this.#index.childrenOf(parentId);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
getLabel(id: string): string | undefined {
|
|
2251
|
+
return this.#index.labelFor(id);
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
/**
|
|
2255
|
+
* Set or clear a label on an entry. Pass undefined/empty to clear.
|
|
2256
|
+
*/
|
|
2257
|
+
appendLabelChange(targetId: string, label: string | undefined): string {
|
|
2258
|
+
if (!this.#index.has(targetId)) throw new Error(`Entry ${targetId} not found`);
|
|
2259
|
+
|
|
2260
|
+
const entry: LabelEntry = { type: "label", ...this.#freshEntryFields(), targetId, label };
|
|
2261
|
+
this.#recordEntry(entry);
|
|
2262
|
+
return entry.id;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
/**
|
|
2266
|
+
* Walk from an entry to root, returning entries in path order. Includes all
|
|
2267
|
+
* entry types; use buildSessionContext() for the resolved LLM messages.
|
|
2268
|
+
*/
|
|
2269
|
+
getBranch(fromId?: string): SessionEntry[] {
|
|
2270
|
+
return this.#index.pathTo(fromId ?? this.#index.leafId());
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
/**
|
|
2274
|
+
* Build the session context (LLM messages), or — with `{ transcript: true }` —
|
|
2275
|
+
* the full-history display transcript, from the current leaf path.
|
|
2276
|
+
*/
|
|
2277
|
+
buildSessionContext(options?: BuildSessionContextOptions): SessionContext {
|
|
2278
|
+
return buildSessionContext(this.#entries, this.#index.leafId(), this.#index.entriesById(), options);
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
/** Strip stale OpenAI Responses assistant replay metadata from loaded entries. */
|
|
2282
|
+
sanitizeLoadedOpenAIResponsesReplayMetadata(): boolean {
|
|
2283
|
+
let changed = false;
|
|
2284
|
+
for (const entry of this.#entries) {
|
|
2285
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
2286
|
+
|
|
2287
|
+
const sanitized = sanitizeRehydratedOpenAIResponsesAssistantMessage(entry.message);
|
|
2288
|
+
if (sanitized === entry.message) continue;
|
|
2289
|
+
|
|
2290
|
+
entry.message = sanitized;
|
|
2291
|
+
changed = true;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
return changed;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
getHeader(): SessionHeader | null {
|
|
2298
|
+
return this.#header;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
/** All session entries (excludes header). Returns a shallow copy. */
|
|
2302
|
+
getEntries(): SessionEntry[] {
|
|
2303
|
+
return [...this.#entries];
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
/**
|
|
2307
|
+
* The session as a tree. A well-formed session has exactly one root; orphaned
|
|
2308
|
+
* entries (broken parent chain) are returned as roots too.
|
|
2309
|
+
*/
|
|
2310
|
+
getTree(): SessionTreeNode[] {
|
|
2311
|
+
return this.#index.tree(this.#entries);
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
/**
|
|
2315
|
+
* Move the leaf to an earlier entry so the next append forms a new branch.
|
|
2316
|
+
* Existing entries are never modified or deleted.
|
|
2317
|
+
*/
|
|
2318
|
+
branch(branchFromId: string): void {
|
|
2319
|
+
if (!this.#index.has(branchFromId)) throw new Error(`Entry ${branchFromId} not found`);
|
|
2320
|
+
this.#setLeaf(branchFromId);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
/** Reset the leaf to null so the next append creates a new root entry. */
|
|
2324
|
+
resetLeaf(): void {
|
|
2325
|
+
this.#setLeaf(null);
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
/** Like branch(), but also records a branch_summary of the abandoned path. */
|
|
2329
|
+
branchWithSummary(branchFromId: string | null, summary: string, details?: unknown, fromExtension?: boolean): string {
|
|
2330
|
+
if (branchFromId !== null && !this.#index.has(branchFromId)) throw new Error(`Entry ${branchFromId} not found`);
|
|
2331
|
+
|
|
2332
|
+
this.#setLeaf(branchFromId);
|
|
2333
|
+
const entry: BranchSummaryEntry = {
|
|
2334
|
+
type: "branch_summary",
|
|
2335
|
+
id: generateId(this.#index),
|
|
2336
|
+
parentId: branchFromId,
|
|
2337
|
+
timestamp: nowIso(),
|
|
2338
|
+
fromId: branchFromId ?? "root",
|
|
2339
|
+
summary,
|
|
2340
|
+
details,
|
|
2341
|
+
fromExtension,
|
|
2342
|
+
};
|
|
2343
|
+
this.#recordEntry(entry);
|
|
2344
|
+
return entry.id;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
/**
|
|
2348
|
+
* Create a new session file containing only the path from root to `leafId`.
|
|
2349
|
+
* Returns the new file path, or undefined when not persisting.
|
|
2350
|
+
*/
|
|
2351
|
+
createBranchedSession(leafId: string): string | undefined {
|
|
2352
|
+
const sourceSessionFile = this.#sessionFile;
|
|
2353
|
+
const branchPath = this.getBranch(leafId);
|
|
2354
|
+
if (branchPath.length === 0) throw new Error(`Entry ${leafId} not found`);
|
|
2355
|
+
|
|
2356
|
+
// Drop label entries from the path; recreate them fresh from the resolved map.
|
|
2357
|
+
const entriesToKeep = branchPath.filter(entry => entry.type !== "label");
|
|
2358
|
+
const keptIds = new Set(entriesToKeep.map(entry => entry.id));
|
|
2359
|
+
const labelsToCarry: Array<{ targetId: string; label: string }> = [];
|
|
2360
|
+
for (const [targetId, label] of this.#index.labelsInEffect()) {
|
|
2361
|
+
if (keptIds.has(targetId)) labelsToCarry.push({ targetId, label });
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
const timestamp = nowIso();
|
|
2365
|
+
const newSessionId = mintSessionId();
|
|
2366
|
+
const newSessionFile = path.join(this.#sessionDir, `${fileSafeTimestamp(timestamp)}_${newSessionId}.jsonl`);
|
|
2367
|
+
const header: SessionHeader = {
|
|
2368
|
+
type: "session",
|
|
2369
|
+
version: CURRENT_SESSION_VERSION,
|
|
2370
|
+
id: newSessionId,
|
|
2371
|
+
timestamp,
|
|
2372
|
+
cwd: this.#cwd,
|
|
2373
|
+
title: this.#sessionName,
|
|
2374
|
+
titleSource: this.#titleSource,
|
|
2375
|
+
parentSession: this.#persist ? sourceSessionFile : undefined,
|
|
2376
|
+
additionalDirectories: this.#additionalDirectories.length > 0 ? [...this.#additionalDirectories] : undefined,
|
|
2377
|
+
};
|
|
2378
|
+
|
|
2379
|
+
const labels: LabelEntry[] = [];
|
|
2380
|
+
let parentId = entriesToKeep[entriesToKeep.length - 1]?.id ?? null;
|
|
2381
|
+
for (const carried of labelsToCarry) {
|
|
2382
|
+
const labelEntry: LabelEntry = {
|
|
2383
|
+
type: "label",
|
|
2384
|
+
id: generateId(new Set([...keptIds, ...labels.map(entry => entry.id)])),
|
|
2385
|
+
parentId,
|
|
2386
|
+
timestamp: nowIso(),
|
|
2387
|
+
targetId: carried.targetId,
|
|
2388
|
+
label: carried.label,
|
|
2389
|
+
};
|
|
2390
|
+
labels.push(labelEntry);
|
|
2391
|
+
parentId = labelEntry.id;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
this.#header = header;
|
|
2395
|
+
this.#entries = [...entriesToKeep, ...labels];
|
|
2396
|
+
this.#sessionId = newSessionId;
|
|
2397
|
+
this.#sessionName = header.title;
|
|
2398
|
+
this.#titleSource = header.titleSource;
|
|
2399
|
+
this.#titleUpdatedAt = timestamp;
|
|
2400
|
+
this.#hasTitleSlot = true;
|
|
2401
|
+
this.#index.rebuild(this.#entries);
|
|
2402
|
+
this.#artifactManager = null;
|
|
2403
|
+
this.#artifactManagerSessionFile = null;
|
|
2404
|
+
this.#forceFileCreation = this.#persist;
|
|
2405
|
+
|
|
2406
|
+
if (!this.#persist) {
|
|
2407
|
+
this.#sessionFile = undefined;
|
|
2408
|
+
this.#fileIsCurrent = false;
|
|
2409
|
+
this.#rewriteRequired = false;
|
|
2410
|
+
return undefined;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
this.#sessionFile = newSessionFile;
|
|
2414
|
+
this.#rewriteSynchronously();
|
|
2415
|
+
this.#rememberBreadcrumb(this.#cwd, newSessionFile);
|
|
2416
|
+
return newSessionFile;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
/** Resolve the canonical default session directory for a cwd. */
|
|
2420
|
+
static getDefaultSessionDir(
|
|
2421
|
+
cwd: string,
|
|
2422
|
+
agentDir?: string,
|
|
2423
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
2424
|
+
): string {
|
|
2425
|
+
return computeDefaultSessionDir(cwd, storage, getSessionsDir(agentDir));
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
/**
|
|
2429
|
+
* Create a new session.
|
|
2430
|
+
* @param cwd Working directory (stored in the session header)
|
|
2431
|
+
* @param sessionDir Optional session directory; defaults to the cwd-derived dir.
|
|
2432
|
+
*/
|
|
2433
|
+
static create(cwd: string, sessionDir?: string, storage: SessionStorage = new FileSessionStorage()): SessionManager {
|
|
2434
|
+
const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
|
|
2435
|
+
const manager = new SessionManager(cwd, dir, true, storage);
|
|
2436
|
+
manager.#resetToNewSession();
|
|
2437
|
+
return manager;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
/**
|
|
2441
|
+
* Create a fresh empty session file in the default session directory for
|
|
2442
|
+
* `cwd`, writing only the session header. The returned path can be passed to
|
|
2443
|
+
* `setSessionFile` / `AgentSession.switchSession` when a caller explicitly
|
|
2444
|
+
* needs a brand-new persisted session at a cwd-derived path.
|
|
2445
|
+
*/
|
|
2446
|
+
static createEmptySessionFile(cwd: string, storage: SessionStorage = new FileSessionStorage()): string {
|
|
2447
|
+
const sessionDir = SessionManager.getDefaultSessionDir(cwd, undefined, storage);
|
|
2448
|
+
const id = mintSessionId();
|
|
2449
|
+
const timestamp = nowIso();
|
|
2450
|
+
const header: SessionHeader = {
|
|
2451
|
+
type: "session",
|
|
2452
|
+
version: CURRENT_SESSION_VERSION,
|
|
2453
|
+
id,
|
|
2454
|
+
timestamp,
|
|
2455
|
+
cwd: path.resolve(cwd),
|
|
2456
|
+
};
|
|
2457
|
+
const file = path.join(sessionDir, `${fileSafeTimestamp(timestamp)}_${id}.jsonl`);
|
|
2458
|
+
storage.writeTextSync(file, `${serializeTitleSlot({ updatedAt: timestamp })}${JSON.stringify(header)}\n`);
|
|
2459
|
+
return file;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
/**
|
|
2463
|
+
* Fork a session into the current project directory: copy history from another
|
|
2464
|
+
* session file while creating a fresh session file in this sessionDir.
|
|
2465
|
+
*
|
|
2466
|
+
* `options.sessionFile` pins the new session's file path (default: an
|
|
2467
|
+
* auto-named `<timestamp>_<id>.jsonl` in `sessionDir`). Callers that register
|
|
2468
|
+
* the fork as a named agent (e.g. `/tan`) pass `<agentId>.jsonl` so the
|
|
2469
|
+
* persisted-subagent scan keys the agent by the same id the live ref uses.
|
|
2470
|
+
*/
|
|
2471
|
+
static async forkFrom(
|
|
2472
|
+
sourcePath: string,
|
|
2473
|
+
cwd: string,
|
|
2474
|
+
sessionDir?: string,
|
|
2475
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
2476
|
+
options?: { suppressBreadcrumb?: boolean; sessionFile?: string },
|
|
2477
|
+
): Promise<SessionManager> {
|
|
2478
|
+
const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
|
|
2479
|
+
const manager = new SessionManager(cwd, dir, true, storage);
|
|
2480
|
+
manager.#suppressBreadcrumb = options?.suppressBreadcrumb === true;
|
|
2481
|
+
|
|
2482
|
+
const sourceEntries = structuredClone(await loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
|
|
2483
|
+
migrateToCurrentVersion(sourceEntries);
|
|
2484
|
+
await resolveBlobRefsInEntries(sourceEntries, manager.#blobs);
|
|
2485
|
+
|
|
2486
|
+
const sourceHeader = sourceEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
2487
|
+
const history = sourceEntries.filter(entry => entry.type !== "session") as SessionEntry[];
|
|
2488
|
+
manager.#resetToNewSession(
|
|
2489
|
+
{
|
|
2490
|
+
parentSession: sourceHeader?.id,
|
|
2491
|
+
providerPromptCacheKey: sourceHeader?.providerPromptCacheKey ?? sourceHeader?.id,
|
|
2492
|
+
},
|
|
2493
|
+
options?.sessionFile,
|
|
2494
|
+
);
|
|
2495
|
+
manager.#header.title = sourceHeader?.title;
|
|
2496
|
+
manager.#header.titleSource = sourceHeader?.titleSource;
|
|
2497
|
+
manager.#additionalDirectories = (sourceHeader?.additionalDirectories ?? []).filter(d => d !== path.resolve(cwd));
|
|
2498
|
+
manager.#header.additionalDirectories =
|
|
2499
|
+
manager.#additionalDirectories.length > 0 ? manager.#additionalDirectories : undefined;
|
|
2500
|
+
manager.#sessionName = manager.#header.title;
|
|
2501
|
+
manager.#titleSource = manager.#header.titleSource;
|
|
2502
|
+
manager.#titleUpdatedAt = nowIso();
|
|
2503
|
+
manager.#hasTitleSlot = true;
|
|
2504
|
+
manager.#entries = history;
|
|
2505
|
+
manager.#index.rebuild(history);
|
|
2506
|
+
manager.sanitizeLoadedOpenAIResponsesReplayMetadata();
|
|
2507
|
+
manager.#forceFileCreation = true;
|
|
2508
|
+
await manager.#rewriteAtomically();
|
|
2509
|
+
return manager;
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
/**
|
|
2513
|
+
* Open a specific session file.
|
|
2514
|
+
* @param sessionDir Optional dir for /new or /branch; defaults to the file's parent.
|
|
2515
|
+
* @param options.initialCwd Cwd to use when the file is empty or missing.
|
|
2516
|
+
*/
|
|
2517
|
+
static async open(
|
|
2518
|
+
filePath: string,
|
|
2519
|
+
sessionDir?: string,
|
|
2520
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
2521
|
+
options?: { initialCwd?: string; suppressBreadcrumb?: boolean },
|
|
2522
|
+
): Promise<SessionManager> {
|
|
2523
|
+
const loaded = await loadEntriesFromFile(filePath, storage);
|
|
2524
|
+
const header = loaded.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
2525
|
+
// Resume into the session's recorded cwd only when that directory still
|
|
2526
|
+
// exists. A deleted project dir would make the constructor's #cwd — and the
|
|
2527
|
+
// `setProjectDir` chdir interactive mode runs next — point at (and fail on)
|
|
2528
|
+
// a missing path, so fall back to the launch cwd and anchor /new and /branch
|
|
2529
|
+
// there too, keeping the resumed session where the user already is.
|
|
2530
|
+
const recordedCwd = header?.cwd;
|
|
2531
|
+
const recordedCwdUsable = !!recordedCwd && (await directoryExists(recordedCwd));
|
|
2532
|
+
const cwd = recordedCwdUsable ? recordedCwd : (options?.initialCwd ?? getProjectDir());
|
|
2533
|
+
const dir =
|
|
2534
|
+
sessionDir ??
|
|
2535
|
+
(recordedCwd && !recordedCwdUsable
|
|
2536
|
+
? SessionManager.getDefaultSessionDir(cwd, undefined, storage)
|
|
2537
|
+
: path.dirname(path.resolve(filePath)));
|
|
2538
|
+
const manager = new SessionManager(cwd, dir, true, storage);
|
|
2539
|
+
manager.#suppressBreadcrumb = options?.suppressBreadcrumb === true;
|
|
2540
|
+
await manager.setSessionFile(filePath);
|
|
2541
|
+
return manager;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
/**
|
|
2545
|
+
* Lock-free peek for cold subagent revival: returns the recorded working
|
|
2546
|
+
* directory (session header) and the latest `session_init` contract (system
|
|
2547
|
+
* prompt / tools / output schema) WITHOUT taking the single-writer lock that
|
|
2548
|
+
* {@link open} acquires — the caller re-opens for the actual revive. Returns
|
|
2549
|
+
* null when the file can't be read; `init` is null for files written before
|
|
2550
|
+
* `session_init` was recorded (no faithful contract to rebuild from).
|
|
2551
|
+
*/
|
|
2552
|
+
static async peekSessionInit(
|
|
2553
|
+
filePath: string,
|
|
2554
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
2555
|
+
): Promise<{
|
|
2556
|
+
cwd: string;
|
|
2557
|
+
init: {
|
|
2558
|
+
systemPrompt: string;
|
|
2559
|
+
task: string;
|
|
2560
|
+
tools: string[];
|
|
2561
|
+
agent?: string;
|
|
2562
|
+
modelRole?: string;
|
|
2563
|
+
resolvedModel?: string;
|
|
2564
|
+
outputSchema?: unknown;
|
|
2565
|
+
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
2566
|
+
restrictToolNames?: boolean;
|
|
2567
|
+
spawns?: string;
|
|
2568
|
+
readSummarize?: boolean;
|
|
2569
|
+
} | null;
|
|
2570
|
+
} | null> {
|
|
2571
|
+
let loaded: FileEntry[];
|
|
2572
|
+
try {
|
|
2573
|
+
loaded = await loadEntriesFromFile(filePath, storage);
|
|
2574
|
+
} catch {
|
|
2575
|
+
return null;
|
|
2576
|
+
}
|
|
2577
|
+
// A missing/empty file has no usable session — nothing to revive from.
|
|
2578
|
+
if (loaded.length === 0) return null;
|
|
2579
|
+
const header = loaded.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
2580
|
+
let init: {
|
|
2581
|
+
systemPrompt: string;
|
|
2582
|
+
task: string;
|
|
2583
|
+
tools: string[];
|
|
2584
|
+
agent?: string;
|
|
2585
|
+
modelRole?: string;
|
|
2586
|
+
resolvedModel?: string;
|
|
2587
|
+
outputSchema?: unknown;
|
|
2588
|
+
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
2589
|
+
restrictToolNames?: boolean;
|
|
2590
|
+
spawns?: string;
|
|
2591
|
+
readSummarize?: boolean;
|
|
2592
|
+
} | null = null;
|
|
2593
|
+
for (let index = loaded.length - 1; index >= 0; index--) {
|
|
2594
|
+
const entry = loaded[index];
|
|
2595
|
+
if (entry.type === "session_init") {
|
|
2596
|
+
init = {
|
|
2597
|
+
systemPrompt: entry.systemPrompt,
|
|
2598
|
+
task: entry.task,
|
|
2599
|
+
tools: entry.tools,
|
|
2600
|
+
agent: entry.agent,
|
|
2601
|
+
modelRole: entry.modelRole,
|
|
2602
|
+
resolvedModel: entry.resolvedModel,
|
|
2603
|
+
outputSchema: entry.outputSchema,
|
|
2604
|
+
outputSchemaMode: entry.outputSchemaMode,
|
|
2605
|
+
restrictToolNames: entry.restrictToolNames,
|
|
2606
|
+
readSummarize: entry.readSummarize,
|
|
2607
|
+
spawns: entry.spawns,
|
|
2608
|
+
};
|
|
2609
|
+
break;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
return { cwd: header?.cwd ?? getProjectDir(), init };
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
/** Continue the most recent session, or create a new one if none exists. */
|
|
2616
|
+
static async continueRecent(
|
|
2617
|
+
cwd: string,
|
|
2618
|
+
sessionDir?: string,
|
|
2619
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
2620
|
+
): Promise<SessionManager> {
|
|
2621
|
+
const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
|
|
2622
|
+
const resolvedCwd = path.resolve(cwd);
|
|
2623
|
+
const breadcrumb = await readTerminalBreadcrumbEntry();
|
|
2624
|
+
let chosenSession: string | null | undefined;
|
|
2625
|
+
|
|
2626
|
+
if (breadcrumb) {
|
|
2627
|
+
// A fresh `/new` boundary whose JSONL was never materialized (lazy
|
|
2628
|
+
// new-session persistence, then a process exit before any assistant
|
|
2629
|
+
// output). Honor the boundary: start fresh rather than falling back to
|
|
2630
|
+
// findMostRecentSession(), which would resurrect the pre-`/new`
|
|
2631
|
+
// transcript. A materialized (or genuinely stale/deleted) crumb reports
|
|
2632
|
+
// exists=false only when fresh, so this never masks a real stale crumb.
|
|
2633
|
+
if (breadcrumb.fresh && !breadcrumb.exists) {
|
|
2634
|
+
const manager = new SessionManager(cwd, dir, true, storage);
|
|
2635
|
+
manager.#resetToNewSession();
|
|
2636
|
+
return manager;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
// Recover stale crumbs: a subagent open (pre-fix) may have pointed this
|
|
2640
|
+
// terminal's breadcrumb at an artifact child; resume the parent instead.
|
|
2641
|
+
breadcrumb.sessionFile = resolveBreadcrumbToInteractiveRoot(breadcrumb.sessionFile);
|
|
2642
|
+
const breadcrumbCwd = path.resolve(breadcrumb.cwd);
|
|
2643
|
+
if (breadcrumbCwd === resolvedCwd) {
|
|
2644
|
+
chosenSession = breadcrumb.sessionFile;
|
|
2645
|
+
} else {
|
|
2646
|
+
// The terminal's last session started in a different cwd. If that cwd is
|
|
2647
|
+
// gone (worktree move/rename) and this location has no sessions of its
|
|
2648
|
+
// own, re-root the moved session here instead of starting fresh. When an
|
|
2649
|
+
// explicit sessionDir is reused across the move, the stale breadcrumb file
|
|
2650
|
+
// may be the newest entry there; prefer a genuine current-cwd session.
|
|
2651
|
+
let newestInTargetDir = await findMostRecentSession(dir, storage);
|
|
2652
|
+
const breadcrumbFile = path.resolve(breadcrumb.sessionFile);
|
|
2653
|
+
const breadcrumbCwdMissing = !fs.existsSync(breadcrumbCwd);
|
|
2654
|
+
const newestIsBreadcrumb = newestInTargetDir ? path.resolve(newestInTargetDir) === breadcrumbFile : false;
|
|
2655
|
+
let currentProjectAlreadyHasSession = false;
|
|
2656
|
+
|
|
2657
|
+
if (breadcrumbCwdMissing && newestIsBreadcrumb) {
|
|
2658
|
+
const localSession = (await SessionManager.list(cwd, dir, storage)).find(
|
|
2659
|
+
session =>
|
|
2660
|
+
path.resolve(session.path) !== breadcrumbFile &&
|
|
2661
|
+
session.cwd &&
|
|
2662
|
+
path.resolve(session.cwd) === resolvedCwd,
|
|
2663
|
+
);
|
|
2664
|
+
if (localSession) {
|
|
2665
|
+
newestInTargetDir = localSession.path;
|
|
2666
|
+
currentProjectAlreadyHasSession = true;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
const looksLikeMovedProject =
|
|
2671
|
+
breadcrumbCwdMissing &&
|
|
2672
|
+
(newestInTargetDir === null || (newestIsBreadcrumb && !currentProjectAlreadyHasSession));
|
|
2673
|
+
if (looksLikeMovedProject) {
|
|
2674
|
+
logger.info("Re-rooting moved session", { from: breadcrumbCwd, to: resolvedCwd });
|
|
2675
|
+
// Anchor at the gone breadcrumb cwd so the moveTo below relocates the
|
|
2676
|
+
// session: open() now falls back to the launch cwd for a missing
|
|
2677
|
+
// recorded cwd, which would no-op moveTo when it equals `cwd`.
|
|
2678
|
+
const manager = await SessionManager.open(breadcrumb.sessionFile, undefined, storage, {
|
|
2679
|
+
initialCwd: breadcrumbCwd,
|
|
2680
|
+
});
|
|
2681
|
+
await manager.moveTo(cwd, sessionDir);
|
|
2682
|
+
return manager;
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
chosenSession = newestInTargetDir;
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
if (chosenSession === undefined) chosenSession = await findMostRecentSession(dir, storage);
|
|
2690
|
+
|
|
2691
|
+
const manager = new SessionManager(cwd, dir, true, storage);
|
|
2692
|
+
if (chosenSession) await manager.setSessionFile(chosenSession);
|
|
2693
|
+
else manager.#resetToNewSession();
|
|
2694
|
+
return manager;
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
/** Create an in-memory session (no file persistence). */
|
|
2698
|
+
static inMemory(
|
|
2699
|
+
cwd: string = getProjectDir(),
|
|
2700
|
+
storage: SessionStorage = new MemorySessionStorage(),
|
|
2701
|
+
): SessionManager {
|
|
2702
|
+
const manager = new SessionManager(cwd, "", false, storage);
|
|
2703
|
+
manager.#resetToNewSession();
|
|
2704
|
+
return manager;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
/**
|
|
2708
|
+
* List sessions for a project directory.
|
|
2709
|
+
* @param sessionDir Optional dir; defaults to the cwd-derived dir.
|
|
2710
|
+
*/
|
|
2711
|
+
static async list(
|
|
2712
|
+
cwd: string,
|
|
2713
|
+
sessionDir?: string,
|
|
2714
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
2715
|
+
): Promise<SessionInfo[]> {
|
|
2716
|
+
const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
|
|
2717
|
+
return listSessions(dir, storage);
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
/** List all sessions across all project directories. */
|
|
2721
|
+
static listAll(storage: SessionStorage = new FileSessionStorage()): Promise<SessionInfo[]> {
|
|
2722
|
+
return listAllSessions(storage);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
/**
|
|
2727
|
+
* If the current session was created by `/move` and contains no real
|
|
2728
|
+
* user/assistant messages, delete it so empty move sessions don't accumulate.
|
|
2729
|
+
*/
|
|
2730
|
+
export async function cleanupEmptyMoveSession(
|
|
2731
|
+
sessionManager: SessionManager,
|
|
2732
|
+
movedFromEmptySessionFile: string | undefined,
|
|
2733
|
+
): Promise<void> {
|
|
2734
|
+
const sessionFile = sessionManager.getSessionFile();
|
|
2735
|
+
if (!sessionFile || !movedFromEmptySessionFile) return;
|
|
2736
|
+
if (path.resolve(sessionFile) !== path.resolve(movedFromEmptySessionFile)) return;
|
|
2737
|
+
const entries = sessionManager.getEntries();
|
|
2738
|
+
const hasRealMessages = entries.some(
|
|
2739
|
+
e => e.type === "message" && (e.message.role === "user" || e.message.role === "assistant"),
|
|
2740
|
+
);
|
|
2741
|
+
if (hasRealMessages) return;
|
|
2742
|
+
try {
|
|
2743
|
+
await sessionManager.dropSession(sessionFile);
|
|
2744
|
+
} catch (err) {
|
|
2745
|
+
logger.warn("Failed to clean up empty move session", { sessionFile, error: String(err) });
|
|
2746
|
+
}
|
|
2747
|
+
}
|