@selesai/code 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/cli/credential-print.js +4 -4
- package/dist/cli.js +0 -0
- package/dist/config.d.ts +5 -3
- package/dist/config.js +6 -4
- package/dist/core/compaction/branch-summarization.js +30 -30
- package/dist/core/compaction/compaction.js +81 -81
- package/dist/core/compaction/utils.js +2 -2
- package/dist/core/export-html/template.css +1066 -1066
- package/dist/core/export-html/template.html +55 -55
- package/dist/core/export-html/template.js +1864 -1864
- package/dist/core/export-html/vendor/highlight.min.js +1212 -1212
- package/dist/core/export-html/vendor/marked.min.js +78 -78
- package/dist/core/messages.js +7 -7
- package/dist/core/skills.d.ts +2 -0
- package/dist/core/skills.js +1 -0
- package/dist/extensions/context-compaction-reminder.test.ts +82 -82
- package/dist/extensions/context-compaction-reminder.ts +28 -28
- package/dist/extensions/pi-intercom/LICENSE +21 -21
- package/dist/extensions/pi-intercom/broker/client.test.ts +83 -83
- package/dist/extensions/pi-intercom/broker/framing.test.ts +114 -114
- package/dist/extensions/pi-intercom/broker/paths.ts +134 -134
- package/dist/extensions/pi-intercom/broker/runtime-claim.test.ts +34 -34
- package/dist/extensions/pi-intercom/broker/runtime-claim.ts +21 -21
- package/dist/extensions/pi-intercom/cwd.test.ts +40 -40
- package/dist/extensions/pi-intercom/cwd.ts +31 -31
- package/dist/extensions/pi-intercom/format-context.test.ts +31 -31
- package/dist/extensions/pi-intercom/format-context.ts +32 -32
- package/dist/extensions/pi-intercom/test/overlay-width.test.ts +66 -66
- package/dist/extensions/pi-intercom/ui/compose.ts +143 -143
- package/dist/extensions/pi-intercom/ui/session-list.ts +166 -166
- package/dist/extensions/pi-rewind-hook/README.md +245 -245
- package/dist/extensions/pi-rewind-hook/index.ts +1445 -1445
- package/dist/extensions/pi-rewind-hook/package.json +29 -29
- package/dist/extensions/pi-subagents/install.mjs +0 -0
- package/dist/extensions/pi-web-agent/package.json +31 -31
- package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -205
- package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -136
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -152
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -25
- package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -28
- package/dist/extensions/pi-web-agent/src/changelog-notice.ts +136 -136
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -946
- package/dist/extensions/pi-web-agent/src/extension.ts +126 -126
- package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -118
- package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -199
- package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -100
- package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -117
- package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -67
- package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -60
- package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -50
- package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -52
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -105
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -45
- package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -28
- package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -47
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -376
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -64
- package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -181
- package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -101
- package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -81
- package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +210 -210
- package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -75
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -61
- package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -54
- package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -41
- package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -20
- package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -63
- package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -114
- package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -72
- package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -96
- package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -71
- package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -31
- package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -31
- package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -157
- package/dist/extensions/pi-web-agent/src/types.ts +88 -88
- package/dist/extensions/ponytail/package.json +8 -8
- package/dist/extensions/question/constants.ts +30 -30
- package/dist/extensions/question/helpers.ts +58 -58
- package/dist/extensions/question/navigation.ts +14 -14
- package/dist/extensions/question/package.json +19 -19
- package/dist/extensions/question/selection-mode.ts +46 -46
- package/dist/extensions/question/shortcuts.ts +44 -44
- package/dist/extensions/question/types.ts +132 -132
- package/dist/extensions/test-resolve-hook-impl.mjs +6 -6
- package/dist/extensions/test-resolve-hook.mjs +6 -6
- package/dist/extensions/web-agent-onboarding.ts +222 -222
- package/dist/modes/interactive/components/settings-selector.d.ts +1 -1
- package/dist/modes/interactive/components/startup-box.d.ts +38 -0
- package/dist/modes/interactive/components/startup-box.js +90 -0
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.js +72 -123
- package/dist/package-manager-cli.js +71 -71
- package/dist/rpc-entry.js +0 -0
- package/dist/skills/agent-browser/SKILL.md +1 -0
- package/dist/skills/batch-grill-me/SKILL.md +1 -0
- package/dist/skills/brandkit/SKILL.md +1 -0
- package/dist/skills/design-references/SKILL.md +1 -0
- package/dist/skills/design-taste-frontend/SKILL.md +1 -0
- package/dist/skills/design-taste-frontend-v1/SKILL.md +1 -0
- package/dist/skills/full-output-enforcement/SKILL.md +1 -0
- package/dist/skills/gpt-taste/SKILL.md +1 -0
- package/dist/skills/grill-me/SKILL.md +1 -0
- package/dist/skills/handoff/SKILL.md +1 -0
- package/dist/skills/handoff-text/SKILL.md +1 -0
- package/dist/skills/high-end-visual-design/SKILL.md +1 -0
- package/dist/skills/image-to-code/SKILL.md +1 -0
- package/dist/skills/imagegen-frontend-mobile/SKILL.md +1 -0
- package/dist/skills/imagegen-frontend-web/SKILL.md +1 -0
- package/dist/skills/implanger/SKILL.md +1 -0
- package/dist/skills/improve-codebase/REFERENCE.md +78 -78
- package/dist/skills/improve-codebase/SKILL.md +1 -0
- package/dist/skills/industrial-brutalist-ui/SKILL.md +1 -0
- package/dist/skills/minimalist-ui/SKILL.md +1 -0
- package/dist/skills/pi-subagents/SKILL.md +1 -0
- package/dist/skills/planger/SKILL.md +1 -0
- package/dist/skills/ponytail/SKILL.md +1 -0
- package/dist/skills/ponytail-audit/SKILL.md +1 -0
- package/dist/skills/ponytail-debt/SKILL.md +1 -0
- package/dist/skills/ponytail-gain/SKILL.md +1 -0
- package/dist/skills/ponytail-help/SKILL.md +1 -0
- package/dist/skills/ponytail-review/SKILL.md +1 -0
- package/dist/skills/redesign-existing-projects/SKILL.md +1 -0
- package/dist/skills/selesai-handoff/SKILL.md +1 -0
- package/dist/skills/stitch-design-taste/SKILL.md +1 -0
- package/dist/skills/unlazy/SKILL.md +1 -0
- package/dist/skills/web-design-guidelines/SKILL.md +1 -0
- package/dist/skills/workflow/SKILL.md +1 -0
- package/docs/compaction.md +396 -396
- package/docs/containerization.md +111 -111
- package/docs/development.md +71 -71
- package/docs/docs.json +164 -164
- package/docs/environment-variables.md +86 -86
- package/docs/index.md +83 -83
- package/docs/json.md +82 -82
- package/docs/models.md +502 -502
- package/docs/packages.md +227 -227
- package/docs/plans/subagent-delegation/phase-0-correctness.md +264 -264
- package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +485 -485
- package/docs/plans/subagent-delegation/phase-2-context-controls.md +281 -281
- package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +361 -361
- package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +380 -380
- package/docs/prompt-templates.md +95 -95
- package/docs/providers.md +293 -293
- package/docs/security.md +59 -59
- package/docs/session-format.md +414 -414
- package/docs/sessions.md +145 -145
- package/docs/shared-host-extensions.md +109 -109
- package/docs/shell-aliases.md +13 -13
- package/docs/skills.md +231 -231
- package/docs/terminal-setup.md +142 -142
- package/docs/termux.md +127 -127
- package/docs/themes.md +295 -295
- package/docs/tmux.md +63 -63
- package/docs/tui.md +927 -927
- package/docs/windows.md +17 -17
- package/examples/README.md +25 -25
- package/examples/extensions/README.md +211 -211
- package/examples/extensions/auto-commit-on-exit.ts +49 -49
- package/examples/extensions/bash-spawn-hook.ts +30 -30
- package/examples/extensions/bookmark.ts +50 -50
- package/examples/extensions/border-status-editor.ts +150 -150
- package/examples/extensions/built-in-tool-renderer.ts +249 -249
- package/examples/extensions/claude-rules.ts +86 -86
- package/examples/extensions/commands.ts +72 -72
- package/examples/extensions/confirm-destructive.ts +59 -59
- package/examples/extensions/custom-compaction.ts +130 -130
- package/examples/extensions/custom-footer.ts +64 -64
- package/examples/extensions/custom-header.ts +73 -73
- package/examples/extensions/custom-provider-anthropic/index.ts +610 -610
- package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -24
- package/examples/extensions/custom-provider-anthropic/package.json +19 -19
- package/examples/extensions/custom-provider-gitlab-duo/index.ts +404 -404
- package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -16
- package/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -82
- package/examples/extensions/dirty-repo-guard.ts +56 -56
- package/examples/extensions/doom-overlay/README.md +46 -46
- package/examples/extensions/doom-overlay/doom/build/doom.js +21 -21
- package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
- package/examples/extensions/doom-overlay/doom/build.sh +152 -152
- package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -72
- package/examples/extensions/doom-overlay/doom-component.ts +132 -132
- package/examples/extensions/doom-overlay/doom-engine.ts +173 -173
- package/examples/extensions/doom-overlay/doom-keys.ts +104 -104
- package/examples/extensions/doom-overlay/index.ts +74 -74
- package/examples/extensions/doom-overlay/wad-finder.ts +51 -51
- package/examples/extensions/dynamic-resources/SKILL.md +8 -8
- package/examples/extensions/dynamic-resources/dynamic.json +79 -79
- package/examples/extensions/dynamic-resources/dynamic.md +5 -5
- package/examples/extensions/dynamic-resources/index.ts +15 -15
- package/examples/extensions/dynamic-tools.ts +74 -74
- package/examples/extensions/event-bus.ts +43 -43
- package/examples/extensions/file-trigger.ts +41 -41
- package/examples/extensions/git-checkpoint.ts +53 -53
- package/examples/extensions/git-merge-and-resolve.ts +115 -115
- package/examples/extensions/github-issue-autocomplete.ts +185 -185
- package/examples/extensions/gondolin/index.ts +531 -531
- package/examples/extensions/gondolin/package-lock.json +185 -185
- package/examples/extensions/gondolin/package.json +19 -19
- package/examples/extensions/handoff.ts +199 -199
- package/examples/extensions/hello.ts +26 -26
- package/examples/extensions/hidden-thinking-label.ts +53 -53
- package/examples/extensions/inline-bash.ts +94 -94
- package/examples/extensions/input-transform-streaming.ts +39 -39
- package/examples/extensions/input-transform.ts +43 -43
- package/examples/extensions/interactive-shell.ts +196 -196
- package/examples/extensions/mac-system-theme.ts +47 -47
- package/examples/extensions/message-renderer.ts +59 -59
- package/examples/extensions/minimal-mode.ts +426 -426
- package/examples/extensions/modal-editor.ts +85 -85
- package/examples/extensions/model-status.ts +31 -31
- package/examples/extensions/notify.ts +55 -55
- package/examples/extensions/overlay-qa-tests.ts +1450 -1450
- package/examples/extensions/overlay-test.ts +153 -153
- package/examples/extensions/permission-gate.ts +34 -34
- package/examples/extensions/pirate.ts +47 -47
- package/examples/extensions/plan-mode/README.md +66 -66
- package/examples/extensions/plan-mode/index.ts +390 -390
- package/examples/extensions/plan-mode/utils.ts +168 -168
- package/examples/extensions/preset.ts +436 -436
- package/examples/extensions/project-trust.ts +64 -64
- package/examples/extensions/prompt-customizer.ts +97 -97
- package/examples/extensions/protected-paths.ts +30 -30
- package/examples/extensions/provider-payload.ts +18 -18
- package/examples/extensions/qna.ts +122 -122
- package/examples/extensions/question.ts +285 -285
- package/examples/extensions/questionnaire.ts +448 -448
- package/examples/extensions/rainbow-editor.ts +88 -88
- package/examples/extensions/reload-runtime.ts +37 -37
- package/examples/extensions/rpc-demo.ts +118 -118
- package/examples/extensions/sandbox/index.ts +321 -321
- package/examples/extensions/sandbox/package-lock.json +92 -92
- package/examples/extensions/sandbox/package.json +19 -19
- package/examples/extensions/send-user-message.ts +97 -97
- package/examples/extensions/session-name.ts +27 -27
- package/examples/extensions/shutdown-command.ts +63 -63
- package/examples/extensions/snake.ts +343 -343
- package/examples/extensions/space-invaders.ts +560 -560
- package/examples/extensions/ssh.ts +220 -220
- package/examples/extensions/status-line.ts +32 -32
- package/examples/extensions/structured-output.ts +65 -65
- package/examples/extensions/subagent/README.md +175 -175
- package/examples/extensions/subagent/agents/planner.md +37 -37
- package/examples/extensions/subagent/agents/reviewer.md +35 -35
- package/examples/extensions/subagent/agents/scout.md +50 -50
- package/examples/extensions/subagent/agents/worker.md +24 -24
- package/examples/extensions/subagent/agents.ts +126 -126
- package/examples/extensions/subagent/index.ts +1015 -1015
- package/examples/extensions/subagent/prompts/implement-and-review.md +10 -10
- package/examples/extensions/subagent/prompts/implement.md +10 -10
- package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -9
- package/examples/extensions/summarize.ts +209 -209
- package/examples/extensions/system-prompt-header.ts +17 -17
- package/examples/extensions/tic-tac-toe.ts +1008 -1008
- package/examples/extensions/timed-confirm.ts +70 -70
- package/examples/extensions/titlebar-spinner.ts +58 -58
- package/examples/extensions/todo.ts +297 -297
- package/examples/extensions/tool-override.ts +144 -144
- package/examples/extensions/tools.ts +146 -146
- package/examples/extensions/trigger-compact.ts +50 -50
- package/examples/extensions/truncated-tool.ts +195 -195
- package/examples/extensions/widget-placement.ts +9 -9
- package/examples/extensions/with-deps/index.ts +32 -32
- package/examples/extensions/with-deps/package-lock.json +31 -31
- package/examples/extensions/with-deps/package.json +22 -22
- package/examples/extensions/working-indicator.ts +123 -123
- package/examples/extensions/working-message-test.ts +25 -25
- package/examples/rpc-extension-ui.ts +632 -632
- package/examples/sdk/01-minimal.ts +26 -26
- package/examples/sdk/02-custom-model.ts +53 -53
- package/examples/sdk/03-custom-prompt.ts +75 -75
- package/examples/sdk/04-skills.ts +55 -55
- package/examples/sdk/05-tools.ts +48 -48
- package/examples/sdk/06-extensions.ts +99 -99
- package/examples/sdk/07-context-files.ts +47 -47
- package/examples/sdk/08-prompt-templates.ts +51 -51
- package/examples/sdk/09-api-keys-and-oauth.ts +52 -52
- package/examples/sdk/10-settings.ts +53 -53
- package/examples/sdk/11-sessions.ts +52 -52
- package/examples/sdk/12-full-control.ts +79 -79
- package/examples/sdk/13-session-runtime.ts +67 -67
- package/examples/sdk/README.md +144 -144
- package/package.json +1 -1
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/cli.d.ts +0 -3
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/register-bedrock.d.ts +0 -2
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/restore-sandbox-env.d.ts +0 -17
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/args.d.ts +0 -57
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/config-selector.d.ts +0 -16
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/file-processor.d.ts +0 -15
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/initial-message.d.ts +0 -18
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/list-models.d.ts +0 -9
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/project-trust.d.ts +0 -10
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/session-picker.d.ts +0 -10
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/startup-ui.d.ts +0 -20
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli.d.ts +0 -3
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/config.d.ts +0 -96
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session-runtime.d.ts +0 -119
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session-services.d.ts +0 -85
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session.d.ts +0 -620
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/auth-guidance.d.ts +0 -5
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/auth-storage.d.ts +0 -56
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/bash-executor.d.ts +0 -32
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/cache-stats.d.ts +0 -49
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/branch-summarization.d.ts +0 -93
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/compaction.d.ts +0 -128
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/index.d.ts +0 -7
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/utils.d.ts +0 -38
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/defaults.d.ts +0 -3
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/diagnostics.d.ts +0 -15
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/event-bus.d.ts +0 -9
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/exec.d.ts +0 -29
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/experimental.d.ts +0 -2
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/ansi-to-html.d.ts +0 -22
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/index.d.ts +0 -37
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/tool-renderer.d.ts +0 -34
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/index.d.ts +0 -12
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/loader.d.ts +0 -23
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/runner.d.ts +0 -171
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +0 -1261
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/wrapper.d.ts +0 -20
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/footer-data-provider.d.ts +0 -54
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/http-dispatcher.d.ts +0 -22
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/index.d.ts +0 -13
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/keybindings.d.ts +0 -358
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/messages.d.ts +0 -77
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-config.d.ts +0 -512
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-registry.d.ts +0 -43
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-resolver.d.ts +0 -121
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-runtime.d.ts +0 -82
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/models-store.d.ts +0 -17
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/output-guard.d.ts +0 -7
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/package-manager.d.ts +0 -210
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/project-trust.d.ts +0 -15
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/prompt-templates.d.ts +0 -54
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/provider-attribution.d.ts +0 -4
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/provider-composer.d.ts +0 -55
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/radius.d.ts +0 -2
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/remote-catalog-provider.d.ts +0 -5
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/resolve-config-value.d.ts +0 -30
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/resource-loader.d.ts +0 -206
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/runtime-credentials.d.ts +0 -15
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/sdk.d.ts +0 -106
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/session-cwd.d.ts +0 -19
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/session-manager.d.ts +0 -356
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/settings-manager.d.ts +0 -297
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/skills.d.ts +0 -60
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/slash-commands.d.ts +0 -15
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/source-info.d.ts +0 -18
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/system-prompt.d.ts +0 -28
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/telemetry.d.ts +0 -3
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/timings.d.ts +0 -10
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/bash.d.ts +0 -68
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/edit-diff.d.ts +0 -106
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/edit.d.ts +0 -51
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/file-mutation-queue.d.ts +0 -6
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/find.d.ts +0 -35
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/grep.d.ts +0 -37
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/index.d.ts +0 -40
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/ls.d.ts +0 -37
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/output-accumulator.d.ts +0 -52
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/path-utils.d.ts +0 -10
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/read.d.ts +0 -35
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/render-utils.d.ts +0 -24
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/tool-definition-wrapper.d.ts +0 -14
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/truncate.d.ts +0 -70
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/write.d.ts +0 -26
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/trust-manager.d.ts +0 -36
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/usage-totals.d.ts +0 -19
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/index.d.ts +0 -3
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/client.d.ts +0 -61
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/huggingface.d.ts +0 -23
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/index.d.ts +0 -3
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/provider.d.ts +0 -10
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/ui.d.ts +0 -42
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/index.d.ts +0 -35
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/main.d.ts +0 -12
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/migrations.d.ts +0 -33
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/index.d.ts +0 -9
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/armin.d.ts +0 -34
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/assistant-message.d.ts +0 -22
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/bash-execution.d.ts +0 -34
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/bordered-loader.d.ts +0 -16
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/branch-summary-message.d.ts +0 -16
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/compaction-summary-message.d.ts +0 -16
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/config-selector.d.ts +0 -102
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/countdown-timer.d.ts +0 -14
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-editor.d.ts +0 -21
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-entry.d.ts +0 -19
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-message.d.ts +0 -20
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/daxnuts.d.ts +0 -23
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/diff.d.ts +0 -12
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/dynamic-border.d.ts +0 -15
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/earendil-announcement.d.ts +0 -5
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-editor.d.ts +0 -22
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-input.d.ts +0 -23
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-selector.d.ts +0 -26
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/first-time-setup.d.ts +0 -25
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/footer.d.ts +0 -32
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/index.d.ts +0 -34
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/keybinding-hints.d.ts +0 -13
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/login-dialog.d.ts +0 -52
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/model-selector.d.ts +0 -54
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/oauth-selector.d.ts +0 -33
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/scoped-models-selector.d.ts +0 -42
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/session-selector-search.d.ts +0 -23
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/session-selector.d.ts +0 -95
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/settings-selector.d.ts +0 -77
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/show-images-selector.d.ts +0 -10
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/skill-invocation-message.d.ts +0 -17
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/status-indicator.d.ts +0 -28
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/theme-selector.d.ts +0 -11
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/thinking-selector.d.ts +0 -11
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/tool-execution.d.ts +0 -63
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/tree-selector.d.ts +0 -94
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/trust-selector.d.ts +0 -23
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/user-message-selector.d.ts +0 -30
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/user-message.d.ts +0 -14
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/visual-truncate.d.ts +0 -24
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/interactive-mode.d.ts +0 -394
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/model-search.d.ts +0 -12
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/theme/theme-controller.d.ts +0 -29
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/theme/theme.d.ts +0 -120
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/print-mode.d.ts +0 -28
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/jsonl.d.ts +0 -17
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-client.d.ts +0 -246
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-mode.d.ts +0 -20
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-types.d.ts +0 -457
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/package-manager-cli.d.ts +0 -8
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/rpc-entry.d.ts +0 -3
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/ansi.d.ts +0 -2
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/changelog.d.ts +0 -22
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/child-process.d.ts +0 -18
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard-image.d.ts +0 -11
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard-native.d.ts +0 -11
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard.d.ts +0 -4
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/deprecation.d.ts +0 -4
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/exif-orientation.d.ts +0 -5
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/frontmatter.d.ts +0 -8
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/fs-watch.d.ts +0 -5
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/git.d.ts +0 -26
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/html.d.ts +0 -7
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-convert.d.ts +0 -10
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-process.d.ts +0 -18
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize-core.d.ts +0 -30
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize-worker.d.ts +0 -2
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize.d.ts +0 -16
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/json.d.ts +0 -3
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/mime.d.ts +0 -3
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/open-browser.d.ts +0 -9
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/paths.d.ts +0 -31
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/photon.d.ts +0 -21
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/pi-user-agent.d.ts +0 -2
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/shell.d.ts +0 -31
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/sleep.d.ts +0 -5
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/syntax-highlight.d.ts +0 -12
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/tools-manager.d.ts +0 -3
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/version-check.d.ts +0 -15
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/windows-self-update.d.ts +0 -3
|
@@ -1,1445 +1,1445 @@
|
|
|
1
|
-
import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@selesai/code";
|
|
2
|
-
import { exec as execCb } from "child_process";
|
|
3
|
-
import { existsSync, readFileSync, realpathSync } from "fs";
|
|
4
|
-
import { mkdtemp, readdir, readFile, rm, stat } from "fs/promises";
|
|
5
|
-
import { tmpdir } from "os";
|
|
6
|
-
import { dirname, isAbsolute, join, relative, resolve } from "path";
|
|
7
|
-
import { promisify } from "util";
|
|
8
|
-
|
|
9
|
-
const execAsync = promisify(execCb);
|
|
10
|
-
|
|
11
|
-
const STORE_REF = "refs/pi-rewind/store";
|
|
12
|
-
const STATUS_KEY = "rewind";
|
|
13
|
-
const FORK_PREFERENCE_SOURCE_ALLOWLIST = new Set(["fork-from-first"]);
|
|
14
|
-
const CHECKPOINT_SOURCE_ALLOWLIST = new Set(["pi-custom-compaction"]);
|
|
15
|
-
const LEGACY_ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
16
|
-
const RETENTION_SWEEP_THRESHOLD = 50;
|
|
17
|
-
const RETENTION_VERSION = 2;
|
|
18
|
-
const EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
19
|
-
|
|
20
|
-
type ExecFn = (cmd: string, args: string[]) => Promise<{ stdout: string; stderr: string; code: number }>;
|
|
21
|
-
|
|
22
|
-
type GitExecResult = Awaited<ReturnType<ExecFn>>;
|
|
23
|
-
|
|
24
|
-
type BindingTuple = [entryId: string, snapshotIndex: number];
|
|
25
|
-
|
|
26
|
-
interface RewindSettings {
|
|
27
|
-
rewind?: {
|
|
28
|
-
silentCheckpoints?: boolean;
|
|
29
|
-
retention?: {
|
|
30
|
-
maxSnapshots?: number;
|
|
31
|
-
maxAgeDays?: number;
|
|
32
|
-
pinLabeledEntries?: boolean;
|
|
33
|
-
scanMode?: "ancestor-only" | "repo-sessions";
|
|
34
|
-
startupBudgetMs?: number;
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
type RewindRetentionSettings = NonNullable<NonNullable<RewindSettings["rewind"]>["retention"]>;
|
|
40
|
-
|
|
41
|
-
interface RewindTurnData {
|
|
42
|
-
v: 2;
|
|
43
|
-
snapshots: string[];
|
|
44
|
-
bindings: BindingTuple[];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface RewindOpData {
|
|
48
|
-
v: 2;
|
|
49
|
-
snapshots: string[];
|
|
50
|
-
bindings?: BindingTuple[];
|
|
51
|
-
current?: number;
|
|
52
|
-
undo?: number;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface RewindForkPendingData {
|
|
56
|
-
v: 2;
|
|
57
|
-
current: string;
|
|
58
|
-
undo?: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
interface ActivePromptCollector {
|
|
62
|
-
snapshots: string[];
|
|
63
|
-
bindings: BindingTuple[];
|
|
64
|
-
promptText?: string;
|
|
65
|
-
pendingUserCommitSha?: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
interface ExactState {
|
|
69
|
-
commitSha: string;
|
|
70
|
-
treeSha: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface ActiveBranchState {
|
|
74
|
-
currentCommitSha?: string;
|
|
75
|
-
currentTreeSha?: string;
|
|
76
|
-
undoCommitSha?: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
interface PendingResultingState {
|
|
80
|
-
currentCommitSha: string;
|
|
81
|
-
undoCommitSha?: string;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
interface ParsedLedgerReference {
|
|
85
|
-
commitSha: string;
|
|
86
|
-
entryId?: string;
|
|
87
|
-
timestamp: number;
|
|
88
|
-
kind: "binding" | "current" | "undo";
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
interface ParsedSessionLedger {
|
|
92
|
-
sessionFile: string;
|
|
93
|
-
sessionId?: string;
|
|
94
|
-
cwd?: string;
|
|
95
|
-
parentSession?: string;
|
|
96
|
-
entryToCommit: Map<string, string>;
|
|
97
|
-
labeledEntryIds: Set<string>;
|
|
98
|
-
references: ParsedLedgerReference[];
|
|
99
|
-
latestCurrentCommitSha?: string;
|
|
100
|
-
latestUndoCommitSha?: string;
|
|
101
|
-
latestForkPending?: RewindForkPendingData;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
interface SessionLikeMessageEntry {
|
|
105
|
-
type: "message";
|
|
106
|
-
id: string;
|
|
107
|
-
parentId: string | null;
|
|
108
|
-
timestamp: string;
|
|
109
|
-
message: {
|
|
110
|
-
role: string;
|
|
111
|
-
timestamp?: number;
|
|
112
|
-
content?: unknown;
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
interface SessionLikeCustomEntry {
|
|
117
|
-
type: "custom";
|
|
118
|
-
id: string;
|
|
119
|
-
parentId: string | null;
|
|
120
|
-
timestamp: string;
|
|
121
|
-
customType: string;
|
|
122
|
-
data?: unknown;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
interface SessionLikeLabelEntry {
|
|
126
|
-
type: "label";
|
|
127
|
-
targetId: string;
|
|
128
|
-
label?: string;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
interface SessionLikeBranchSummaryEntry {
|
|
132
|
-
type: "branch_summary";
|
|
133
|
-
id: string;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
interface SessionLikeGenericEntry {
|
|
137
|
-
type: string;
|
|
138
|
-
id?: string;
|
|
139
|
-
parentId?: string | null;
|
|
140
|
-
timestamp?: string;
|
|
141
|
-
message?: {
|
|
142
|
-
role?: string;
|
|
143
|
-
timestamp?: number;
|
|
144
|
-
content?: unknown;
|
|
145
|
-
};
|
|
146
|
-
customType?: string;
|
|
147
|
-
data?: unknown;
|
|
148
|
-
targetId?: string;
|
|
149
|
-
label?: string;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
type SessionLikeEntry =
|
|
153
|
-
| SessionLikeMessageEntry
|
|
154
|
-
| SessionLikeCustomEntry
|
|
155
|
-
| SessionLikeLabelEntry
|
|
156
|
-
| SessionLikeBranchSummaryEntry
|
|
157
|
-
| SessionLikeGenericEntry;
|
|
158
|
-
|
|
159
|
-
let cachedSettings: RewindSettings | null = null;
|
|
160
|
-
|
|
161
|
-
function getSettingsFilePath(): string {
|
|
162
|
-
return join(getAgentDir(), "settings.json");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function getDefaultSessionsDir(): string {
|
|
166
|
-
return join(getAgentDir(), "sessions");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function getSettings(): RewindSettings {
|
|
170
|
-
if (cachedSettings) {
|
|
171
|
-
return cachedSettings;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
cachedSettings = JSON.parse(readFileSync(getSettingsFilePath(), "utf-8")) as RewindSettings;
|
|
176
|
-
} catch {
|
|
177
|
-
// Invalid/missing settings must not disable rewind; fall back to defaults.
|
|
178
|
-
cachedSettings = {};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return cachedSettings;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function getSilentCheckpointsSetting(): boolean {
|
|
185
|
-
return getSettings().rewind?.silentCheckpoints === true;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function getRetentionSettings(): RewindRetentionSettings | undefined {
|
|
189
|
-
return getSettings().rewind?.retention;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function getRetentionScanMode(): "ancestor-only" | "repo-sessions" {
|
|
193
|
-
return getRetentionSettings()?.scanMode === "repo-sessions" ? "repo-sessions" : "ancestor-only";
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function getStartupSweepBudgetMs(): number | undefined {
|
|
197
|
-
const value = getRetentionSettings()?.startupBudgetMs;
|
|
198
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
199
|
-
return undefined;
|
|
200
|
-
}
|
|
201
|
-
return value;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function isRewindTurnData(value: unknown): value is RewindTurnData {
|
|
205
|
-
if (!value || typeof value !== "object") return false;
|
|
206
|
-
const data = value as Partial<RewindTurnData>;
|
|
207
|
-
return data.v === 2 && Array.isArray(data.snapshots) && Array.isArray(data.bindings);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function isRewindOpData(value: unknown): value is RewindOpData {
|
|
211
|
-
if (!value || typeof value !== "object") return false;
|
|
212
|
-
const data = value as Partial<RewindOpData>;
|
|
213
|
-
return data.v === 2 && Array.isArray(data.snapshots);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function isRewindForkPendingData(value: unknown): value is RewindForkPendingData {
|
|
217
|
-
if (!value || typeof value !== "object") return false;
|
|
218
|
-
const data = value as Partial<RewindForkPendingData>;
|
|
219
|
-
return data.v === 2 && typeof data.current === "string" && data.current.length > 0;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function canonicalizePath(value: string): string {
|
|
223
|
-
const resolvedValue = resolve(value);
|
|
224
|
-
try {
|
|
225
|
-
return realpathSync.native(resolvedValue);
|
|
226
|
-
} catch {
|
|
227
|
-
// Path may not exist yet; compare with the resolved path directly.
|
|
228
|
-
return resolvedValue;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function isInsidePath(targetPath: string, parentPath: string): boolean {
|
|
233
|
-
const resolvedTarget = canonicalizePath(targetPath);
|
|
234
|
-
const resolvedParent = canonicalizePath(parentPath);
|
|
235
|
-
const rel = relative(resolvedParent, resolvedTarget);
|
|
236
|
-
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function toTimestamp(value: string | undefined): number {
|
|
240
|
-
if (!value) return 0;
|
|
241
|
-
const parsed = Date.parse(value);
|
|
242
|
-
return Number.isFinite(parsed) ? parsed : 0;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function getTextContent(content: unknown): string {
|
|
246
|
-
if (typeof content === "string") return content;
|
|
247
|
-
if (!Array.isArray(content)) return "";
|
|
248
|
-
return content
|
|
249
|
-
.filter((block): block is { type: string; text?: string } => !!block && typeof block === "object")
|
|
250
|
-
.filter((block) => block.type === "text")
|
|
251
|
-
.map((block) => block.text ?? "")
|
|
252
|
-
.join("\n");
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function updateLabelSet(labelIds: Set<string>, entry: SessionLikeLabelEntry) {
|
|
256
|
-
if (!entry.targetId) return;
|
|
257
|
-
if (entry.label && entry.label.trim()) {
|
|
258
|
-
labelIds.add(entry.targetId);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
labelIds.delete(entry.targetId);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function applyBindings(target: Map<string, string>, snapshots: string[], bindings?: BindingTuple[]) {
|
|
265
|
-
if (!bindings) return;
|
|
266
|
-
for (const [entryId, snapshotIndex] of bindings) {
|
|
267
|
-
const commitSha = snapshots[snapshotIndex];
|
|
268
|
-
if (entryId && commitSha) {
|
|
269
|
-
target.set(entryId, commitSha);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function addReferences(target: ParsedLedgerReference[], snapshots: string[], timestamp: number, data: RewindTurnData | RewindOpData) {
|
|
275
|
-
if ("bindings" in data && data.bindings) {
|
|
276
|
-
for (const [entryId, snapshotIndex] of data.bindings) {
|
|
277
|
-
const commitSha = snapshots[snapshotIndex];
|
|
278
|
-
if (!commitSha) continue;
|
|
279
|
-
target.push({ commitSha, entryId, timestamp, kind: "binding" });
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if ("current" in data && typeof data.current === "number") {
|
|
284
|
-
const commitSha = snapshots[data.current];
|
|
285
|
-
if (commitSha) {
|
|
286
|
-
target.push({ commitSha, timestamp, kind: "current" });
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if ("undo" in data && typeof data.undo === "number") {
|
|
291
|
-
const commitSha = snapshots[data.undo];
|
|
292
|
-
if (commitSha) {
|
|
293
|
-
target.push({ commitSha, timestamp, kind: "undo" });
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function resolveBindingSnapshotIndex(snapshots: string[], commitSha: string): number {
|
|
299
|
-
const existingIndex = snapshots.indexOf(commitSha);
|
|
300
|
-
if (existingIndex >= 0) return existingIndex;
|
|
301
|
-
snapshots.push(commitSha);
|
|
302
|
-
return snapshots.length - 1;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function addBindingToCollector(collector: ActivePromptCollector, entryId: string, commitSha: string) {
|
|
306
|
-
const snapshotIndex = resolveBindingSnapshotIndex(collector.snapshots, commitSha);
|
|
307
|
-
collector.bindings.push([entryId, snapshotIndex]);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function getCommitFromData(data: RewindOpData, indexKey: "current" | "undo"): string | undefined {
|
|
311
|
-
const snapshotIndex = data[indexKey];
|
|
312
|
-
return typeof snapshotIndex === "number" ? data.snapshots[snapshotIndex] : undefined;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function isRestorableTreeEntry(entry: SessionLikeEntry | undefined): boolean {
|
|
316
|
-
if (!entry) return false;
|
|
317
|
-
if (entry.type === "message") {
|
|
318
|
-
return entry.message.role === "user" || entry.message.role === "assistant";
|
|
319
|
-
}
|
|
320
|
-
return entry.type === "branch_summary" || entry.type === "compaction" || entry.type === "custom_message";
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function isAssistantMessageEntry(entry: SessionLikeEntry): entry is SessionLikeMessageEntry {
|
|
324
|
-
return entry.type === "message" && entry.message.role === "assistant";
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function findLatestUserMessageEntry(entries: SessionLikeEntry[]): SessionLikeMessageEntry | null {
|
|
328
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
329
|
-
const entry = entries[index];
|
|
330
|
-
if (entry.type === "message" && entry.message.role === "user") {
|
|
331
|
-
return entry;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
return null;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function findLatestMatchingUserMessageEntry(
|
|
338
|
-
entries: SessionLikeEntry[],
|
|
339
|
-
promptText: string | null | undefined,
|
|
340
|
-
): SessionLikeMessageEntry | null {
|
|
341
|
-
if (!promptText) return null;
|
|
342
|
-
|
|
343
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
344
|
-
const entry = entries[index];
|
|
345
|
-
if (entry.type !== "message" || entry.message.role !== "user") continue;
|
|
346
|
-
if (getTextContent(entry.message.content) === promptText) {
|
|
347
|
-
return entry;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function findAssistantEntryForTurn(entries: SessionLikeEntry[], message: { timestamp?: number; content?: unknown }): SessionLikeMessageEntry | null {
|
|
355
|
-
const targetTimestamp = message.timestamp;
|
|
356
|
-
const targetText = getTextContent(message.content);
|
|
357
|
-
|
|
358
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
359
|
-
const entry = entries[index];
|
|
360
|
-
if (!isAssistantMessageEntry(entry)) continue;
|
|
361
|
-
|
|
362
|
-
if (targetTimestamp !== undefined && entry.message.timestamp === targetTimestamp) {
|
|
363
|
-
return entry;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (targetText && getTextContent(entry.message.content) === targetText) {
|
|
367
|
-
return entry;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return null;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
export default function rewindExtension(pi: ExtensionAPI) {
|
|
375
|
-
const entryToCommit = new Map<string, string>();
|
|
376
|
-
const parsedSessionCache = new Map<string, { mtimeMs: number; ledger: ParsedSessionLedger }>();
|
|
377
|
-
|
|
378
|
-
let repoRoot: string | null = null;
|
|
379
|
-
let sessionId: string | null = null;
|
|
380
|
-
let currentSessionFile: string | undefined;
|
|
381
|
-
let currentParentSession: string | undefined;
|
|
382
|
-
let currentSessionCwd: string | undefined;
|
|
383
|
-
let isGitRepo = false;
|
|
384
|
-
let lastExact: ExactState | null = null;
|
|
385
|
-
let activeBranchState: ActiveBranchState = {};
|
|
386
|
-
let promptCollector: ActivePromptCollector | null = null;
|
|
387
|
-
let pendingTreeState: PendingResultingState | null = null;
|
|
388
|
-
let activePromptText: string | null = null;
|
|
389
|
-
let newSnapshotsSinceSweep = 0;
|
|
390
|
-
let sweepRunning = false;
|
|
391
|
-
let sweepCompletedThisSession = false;
|
|
392
|
-
let forceConversationOnlyOnNextFork = false;
|
|
393
|
-
let forceConversationOnlySource: string | null = null;
|
|
394
|
-
|
|
395
|
-
function notify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info") {
|
|
396
|
-
if (!ctx.hasUI) return;
|
|
397
|
-
if (level === "info" && getSilentCheckpointsSetting()) return;
|
|
398
|
-
ctx.ui.notify(message, level);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function updateStatus(ctx: ExtensionContext) {
|
|
402
|
-
if (!ctx.hasUI) return;
|
|
403
|
-
if (!isGitRepo || getSilentCheckpointsSetting()) {
|
|
404
|
-
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const uniqueSnapshots = new Set(entryToCommit.values()).size;
|
|
409
|
-
const theme = ctx.ui.theme;
|
|
410
|
-
ctx.ui.setStatus(
|
|
411
|
-
STATUS_KEY,
|
|
412
|
-
theme.fg("dim", "◆ ") + theme.fg("muted", `${entryToCommit.size} points / ${uniqueSnapshots} snapshots`),
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function resetState() {
|
|
417
|
-
entryToCommit.clear();
|
|
418
|
-
parsedSessionCache.clear();
|
|
419
|
-
repoRoot = null;
|
|
420
|
-
sessionId = null;
|
|
421
|
-
currentSessionFile = undefined;
|
|
422
|
-
currentParentSession = undefined;
|
|
423
|
-
currentSessionCwd = undefined;
|
|
424
|
-
isGitRepo = false;
|
|
425
|
-
lastExact = null;
|
|
426
|
-
activeBranchState = {};
|
|
427
|
-
promptCollector = null;
|
|
428
|
-
pendingTreeState = null;
|
|
429
|
-
activePromptText = null;
|
|
430
|
-
newSnapshotsSinceSweep = 0;
|
|
431
|
-
sweepCompletedThisSession = false;
|
|
432
|
-
forceConversationOnlyOnNextFork = false;
|
|
433
|
-
forceConversationOnlySource = null;
|
|
434
|
-
cachedSettings = null;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function syncSessionIdentity(ctx: ExtensionContext) {
|
|
438
|
-
sessionId = ctx.sessionManager.getSessionId();
|
|
439
|
-
currentSessionFile = ctx.sessionManager.getSessionFile();
|
|
440
|
-
currentParentSession = ctx.sessionManager.getHeader()?.parentSession;
|
|
441
|
-
currentSessionCwd = ctx.sessionManager.getCwd();
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
async function execGitChecked(args: string[]): Promise<GitExecResult> {
|
|
445
|
-
const result = await pi.exec("git", args);
|
|
446
|
-
if (result.code !== 0) {
|
|
447
|
-
const stderr = result.stderr.trim();
|
|
448
|
-
throw new Error(stderr || `git ${args.join(" ")} failed with code ${result.code}`);
|
|
449
|
-
}
|
|
450
|
-
return result;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
async function getRepoRoot(exec: ExecFn): Promise<string> {
|
|
454
|
-
if (repoRoot) return repoRoot;
|
|
455
|
-
const result = await exec("git", ["rev-parse", "--show-toplevel"]);
|
|
456
|
-
if (result.code !== 0) {
|
|
457
|
-
const stderr = result.stderr.trim();
|
|
458
|
-
throw new Error(stderr || `git rev-parse --show-toplevel failed with code ${result.code}`);
|
|
459
|
-
}
|
|
460
|
-
repoRoot = result.stdout.trim();
|
|
461
|
-
return repoRoot;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
async function captureWorktreeTree(): Promise<{ treeSha: string }> {
|
|
465
|
-
const root = await getRepoRoot(pi.exec);
|
|
466
|
-
const tempDir = await mkdtemp(join(tmpdir(), "pi-rewind-"));
|
|
467
|
-
const tempIndex = join(tempDir, "index");
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
471
|
-
await execAsync("git add -A", { cwd: root, env });
|
|
472
|
-
const { stdout } = await execAsync("git write-tree", { cwd: root, env });
|
|
473
|
-
return { treeSha: stdout.trim() };
|
|
474
|
-
} finally {
|
|
475
|
-
await rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
476
|
-
// Best effort cleanup for temporary index directory.
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
async function getCommitTreeSha(commitSha: string): Promise<string> {
|
|
482
|
-
const result = await execGitChecked(["show", "-s", "--format=%T", commitSha]);
|
|
483
|
-
return result.stdout.trim();
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
async function commitExists(commitSha: string): Promise<boolean> {
|
|
487
|
-
const result = await pi.exec("git", ["cat-file", "-e", `${commitSha}^{commit}`]);
|
|
488
|
-
return result.code === 0;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
async function getStoreHead(): Promise<string | undefined> {
|
|
492
|
-
const result = await pi.exec("git", ["rev-parse", "--verify", STORE_REF]);
|
|
493
|
-
if (result.code !== 0) {
|
|
494
|
-
return undefined;
|
|
495
|
-
}
|
|
496
|
-
const value = result.stdout.trim();
|
|
497
|
-
return value || undefined;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
async function createStoreKeepaliveCommit(snapshotCommitSha: string, previousStoreHead?: string): Promise<string> {
|
|
501
|
-
const args = ["commit-tree", EMPTY_TREE_SHA];
|
|
502
|
-
|
|
503
|
-
if (previousStoreHead) {
|
|
504
|
-
args.push("-p", previousStoreHead);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
args.push("-p", snapshotCommitSha, "-m", "pi rewind store");
|
|
508
|
-
const result = await execGitChecked(args);
|
|
509
|
-
return result.stdout.trim();
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
async function appendSnapshotToStore(commitSha: string): Promise<void> {
|
|
513
|
-
let attempts = 0;
|
|
514
|
-
let lastError: unknown;
|
|
515
|
-
|
|
516
|
-
while (attempts < 5) {
|
|
517
|
-
attempts += 1;
|
|
518
|
-
const oldHead = await getStoreHead();
|
|
519
|
-
const keepaliveCommit = await createStoreKeepaliveCommit(commitSha, oldHead);
|
|
520
|
-
|
|
521
|
-
try {
|
|
522
|
-
if (oldHead) {
|
|
523
|
-
await execGitChecked(["update-ref", STORE_REF, keepaliveCommit, oldHead]);
|
|
524
|
-
} else {
|
|
525
|
-
await execGitChecked(["update-ref", STORE_REF, keepaliveCommit, LEGACY_ZERO_SHA]);
|
|
526
|
-
}
|
|
527
|
-
return;
|
|
528
|
-
} catch (error) {
|
|
529
|
-
// Retry if another process updated the store ref concurrently.
|
|
530
|
-
// Keep the most recent error for actionable failure context.
|
|
531
|
-
lastError = error;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const detail = lastError instanceof Error ? lastError.message : String(lastError);
|
|
536
|
-
throw new Error(`failed to update rewind store ref: ${detail}`);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
async function rewriteStoreToLiveSet(liveCommitShas: string[]): Promise<"rewritten" | "preserved-empty"> {
|
|
540
|
-
const uniqueLiveCommits = [...new Set(liveCommitShas.filter(Boolean))];
|
|
541
|
-
if (uniqueLiveCommits.length === 0) {
|
|
542
|
-
return "preserved-empty";
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
let head: string | undefined;
|
|
546
|
-
for (const commitSha of uniqueLiveCommits) {
|
|
547
|
-
head = await createStoreKeepaliveCommit(commitSha, head);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const oldHead = await getStoreHead();
|
|
551
|
-
if (oldHead) {
|
|
552
|
-
await execGitChecked(["update-ref", STORE_REF, head!, oldHead]);
|
|
553
|
-
return "rewritten";
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
await execGitChecked(["update-ref", STORE_REF, head!, LEGACY_ZERO_SHA]);
|
|
557
|
-
return "rewritten";
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
async function ensureSnapshotForTree(treeSha: string): Promise<string> {
|
|
561
|
-
if (lastExact && lastExact.treeSha === treeSha) {
|
|
562
|
-
return lastExact.commitSha;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const result = await execGitChecked(["commit-tree", treeSha, "-m", "pi rewind snapshot"]);
|
|
566
|
-
const commitSha = result.stdout.trim();
|
|
567
|
-
await appendSnapshotToStore(commitSha);
|
|
568
|
-
lastExact = { commitSha, treeSha };
|
|
569
|
-
newSnapshotsSinceSweep += 1;
|
|
570
|
-
return commitSha;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
async function ensureSnapshotForCurrentWorktree(): Promise<string> {
|
|
574
|
-
const { treeSha } = await captureWorktreeTree();
|
|
575
|
-
return ensureSnapshotForTree(treeSha);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
async function deletePathsFromWorkingTree(paths: string[]) {
|
|
579
|
-
if (paths.length === 0) return;
|
|
580
|
-
const root = await getRepoRoot(pi.exec);
|
|
581
|
-
|
|
582
|
-
for (const repoRelativePath of paths) {
|
|
583
|
-
const absolutePath = resolve(root, repoRelativePath);
|
|
584
|
-
if (!isInsidePath(absolutePath, root)) {
|
|
585
|
-
throw new Error(`refusing to delete path outside repo root: ${repoRelativePath}`);
|
|
586
|
-
}
|
|
587
|
-
await rm(absolutePath, { recursive: true, force: true });
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
async function getDeletedPaths(currentTreeSha: string, targetTreeSha: string): Promise<string[]> {
|
|
592
|
-
const result = await execGitChecked([
|
|
593
|
-
"diff",
|
|
594
|
-
"--name-only",
|
|
595
|
-
"--diff-filter=D",
|
|
596
|
-
"-z",
|
|
597
|
-
currentTreeSha,
|
|
598
|
-
targetTreeSha,
|
|
599
|
-
"--",
|
|
600
|
-
]);
|
|
601
|
-
|
|
602
|
-
return result.stdout.split("\0").filter(Boolean);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
async function restoreCommitExactly(targetCommitSha: string): Promise<{ changed: boolean; undoCommitSha?: string; targetTreeSha: string }> {
|
|
606
|
-
const { treeSha: currentTreeSha } = await captureWorktreeTree();
|
|
607
|
-
const targetTreeSha = await getCommitTreeSha(targetCommitSha);
|
|
608
|
-
|
|
609
|
-
if (currentTreeSha === targetTreeSha) {
|
|
610
|
-
lastExact = { commitSha: targetCommitSha, treeSha: targetTreeSha };
|
|
611
|
-
return { changed: false, targetTreeSha };
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
const undoCommitSha = await ensureSnapshotForTree(currentTreeSha);
|
|
615
|
-
const pathsToDelete = await getDeletedPaths(currentTreeSha, targetTreeSha);
|
|
616
|
-
await deletePathsFromWorkingTree(pathsToDelete);
|
|
617
|
-
await execGitChecked(["restore", `--source=${targetCommitSha}`, "--worktree", "--", "."]);
|
|
618
|
-
lastExact = { commitSha: targetCommitSha, treeSha: targetTreeSha };
|
|
619
|
-
return { changed: true, undoCommitSha, targetTreeSha };
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function bindPendingPromptUser(entries: SessionLikeEntry[], collector: ActivePromptCollector) {
|
|
623
|
-
if (!collector.pendingUserCommitSha) return;
|
|
624
|
-
|
|
625
|
-
const userEntry = findLatestMatchingUserMessageEntry(entries, collector.promptText) ?? findLatestUserMessageEntry(entries);
|
|
626
|
-
if (!userEntry) return;
|
|
627
|
-
if (collector.bindings.some(([entryId]) => entryId === userEntry.id)) {
|
|
628
|
-
collector.pendingUserCommitSha = undefined;
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
addBindingToCollector(collector, userEntry.id, collector.pendingUserCommitSha);
|
|
633
|
-
collector.pendingUserCommitSha = undefined;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function appendRewindTurn(ctx: ExtensionContext, collector: ActivePromptCollector) {
|
|
637
|
-
if (collector.bindings.length === 0) return;
|
|
638
|
-
|
|
639
|
-
const data: RewindTurnData = {
|
|
640
|
-
v: RETENTION_VERSION,
|
|
641
|
-
snapshots: collector.snapshots,
|
|
642
|
-
bindings: collector.bindings,
|
|
643
|
-
};
|
|
644
|
-
|
|
645
|
-
pi.appendEntry("rewind-turn", data);
|
|
646
|
-
applyBindings(entryToCommit, data.snapshots, data.bindings);
|
|
647
|
-
|
|
648
|
-
const latestBinding = data.bindings[data.bindings.length - 1];
|
|
649
|
-
if (latestBinding) {
|
|
650
|
-
activeBranchState.currentCommitSha = data.snapshots[latestBinding[1]];
|
|
651
|
-
activeBranchState.currentTreeSha = lastExact?.commitSha === activeBranchState.currentCommitSha ? lastExact.treeSha : undefined;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
updateStatus(ctx);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function appendRewindOp(ctx: ExtensionContext, data: RewindOpData) {
|
|
658
|
-
const hasBindings = Boolean(data.bindings?.length);
|
|
659
|
-
const hasCurrent = typeof data.current === "number";
|
|
660
|
-
const hasUndo = typeof data.undo === "number";
|
|
661
|
-
if (!hasBindings && !hasCurrent && !hasUndo) return;
|
|
662
|
-
|
|
663
|
-
pi.appendEntry("rewind-op", data);
|
|
664
|
-
applyBindings(entryToCommit, data.snapshots, data.bindings);
|
|
665
|
-
|
|
666
|
-
const currentCommitSha = getCommitFromData(data, "current");
|
|
667
|
-
if (currentCommitSha) {
|
|
668
|
-
activeBranchState.currentCommitSha = currentCommitSha;
|
|
669
|
-
activeBranchState.currentTreeSha = lastExact?.commitSha === currentCommitSha ? lastExact.treeSha : undefined;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const undoCommitSha = getCommitFromData(data, "undo");
|
|
673
|
-
if (undoCommitSha) {
|
|
674
|
-
activeBranchState.undoCommitSha = undoCommitSha;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
updateStatus(ctx);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function appendForkPendingState(data: PendingResultingState) {
|
|
681
|
-
const forkPending: RewindForkPendingData = {
|
|
682
|
-
v: RETENTION_VERSION,
|
|
683
|
-
current: data.currentCommitSha,
|
|
684
|
-
};
|
|
685
|
-
|
|
686
|
-
if (data.undoCommitSha) {
|
|
687
|
-
forkPending.undo = data.undoCommitSha;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
pi.appendEntry("rewind-fork-pending", forkPending);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function buildCurrentSessionLedger(ctx: ExtensionContext): ParsedSessionLedger {
|
|
694
|
-
const ledger: ParsedSessionLedger = {
|
|
695
|
-
sessionFile: currentSessionFile ?? "",
|
|
696
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
697
|
-
cwd: ctx.sessionManager.getCwd(),
|
|
698
|
-
parentSession: ctx.sessionManager.getHeader()?.parentSession,
|
|
699
|
-
entryToCommit: new Map<string, string>(),
|
|
700
|
-
labeledEntryIds: new Set<string>(),
|
|
701
|
-
references: [],
|
|
702
|
-
};
|
|
703
|
-
|
|
704
|
-
for (const rawEntry of ctx.sessionManager.getEntries() as SessionLikeEntry[]) {
|
|
705
|
-
if (rawEntry.type === "custom" && rawEntry.customType === "rewind-turn" && isRewindTurnData(rawEntry.data)) {
|
|
706
|
-
applyBindings(ledger.entryToCommit, rawEntry.data.snapshots, rawEntry.data.bindings);
|
|
707
|
-
addReferences(ledger.references, rawEntry.data.snapshots, toTimestamp(rawEntry.timestamp), rawEntry.data);
|
|
708
|
-
continue;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
if (rawEntry.type === "custom" && rawEntry.customType === "rewind-op" && isRewindOpData(rawEntry.data)) {
|
|
712
|
-
applyBindings(ledger.entryToCommit, rawEntry.data.snapshots, rawEntry.data.bindings);
|
|
713
|
-
addReferences(ledger.references, rawEntry.data.snapshots, toTimestamp(rawEntry.timestamp), rawEntry.data);
|
|
714
|
-
const currentCommitSha = getCommitFromData(rawEntry.data, "current");
|
|
715
|
-
if (currentCommitSha) ledger.latestCurrentCommitSha = currentCommitSha;
|
|
716
|
-
const undoCommitSha = getCommitFromData(rawEntry.data, "undo");
|
|
717
|
-
if (undoCommitSha) ledger.latestUndoCommitSha = undoCommitSha;
|
|
718
|
-
continue;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
if (rawEntry.type === "custom" && rawEntry.customType === "rewind-fork-pending" && isRewindForkPendingData(rawEntry.data)) {
|
|
722
|
-
ledger.latestForkPending = rawEntry.data;
|
|
723
|
-
continue;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
if (rawEntry.type === "label") {
|
|
727
|
-
updateLabelSet(ledger.labeledEntryIds, rawEntry);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
return ledger;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
async function parseSessionLedgerFile(sessionFile: string): Promise<ParsedSessionLedger | null> {
|
|
735
|
-
try {
|
|
736
|
-
const fileStat = await stat(sessionFile);
|
|
737
|
-
const cached = parsedSessionCache.get(sessionFile);
|
|
738
|
-
if (cached && cached.mtimeMs === fileStat.mtimeMs) {
|
|
739
|
-
return cached.ledger;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
const content = await readFile(sessionFile, "utf-8");
|
|
743
|
-
const ledger: ParsedSessionLedger = {
|
|
744
|
-
sessionFile,
|
|
745
|
-
entryToCommit: new Map<string, string>(),
|
|
746
|
-
labeledEntryIds: new Set<string>(),
|
|
747
|
-
references: [],
|
|
748
|
-
};
|
|
749
|
-
|
|
750
|
-
const hasRewindEntries = content.includes('"rewind-');
|
|
751
|
-
|
|
752
|
-
if (!hasRewindEntries) {
|
|
753
|
-
// Fast path: extract session header only, skip line-by-line JSON parsing
|
|
754
|
-
let pos = 0;
|
|
755
|
-
for (let i = 0; i < 5 && pos < content.length; i++) {
|
|
756
|
-
const nextNewline = content.indexOf("\n", pos);
|
|
757
|
-
const line = nextNewline >= 0 ? content.substring(pos, nextNewline) : content.substring(pos);
|
|
758
|
-
pos = nextNewline >= 0 ? nextNewline + 1 : content.length;
|
|
759
|
-
if (!line) continue;
|
|
760
|
-
try {
|
|
761
|
-
const entry = JSON.parse(line);
|
|
762
|
-
if (entry?.type === "session") {
|
|
763
|
-
ledger.sessionId = entry.id;
|
|
764
|
-
ledger.cwd = entry.cwd;
|
|
765
|
-
ledger.parentSession = entry.parentSession;
|
|
766
|
-
break;
|
|
767
|
-
}
|
|
768
|
-
} catch {
|
|
769
|
-
// Ignore malformed header lines from partial/corrupt session files.
|
|
770
|
-
continue;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
parsedSessionCache.set(sessionFile, { mtimeMs: fileStat.mtimeMs, ledger });
|
|
774
|
-
return ledger;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const lines = content.split("\n").filter(Boolean);
|
|
778
|
-
|
|
779
|
-
for (const line of lines) {
|
|
780
|
-
let entry: SessionLikeGenericEntry | null = null;
|
|
781
|
-
try {
|
|
782
|
-
const parsed = JSON.parse(line);
|
|
783
|
-
if (parsed && typeof parsed === "object") {
|
|
784
|
-
entry = parsed as SessionLikeGenericEntry;
|
|
785
|
-
}
|
|
786
|
-
} catch {
|
|
787
|
-
// Ignore malformed JSONL lines; retention discovery is best-effort.
|
|
788
|
-
continue;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
if (!entry) continue;
|
|
792
|
-
|
|
793
|
-
if (entry?.type === "session") {
|
|
794
|
-
ledger.sessionId = entry.id;
|
|
795
|
-
ledger.cwd = entry.cwd;
|
|
796
|
-
ledger.parentSession = entry.parentSession;
|
|
797
|
-
continue;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
if (entry?.type === "custom" && entry?.customType === "rewind-turn" && isRewindTurnData(entry.data)) {
|
|
801
|
-
applyBindings(ledger.entryToCommit, entry.data.snapshots, entry.data.bindings);
|
|
802
|
-
addReferences(ledger.references, entry.data.snapshots, toTimestamp(entry.timestamp), entry.data);
|
|
803
|
-
continue;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
if (entry?.type === "custom" && entry?.customType === "rewind-op" && isRewindOpData(entry.data)) {
|
|
807
|
-
applyBindings(ledger.entryToCommit, entry.data.snapshots, entry.data.bindings);
|
|
808
|
-
addReferences(ledger.references, entry.data.snapshots, toTimestamp(entry.timestamp), entry.data);
|
|
809
|
-
const currentCommitSha = getCommitFromData(entry.data, "current");
|
|
810
|
-
if (currentCommitSha) ledger.latestCurrentCommitSha = currentCommitSha;
|
|
811
|
-
const undoCommitSha = getCommitFromData(entry.data, "undo");
|
|
812
|
-
if (undoCommitSha) ledger.latestUndoCommitSha = undoCommitSha;
|
|
813
|
-
continue;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
if (entry?.type === "custom" && entry?.customType === "rewind-fork-pending" && isRewindForkPendingData(entry.data)) {
|
|
817
|
-
ledger.latestForkPending = entry.data;
|
|
818
|
-
continue;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
if (entry?.type === "label") {
|
|
822
|
-
updateLabelSet(ledger.labeledEntryIds, entry);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
parsedSessionCache.set(sessionFile, { mtimeMs: fileStat.mtimeMs, ledger });
|
|
827
|
-
return ledger;
|
|
828
|
-
} catch {
|
|
829
|
-
// Unreadable/missing session file: skip it and continue lineage/discovery.
|
|
830
|
-
return null;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
async function resolveEntrySnapshotWithLineage(entryId: string, sessionFile = currentSessionFile): Promise<string | undefined> {
|
|
835
|
-
let cursor = sessionFile;
|
|
836
|
-
|
|
837
|
-
while (cursor) {
|
|
838
|
-
const ledger = cursor === currentSessionFile ? buildCurrentSessionLedgerFromMemory() : await parseSessionLedgerFile(cursor);
|
|
839
|
-
if (!ledger) break;
|
|
840
|
-
|
|
841
|
-
const commitSha = ledger.entryToCommit.get(entryId);
|
|
842
|
-
if (commitSha && (await commitExists(commitSha))) {
|
|
843
|
-
return commitSha;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
cursor = ledger.parentSession;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
return undefined;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
function buildCurrentSessionLedgerFromMemory(): ParsedSessionLedger {
|
|
853
|
-
return {
|
|
854
|
-
sessionFile: currentSessionFile ?? "",
|
|
855
|
-
sessionId: sessionId ?? undefined,
|
|
856
|
-
cwd: currentSessionCwd,
|
|
857
|
-
parentSession: currentParentSession,
|
|
858
|
-
entryToCommit: new Map(entryToCommit),
|
|
859
|
-
labeledEntryIds: new Set(),
|
|
860
|
-
references: [],
|
|
861
|
-
latestCurrentCommitSha: activeBranchState.currentCommitSha,
|
|
862
|
-
latestUndoCommitSha: activeBranchState.undoCommitSha,
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
async function reconstructState(ctx: ExtensionContext) {
|
|
867
|
-
entryToCommit.clear();
|
|
868
|
-
activeBranchState = {};
|
|
869
|
-
lastExact = null;
|
|
870
|
-
|
|
871
|
-
const currentLedger = buildCurrentSessionLedger(ctx);
|
|
872
|
-
for (const [entryId, commitSha] of currentLedger.entryToCommit.entries()) {
|
|
873
|
-
entryToCommit.set(entryId, commitSha);
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
let latestVisibleBindingCommitSha: string | undefined;
|
|
877
|
-
for (const entry of ctx.sessionManager.getBranch() as SessionLikeEntry[]) {
|
|
878
|
-
const boundCommitSha = entry.id ? entryToCommit.get(entry.id) : undefined;
|
|
879
|
-
if (boundCommitSha && isRestorableTreeEntry(entry)) {
|
|
880
|
-
latestVisibleBindingCommitSha = boundCommitSha;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
if (entry.type === "custom" && entry.customType === "rewind-op" && isRewindOpData(entry.data)) {
|
|
884
|
-
const currentCommitSha = getCommitFromData(entry.data, "current");
|
|
885
|
-
if (currentCommitSha) {
|
|
886
|
-
activeBranchState.currentCommitSha = currentCommitSha;
|
|
887
|
-
}
|
|
888
|
-
const undoCommitSha = getCommitFromData(entry.data, "undo");
|
|
889
|
-
if (undoCommitSha) {
|
|
890
|
-
activeBranchState.undoCommitSha = undoCommitSha;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
if (!activeBranchState.currentCommitSha) {
|
|
896
|
-
activeBranchState.currentCommitSha = latestVisibleBindingCommitSha;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
if (activeBranchState.currentCommitSha && (await commitExists(activeBranchState.currentCommitSha))) {
|
|
900
|
-
activeBranchState.currentTreeSha = await getCommitTreeSha(activeBranchState.currentCommitSha);
|
|
901
|
-
const { treeSha: worktreeTreeSha } = await captureWorktreeTree();
|
|
902
|
-
|
|
903
|
-
if (activeBranchState.currentTreeSha === worktreeTreeSha) {
|
|
904
|
-
lastExact = {
|
|
905
|
-
commitSha: activeBranchState.currentCommitSha,
|
|
906
|
-
treeSha: activeBranchState.currentTreeSha,
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
async function discoverSessionFiles(scanMode: "ancestor-only" | "repo-sessions"): Promise<string[]> {
|
|
913
|
-
const discovered = new Set<string>();
|
|
914
|
-
|
|
915
|
-
if (scanMode === "repo-sessions") {
|
|
916
|
-
const roots = new Set<string>();
|
|
917
|
-
const defaultSessionsDir = getDefaultSessionsDir();
|
|
918
|
-
if (existsSync(defaultSessionsDir)) {
|
|
919
|
-
roots.add(defaultSessionsDir);
|
|
920
|
-
}
|
|
921
|
-
if (currentSessionFile) {
|
|
922
|
-
roots.add(dirname(currentSessionFile));
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
const stack = [...roots];
|
|
926
|
-
while (stack.length > 0) {
|
|
927
|
-
const dir = stack.pop();
|
|
928
|
-
if (!dir) continue;
|
|
929
|
-
|
|
930
|
-
let entries: Awaited<ReturnType<typeof readdir>>;
|
|
931
|
-
try {
|
|
932
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
933
|
-
} catch {
|
|
934
|
-
// Skip unreadable directories during best-effort discovery.
|
|
935
|
-
continue;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
for (const entry of entries) {
|
|
939
|
-
const fullPath = join(dir, entry.name);
|
|
940
|
-
if (entry.isDirectory()) {
|
|
941
|
-
stack.push(fullPath);
|
|
942
|
-
continue;
|
|
943
|
-
}
|
|
944
|
-
if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
945
|
-
discovered.add(fullPath);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
let ancestorCursor = currentSessionFile;
|
|
952
|
-
while (ancestorCursor) {
|
|
953
|
-
discovered.add(ancestorCursor);
|
|
954
|
-
const ledger = ancestorCursor === currentSessionFile ? buildCurrentSessionLedgerFromMemory() : await parseSessionLedgerFile(ancestorCursor);
|
|
955
|
-
ancestorCursor = ledger?.parentSession;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
return [...discovered];
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
async function maybeSweepRetention(ctx: ExtensionContext, reason: "startup" | "new-snapshots" | "shutdown") {
|
|
962
|
-
const retention = getRetentionSettings();
|
|
963
|
-
if (!retention) return;
|
|
964
|
-
if (reason === "new-snapshots" && newSnapshotsSinceSweep < RETENTION_SWEEP_THRESHOLD) return;
|
|
965
|
-
if (reason === "shutdown" && sweepCompletedThisSession && newSnapshotsSinceSweep < RETENTION_SWEEP_THRESHOLD) return;
|
|
966
|
-
if (!repoRoot) return;
|
|
967
|
-
if (sweepRunning) return;
|
|
968
|
-
sweepRunning = true;
|
|
969
|
-
try {
|
|
970
|
-
await runRetentionSweep(ctx, reason);
|
|
971
|
-
} finally {
|
|
972
|
-
sweepRunning = false;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
async function runRetentionSweep(ctx: ExtensionContext, reason: "startup" | "new-snapshots" | "shutdown") {
|
|
977
|
-
const retention = getRetentionSettings();
|
|
978
|
-
if (!retention) return;
|
|
979
|
-
|
|
980
|
-
const scanMode = getRetentionScanMode();
|
|
981
|
-
const startupBudgetMs = reason === "startup" ? getStartupSweepBudgetMs() : undefined;
|
|
982
|
-
const startedAt = Date.now();
|
|
983
|
-
|
|
984
|
-
const budgetExceeded = () => typeof startupBudgetMs === "number" && Date.now() - startedAt > startupBudgetMs;
|
|
985
|
-
|
|
986
|
-
const sessionFiles = await discoverSessionFiles(scanMode);
|
|
987
|
-
if (budgetExceeded()) {
|
|
988
|
-
notify(ctx, `Rewind retention startup sweep skipped after exceeding ${startupBudgetMs}ms budget`, "warning");
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
const ledgers: ParsedSessionLedger[] = [];
|
|
993
|
-
|
|
994
|
-
for (const sessionFile of sessionFiles) {
|
|
995
|
-
if (budgetExceeded()) {
|
|
996
|
-
notify(ctx, `Rewind retention startup sweep skipped after exceeding ${startupBudgetMs}ms budget`, "warning");
|
|
997
|
-
return;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
const ledger = sessionFile === currentSessionFile ? buildCurrentSessionLedger(ctx) : await parseSessionLedgerFile(sessionFile);
|
|
1001
|
-
if (!ledger?.cwd) continue;
|
|
1002
|
-
if (!isInsidePath(ledger.cwd, repoRoot)) continue;
|
|
1003
|
-
ledgers.push(ledger);
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
const latestReferenceByCommit = new Map<string, number>();
|
|
1007
|
-
const pinnedCommits = new Set<string>();
|
|
1008
|
-
const currentCommits = new Set<string>();
|
|
1009
|
-
const undoCommits = new Set<string>();
|
|
1010
|
-
|
|
1011
|
-
for (const ledger of ledgers) {
|
|
1012
|
-
if (budgetExceeded()) {
|
|
1013
|
-
notify(ctx, `Rewind retention startup sweep skipped after exceeding ${startupBudgetMs}ms budget`, "warning");
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
for (const reference of ledger.references) {
|
|
1018
|
-
const prev = latestReferenceByCommit.get(reference.commitSha) ?? 0;
|
|
1019
|
-
if (reference.timestamp > prev) {
|
|
1020
|
-
latestReferenceByCommit.set(reference.commitSha, reference.timestamp);
|
|
1021
|
-
}
|
|
1022
|
-
if (reference.kind === "binding" && retention.pinLabeledEntries && reference.entryId && ledger.labeledEntryIds.has(reference.entryId)) {
|
|
1023
|
-
pinnedCommits.add(reference.commitSha);
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
if (ledger.latestCurrentCommitSha) {
|
|
1028
|
-
currentCommits.add(ledger.latestCurrentCommitSha);
|
|
1029
|
-
}
|
|
1030
|
-
if (ledger.latestUndoCommitSha) {
|
|
1031
|
-
undoCommits.add(ledger.latestUndoCommitSha);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
for (const commitSha of [...currentCommits, ...undoCommits]) {
|
|
1036
|
-
if (budgetExceeded()) {
|
|
1037
|
-
notify(ctx, `Rewind retention startup sweep skipped after exceeding ${startupBudgetMs}ms budget`, "warning");
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
if (await commitExists(commitSha)) {
|
|
1041
|
-
pinnedCommits.add(commitSha);
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
let candidates = [...latestReferenceByCommit.entries()]
|
|
1046
|
-
.filter(([commitSha]) => !pinnedCommits.has(commitSha))
|
|
1047
|
-
.sort((left, right) => right[1] - left[1]);
|
|
1048
|
-
|
|
1049
|
-
if (typeof retention.maxAgeDays === "number" && retention.maxAgeDays >= 0) {
|
|
1050
|
-
const cutoff = Date.now() - retention.maxAgeDays * 24 * 60 * 60 * 1000;
|
|
1051
|
-
candidates = candidates.filter(([, timestamp]) => timestamp >= cutoff);
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
if (typeof retention.maxSnapshots === "number" && retention.maxSnapshots >= 0 && candidates.length > retention.maxSnapshots) {
|
|
1055
|
-
candidates = candidates.slice(0, retention.maxSnapshots);
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
const liveSet = [...new Set([...pinnedCommits, ...candidates.map(([commitSha]) => commitSha)])];
|
|
1059
|
-
const existingLiveSet: string[] = [];
|
|
1060
|
-
for (const commitSha of liveSet) {
|
|
1061
|
-
if (budgetExceeded()) {
|
|
1062
|
-
notify(ctx, `Rewind retention startup sweep skipped after exceeding ${startupBudgetMs}ms budget`, "warning");
|
|
1063
|
-
return;
|
|
1064
|
-
}
|
|
1065
|
-
if (await commitExists(commitSha)) {
|
|
1066
|
-
existingLiveSet.push(commitSha);
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
const rewriteResult = await rewriteStoreToLiveSet(existingLiveSet);
|
|
1071
|
-
if (rewriteResult === "preserved-empty") {
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
// Skip gc on background startup sweeps to avoid racing with concurrent snapshot creation
|
|
1076
|
-
if (reason !== "startup") {
|
|
1077
|
-
try {
|
|
1078
|
-
const result = await pi.exec("git", ["gc", "--auto"]);
|
|
1079
|
-
if (result.code !== 0) {
|
|
1080
|
-
throw new Error(result.stderr.trim() || `git gc --auto failed with code ${result.code}`);
|
|
1081
|
-
}
|
|
1082
|
-
} catch {
|
|
1083
|
-
// Best effort only; retention reachability rewrite already completed.
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
newSnapshotsSinceSweep = 0;
|
|
1088
|
-
sweepCompletedThisSession = true;
|
|
1089
|
-
updateStatus(ctx);
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
let activeContext: ExtensionContext | undefined;
|
|
1093
|
-
|
|
1094
|
-
async function checkpointEntry(ctx: ExtensionContext, entryId: string) {
|
|
1095
|
-
syncSessionIdentity(ctx);
|
|
1096
|
-
if (!isGitRepo) return;
|
|
1097
|
-
const entry = ctx.sessionManager.getEntry(entryId) as SessionLikeEntry | undefined;
|
|
1098
|
-
if (!isRestorableTreeEntry(entry)) return;
|
|
1099
|
-
if (entryToCommit.has(entryId)) return;
|
|
1100
|
-
|
|
1101
|
-
const currentCommitSha = await ensureSnapshotForCurrentWorktree();
|
|
1102
|
-
appendRewindOp(ctx, {
|
|
1103
|
-
v: RETENTION_VERSION,
|
|
1104
|
-
snapshots: [currentCommitSha],
|
|
1105
|
-
bindings: [[entryId, 0]],
|
|
1106
|
-
});
|
|
1107
|
-
await reconstructState(ctx);
|
|
1108
|
-
updateStatus(ctx);
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
async function initializeForSession(ctx: ExtensionContext) {
|
|
1112
|
-
activeContext = ctx;
|
|
1113
|
-
resetState();
|
|
1114
|
-
syncSessionIdentity(ctx);
|
|
1115
|
-
|
|
1116
|
-
try {
|
|
1117
|
-
const result = await pi.exec("git", ["rev-parse", "--is-inside-work-tree"]);
|
|
1118
|
-
isGitRepo = result.code === 0 && result.stdout.trim() === "true";
|
|
1119
|
-
} catch {
|
|
1120
|
-
// Treat git probing failures as non-git context for this session.
|
|
1121
|
-
isGitRepo = false;
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
if (!isGitRepo) {
|
|
1125
|
-
updateStatus(ctx);
|
|
1126
|
-
return;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
await getRepoRoot(pi.exec);
|
|
1130
|
-
await reconstructState(ctx);
|
|
1131
|
-
updateStatus(ctx);
|
|
1132
|
-
maybeSweepRetention(ctx, "startup").catch((error) => {
|
|
1133
|
-
notify(ctx, `Rewind retention startup sweep failed: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
pi.events.on("rewind:fork-preference", (data) => {
|
|
1138
|
-
if (!data || typeof data !== "object") return;
|
|
1139
|
-
if (!("mode" in data) || data.mode !== "conversation-only") return;
|
|
1140
|
-
if (!("source" in data) || typeof data.source !== "string") return;
|
|
1141
|
-
if (!FORK_PREFERENCE_SOURCE_ALLOWLIST.has(data.source)) return;
|
|
1142
|
-
forceConversationOnlyOnNextFork = true;
|
|
1143
|
-
forceConversationOnlySource = data.source;
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
pi.events.on("rewind:checkpoint-entry", (data) => {
|
|
1147
|
-
if (!data || typeof data !== "object") return;
|
|
1148
|
-
if (!("source" in data) || typeof data.source !== "string") return;
|
|
1149
|
-
if (!CHECKPOINT_SOURCE_ALLOWLIST.has(data.source)) return;
|
|
1150
|
-
if (!("entryId" in data) || typeof data.entryId !== "string") return;
|
|
1151
|
-
const ctx = activeContext;
|
|
1152
|
-
if (!ctx) return;
|
|
1153
|
-
|
|
1154
|
-
checkpointEntry(ctx, data.entryId).catch((error) => {
|
|
1155
|
-
notify(ctx, `Rewind: failed to checkpoint ${data.entryId} (${error instanceof Error ? error.message : String(error)})`, "warning");
|
|
1156
|
-
});
|
|
1157
|
-
});
|
|
1158
|
-
|
|
1159
|
-
pi.on("before_agent_start", async (event) => {
|
|
1160
|
-
activePromptText = event.prompt;
|
|
1161
|
-
});
|
|
1162
|
-
|
|
1163
|
-
pi.on("session_start", async (event, ctx) => {
|
|
1164
|
-
await initializeForSession(ctx);
|
|
1165
|
-
|
|
1166
|
-
if (event.reason !== "fork" || !event.previousSessionFile || !isGitRepo) {
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
const previousLedger = await parseSessionLedgerFile(event.previousSessionFile);
|
|
1171
|
-
const pendingFork = previousLedger?.latestForkPending;
|
|
1172
|
-
if (!pendingFork) {
|
|
1173
|
-
return;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
if (!(await commitExists(pendingFork.current))) {
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
const snapshots = [pendingFork.current];
|
|
1181
|
-
const data: RewindOpData = { v: RETENTION_VERSION, snapshots, current: 0 };
|
|
1182
|
-
|
|
1183
|
-
if (pendingFork.undo && (await commitExists(pendingFork.undo))) {
|
|
1184
|
-
data.snapshots.push(pendingFork.undo);
|
|
1185
|
-
data.undo = 1;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
appendRewindOp(ctx, data);
|
|
1189
|
-
await reconstructState(ctx);
|
|
1190
|
-
updateStatus(ctx);
|
|
1191
|
-
});
|
|
1192
|
-
|
|
1193
|
-
pi.on("session_tree", async (event, ctx) => {
|
|
1194
|
-
syncSessionIdentity(ctx);
|
|
1195
|
-
if (!isGitRepo || !pendingTreeState) {
|
|
1196
|
-
await reconstructState(ctx);
|
|
1197
|
-
updateStatus(ctx);
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
const snapshots = [pendingTreeState.currentCommitSha];
|
|
1202
|
-
const data: RewindOpData = { v: RETENTION_VERSION, snapshots, current: 0 };
|
|
1203
|
-
if (pendingTreeState.undoCommitSha) {
|
|
1204
|
-
data.snapshots.push(pendingTreeState.undoCommitSha);
|
|
1205
|
-
data.undo = 1;
|
|
1206
|
-
}
|
|
1207
|
-
if (event.summaryEntry?.id) {
|
|
1208
|
-
data.bindings = [[event.summaryEntry.id, 0]];
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
appendRewindOp(ctx, data);
|
|
1212
|
-
pendingTreeState = null;
|
|
1213
|
-
await reconstructState(ctx);
|
|
1214
|
-
updateStatus(ctx);
|
|
1215
|
-
});
|
|
1216
|
-
|
|
1217
|
-
pi.on("session_compact", async (event, ctx) => {
|
|
1218
|
-
syncSessionIdentity(ctx);
|
|
1219
|
-
if (!isGitRepo) return;
|
|
1220
|
-
|
|
1221
|
-
let currentCommitSha = activeBranchState.currentCommitSha;
|
|
1222
|
-
if (!currentCommitSha) {
|
|
1223
|
-
currentCommitSha = await ensureSnapshotForCurrentWorktree();
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
appendRewindOp(ctx, {
|
|
1227
|
-
v: RETENTION_VERSION,
|
|
1228
|
-
snapshots: [currentCommitSha],
|
|
1229
|
-
bindings: [[event.compactionEntry.id, 0]],
|
|
1230
|
-
});
|
|
1231
|
-
await reconstructState(ctx);
|
|
1232
|
-
updateStatus(ctx);
|
|
1233
|
-
});
|
|
1234
|
-
|
|
1235
|
-
pi.on("session_shutdown", async (_event, ctx) => {
|
|
1236
|
-
syncSessionIdentity(ctx);
|
|
1237
|
-
if (!isGitRepo) return;
|
|
1238
|
-
await maybeSweepRetention(ctx, "shutdown");
|
|
1239
|
-
});
|
|
1240
|
-
|
|
1241
|
-
pi.on("turn_start", async (event, ctx) => {
|
|
1242
|
-
if (!isGitRepo) return;
|
|
1243
|
-
if (event.turnIndex !== 0) return;
|
|
1244
|
-
|
|
1245
|
-
try {
|
|
1246
|
-
const { treeSha } = await captureWorktreeTree();
|
|
1247
|
-
const commitSha = await ensureSnapshotForTree(treeSha);
|
|
1248
|
-
promptCollector = {
|
|
1249
|
-
snapshots: [],
|
|
1250
|
-
bindings: [],
|
|
1251
|
-
promptText: activePromptText ?? undefined,
|
|
1252
|
-
pendingUserCommitSha: commitSha,
|
|
1253
|
-
};
|
|
1254
|
-
|
|
1255
|
-
bindPendingPromptUser(ctx.sessionManager.getBranch() as SessionLikeEntry[], promptCollector);
|
|
1256
|
-
} catch (error) {
|
|
1257
|
-
promptCollector = null;
|
|
1258
|
-
notify(ctx, `Rewind: failed to capture start snapshot (${error instanceof Error ? error.message : String(error)})`, "warning");
|
|
1259
|
-
}
|
|
1260
|
-
});
|
|
1261
|
-
|
|
1262
|
-
pi.on("turn_end", async (event, ctx) => {
|
|
1263
|
-
if (!isGitRepo || !promptCollector) return;
|
|
1264
|
-
|
|
1265
|
-
try {
|
|
1266
|
-
const branchEntries = ctx.sessionManager.getBranch() as SessionLikeEntry[];
|
|
1267
|
-
bindPendingPromptUser(branchEntries, promptCollector);
|
|
1268
|
-
|
|
1269
|
-
if (event.message.role !== "assistant") return;
|
|
1270
|
-
|
|
1271
|
-
const assistantEntry = findAssistantEntryForTurn(branchEntries, event.message);
|
|
1272
|
-
if (!assistantEntry) return;
|
|
1273
|
-
|
|
1274
|
-
const { treeSha } = await captureWorktreeTree();
|
|
1275
|
-
const commitSha = await ensureSnapshotForTree(treeSha);
|
|
1276
|
-
addBindingToCollector(promptCollector, assistantEntry.id, commitSha);
|
|
1277
|
-
} catch (error) {
|
|
1278
|
-
notify(ctx, `Rewind: failed to capture assistant snapshot (${error instanceof Error ? error.message : String(error)})`, "warning");
|
|
1279
|
-
}
|
|
1280
|
-
});
|
|
1281
|
-
|
|
1282
|
-
pi.on("agent_end", async (_event, ctx) => {
|
|
1283
|
-
if (!isGitRepo || !promptCollector) return;
|
|
1284
|
-
|
|
1285
|
-
try {
|
|
1286
|
-
bindPendingPromptUser(ctx.sessionManager.getBranch() as SessionLikeEntry[], promptCollector);
|
|
1287
|
-
appendRewindTurn(ctx, promptCollector);
|
|
1288
|
-
await reconstructState(ctx);
|
|
1289
|
-
updateStatus(ctx);
|
|
1290
|
-
await maybeSweepRetention(ctx, "new-snapshots");
|
|
1291
|
-
} catch (error) {
|
|
1292
|
-
notify(ctx, `Rewind: failed to finalize rewind turn (${error instanceof Error ? error.message : String(error)})`, "warning");
|
|
1293
|
-
} finally {
|
|
1294
|
-
promptCollector = null;
|
|
1295
|
-
activePromptText = null;
|
|
1296
|
-
}
|
|
1297
|
-
});
|
|
1298
|
-
|
|
1299
|
-
pi.on("session_before_fork", async (event, ctx) => {
|
|
1300
|
-
const shouldForceConversationOnly = forceConversationOnlyOnNextFork;
|
|
1301
|
-
const forcedBySource = forceConversationOnlySource;
|
|
1302
|
-
forceConversationOnlyOnNextFork = false;
|
|
1303
|
-
forceConversationOnlySource = null;
|
|
1304
|
-
|
|
1305
|
-
if (!isGitRepo) return;
|
|
1306
|
-
|
|
1307
|
-
try {
|
|
1308
|
-
if (!ctx.hasUI) {
|
|
1309
|
-
const nextState = { currentCommitSha: await ensureSnapshotForCurrentWorktree() };
|
|
1310
|
-
appendForkPendingState(nextState);
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
const targetCommitSha = await resolveEntrySnapshotWithLineage(event.entryId);
|
|
1315
|
-
const hasUndo = Boolean(activeBranchState.undoCommitSha && (await commitExists(activeBranchState.undoCommitSha)));
|
|
1316
|
-
|
|
1317
|
-
if (shouldForceConversationOnly) {
|
|
1318
|
-
const nextState = { currentCommitSha: await ensureSnapshotForCurrentWorktree() };
|
|
1319
|
-
appendForkPendingState(nextState);
|
|
1320
|
-
notify(ctx, `Rewind: using conversation-only fork (keep current files)${forcedBySource ? ` (${forcedBySource})` : ""}`);
|
|
1321
|
-
return;
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
const options = ["Conversation only (keep current files)"];
|
|
1325
|
-
if (targetCommitSha) {
|
|
1326
|
-
options.push("Restore all (files + conversation)", "Code only (restore files, keep conversation)");
|
|
1327
|
-
}
|
|
1328
|
-
if (hasUndo) {
|
|
1329
|
-
options.push("Undo last file rewind");
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
const choice = await ctx.ui.select("Restore Options", options);
|
|
1333
|
-
if (!choice) {
|
|
1334
|
-
notify(ctx, "Rewind cancelled");
|
|
1335
|
-
return { cancel: true };
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
if (choice === "Undo last file rewind") {
|
|
1339
|
-
const restore = await restoreCommitExactly(activeBranchState.undoCommitSha!);
|
|
1340
|
-
const nextState = {
|
|
1341
|
-
currentCommitSha: activeBranchState.undoCommitSha!,
|
|
1342
|
-
undoCommitSha: restore.undoCommitSha,
|
|
1343
|
-
};
|
|
1344
|
-
appendForkPendingState(nextState);
|
|
1345
|
-
notify(ctx, "Files restored to before last rewind");
|
|
1346
|
-
return;
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
if (choice === "Conversation only (keep current files)") {
|
|
1350
|
-
const nextState = { currentCommitSha: await ensureSnapshotForCurrentWorktree() };
|
|
1351
|
-
appendForkPendingState(nextState);
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
if (!targetCommitSha) {
|
|
1356
|
-
notify(ctx, "No exact rewind point available for that entry", "error");
|
|
1357
|
-
return { cancel: true };
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
const restore = await restoreCommitExactly(targetCommitSha);
|
|
1361
|
-
const nextState = {
|
|
1362
|
-
currentCommitSha: targetCommitSha,
|
|
1363
|
-
undoCommitSha: restore.undoCommitSha,
|
|
1364
|
-
};
|
|
1365
|
-
appendForkPendingState(nextState);
|
|
1366
|
-
notify(ctx, "Files restored from rewind point");
|
|
1367
|
-
|
|
1368
|
-
if (choice === "Code only (restore files, keep conversation)") {
|
|
1369
|
-
return { skipConversationRestore: true };
|
|
1370
|
-
}
|
|
1371
|
-
} catch (error) {
|
|
1372
|
-
notify(ctx, `Rewind failed before fork: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1373
|
-
return { cancel: true };
|
|
1374
|
-
}
|
|
1375
|
-
});
|
|
1376
|
-
|
|
1377
|
-
pi.on("session_before_tree", async (event, ctx) => {
|
|
1378
|
-
if (!isGitRepo) return;
|
|
1379
|
-
|
|
1380
|
-
try {
|
|
1381
|
-
if ((globalThis as typeof globalThis & { __boomerangCollapseInProgress?: boolean }).__boomerangCollapseInProgress === true) {
|
|
1382
|
-
pendingTreeState = { currentCommitSha: await ensureSnapshotForCurrentWorktree() };
|
|
1383
|
-
return;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
if (!ctx.hasUI) return;
|
|
1387
|
-
|
|
1388
|
-
const targetEntry = ctx.sessionManager.getEntry(event.preparation.targetId) as SessionLikeEntry | undefined;
|
|
1389
|
-
const targetCommitSha = isRestorableTreeEntry(targetEntry)
|
|
1390
|
-
? await resolveEntrySnapshotWithLineage(event.preparation.targetId, currentSessionFile)
|
|
1391
|
-
: undefined;
|
|
1392
|
-
const hasUndo = Boolean(activeBranchState.undoCommitSha && (await commitExists(activeBranchState.undoCommitSha)));
|
|
1393
|
-
|
|
1394
|
-
const options = ["Keep current files"];
|
|
1395
|
-
if (targetCommitSha) {
|
|
1396
|
-
options.push("Restore files to that point");
|
|
1397
|
-
}
|
|
1398
|
-
if (hasUndo) {
|
|
1399
|
-
options.push("Undo last file rewind");
|
|
1400
|
-
}
|
|
1401
|
-
options.push("Cancel navigation");
|
|
1402
|
-
|
|
1403
|
-
const choice = await ctx.ui.select("Restore Options", options);
|
|
1404
|
-
if (!choice || choice === "Cancel navigation") {
|
|
1405
|
-
notify(ctx, "Navigation cancelled");
|
|
1406
|
-
return { cancel: true };
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
if (choice === "Undo last file rewind") {
|
|
1410
|
-
const restore = await restoreCommitExactly(activeBranchState.undoCommitSha!);
|
|
1411
|
-
const snapshots = [activeBranchState.undoCommitSha!];
|
|
1412
|
-
const data: RewindOpData = { v: RETENTION_VERSION, snapshots, current: 0 };
|
|
1413
|
-
if (restore.undoCommitSha) {
|
|
1414
|
-
data.snapshots.push(restore.undoCommitSha);
|
|
1415
|
-
data.undo = 1;
|
|
1416
|
-
}
|
|
1417
|
-
appendRewindOp(ctx, data);
|
|
1418
|
-
notify(ctx, "Files restored to before last rewind");
|
|
1419
|
-
await reconstructState(ctx);
|
|
1420
|
-
return { cancel: true };
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
if (choice === "Keep current files") {
|
|
1424
|
-
pendingTreeState = { currentCommitSha: await ensureSnapshotForCurrentWorktree() };
|
|
1425
|
-
return;
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
if (!targetCommitSha) {
|
|
1429
|
-
notify(ctx, "Exact file rewind is only available for user, assistant, custom message, compaction, and summary nodes", "error");
|
|
1430
|
-
return { cancel: true };
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
const restore = await restoreCommitExactly(targetCommitSha);
|
|
1434
|
-
pendingTreeState = {
|
|
1435
|
-
currentCommitSha: targetCommitSha,
|
|
1436
|
-
undoCommitSha: restore.undoCommitSha,
|
|
1437
|
-
};
|
|
1438
|
-
notify(ctx, "Files restored to rewind point");
|
|
1439
|
-
} catch (error) {
|
|
1440
|
-
pendingTreeState = null;
|
|
1441
|
-
notify(ctx, `Rewind failed before tree navigation: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1442
|
-
return { cancel: true };
|
|
1443
|
-
}
|
|
1444
|
-
});
|
|
1445
|
-
}
|
|
1
|
+
import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@selesai/code";
|
|
2
|
+
import { exec as execCb } from "child_process";
|
|
3
|
+
import { existsSync, readFileSync, realpathSync } from "fs";
|
|
4
|
+
import { mkdtemp, readdir, readFile, rm, stat } from "fs/promises";
|
|
5
|
+
import { tmpdir } from "os";
|
|
6
|
+
import { dirname, isAbsolute, join, relative, resolve } from "path";
|
|
7
|
+
import { promisify } from "util";
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(execCb);
|
|
10
|
+
|
|
11
|
+
const STORE_REF = "refs/pi-rewind/store";
|
|
12
|
+
const STATUS_KEY = "rewind";
|
|
13
|
+
const FORK_PREFERENCE_SOURCE_ALLOWLIST = new Set(["fork-from-first"]);
|
|
14
|
+
const CHECKPOINT_SOURCE_ALLOWLIST = new Set(["pi-custom-compaction"]);
|
|
15
|
+
const LEGACY_ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
16
|
+
const RETENTION_SWEEP_THRESHOLD = 50;
|
|
17
|
+
const RETENTION_VERSION = 2;
|
|
18
|
+
const EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
19
|
+
|
|
20
|
+
type ExecFn = (cmd: string, args: string[]) => Promise<{ stdout: string; stderr: string; code: number }>;
|
|
21
|
+
|
|
22
|
+
type GitExecResult = Awaited<ReturnType<ExecFn>>;
|
|
23
|
+
|
|
24
|
+
type BindingTuple = [entryId: string, snapshotIndex: number];
|
|
25
|
+
|
|
26
|
+
interface RewindSettings {
|
|
27
|
+
rewind?: {
|
|
28
|
+
silentCheckpoints?: boolean;
|
|
29
|
+
retention?: {
|
|
30
|
+
maxSnapshots?: number;
|
|
31
|
+
maxAgeDays?: number;
|
|
32
|
+
pinLabeledEntries?: boolean;
|
|
33
|
+
scanMode?: "ancestor-only" | "repo-sessions";
|
|
34
|
+
startupBudgetMs?: number;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type RewindRetentionSettings = NonNullable<NonNullable<RewindSettings["rewind"]>["retention"]>;
|
|
40
|
+
|
|
41
|
+
interface RewindTurnData {
|
|
42
|
+
v: 2;
|
|
43
|
+
snapshots: string[];
|
|
44
|
+
bindings: BindingTuple[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface RewindOpData {
|
|
48
|
+
v: 2;
|
|
49
|
+
snapshots: string[];
|
|
50
|
+
bindings?: BindingTuple[];
|
|
51
|
+
current?: number;
|
|
52
|
+
undo?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface RewindForkPendingData {
|
|
56
|
+
v: 2;
|
|
57
|
+
current: string;
|
|
58
|
+
undo?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ActivePromptCollector {
|
|
62
|
+
snapshots: string[];
|
|
63
|
+
bindings: BindingTuple[];
|
|
64
|
+
promptText?: string;
|
|
65
|
+
pendingUserCommitSha?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ExactState {
|
|
69
|
+
commitSha: string;
|
|
70
|
+
treeSha: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface ActiveBranchState {
|
|
74
|
+
currentCommitSha?: string;
|
|
75
|
+
currentTreeSha?: string;
|
|
76
|
+
undoCommitSha?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface PendingResultingState {
|
|
80
|
+
currentCommitSha: string;
|
|
81
|
+
undoCommitSha?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ParsedLedgerReference {
|
|
85
|
+
commitSha: string;
|
|
86
|
+
entryId?: string;
|
|
87
|
+
timestamp: number;
|
|
88
|
+
kind: "binding" | "current" | "undo";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface ParsedSessionLedger {
|
|
92
|
+
sessionFile: string;
|
|
93
|
+
sessionId?: string;
|
|
94
|
+
cwd?: string;
|
|
95
|
+
parentSession?: string;
|
|
96
|
+
entryToCommit: Map<string, string>;
|
|
97
|
+
labeledEntryIds: Set<string>;
|
|
98
|
+
references: ParsedLedgerReference[];
|
|
99
|
+
latestCurrentCommitSha?: string;
|
|
100
|
+
latestUndoCommitSha?: string;
|
|
101
|
+
latestForkPending?: RewindForkPendingData;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface SessionLikeMessageEntry {
|
|
105
|
+
type: "message";
|
|
106
|
+
id: string;
|
|
107
|
+
parentId: string | null;
|
|
108
|
+
timestamp: string;
|
|
109
|
+
message: {
|
|
110
|
+
role: string;
|
|
111
|
+
timestamp?: number;
|
|
112
|
+
content?: unknown;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface SessionLikeCustomEntry {
|
|
117
|
+
type: "custom";
|
|
118
|
+
id: string;
|
|
119
|
+
parentId: string | null;
|
|
120
|
+
timestamp: string;
|
|
121
|
+
customType: string;
|
|
122
|
+
data?: unknown;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface SessionLikeLabelEntry {
|
|
126
|
+
type: "label";
|
|
127
|
+
targetId: string;
|
|
128
|
+
label?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface SessionLikeBranchSummaryEntry {
|
|
132
|
+
type: "branch_summary";
|
|
133
|
+
id: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface SessionLikeGenericEntry {
|
|
137
|
+
type: string;
|
|
138
|
+
id?: string;
|
|
139
|
+
parentId?: string | null;
|
|
140
|
+
timestamp?: string;
|
|
141
|
+
message?: {
|
|
142
|
+
role?: string;
|
|
143
|
+
timestamp?: number;
|
|
144
|
+
content?: unknown;
|
|
145
|
+
};
|
|
146
|
+
customType?: string;
|
|
147
|
+
data?: unknown;
|
|
148
|
+
targetId?: string;
|
|
149
|
+
label?: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
type SessionLikeEntry =
|
|
153
|
+
| SessionLikeMessageEntry
|
|
154
|
+
| SessionLikeCustomEntry
|
|
155
|
+
| SessionLikeLabelEntry
|
|
156
|
+
| SessionLikeBranchSummaryEntry
|
|
157
|
+
| SessionLikeGenericEntry;
|
|
158
|
+
|
|
159
|
+
let cachedSettings: RewindSettings | null = null;
|
|
160
|
+
|
|
161
|
+
function getSettingsFilePath(): string {
|
|
162
|
+
return join(getAgentDir(), "settings.json");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getDefaultSessionsDir(): string {
|
|
166
|
+
return join(getAgentDir(), "sessions");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getSettings(): RewindSettings {
|
|
170
|
+
if (cachedSettings) {
|
|
171
|
+
return cachedSettings;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
cachedSettings = JSON.parse(readFileSync(getSettingsFilePath(), "utf-8")) as RewindSettings;
|
|
176
|
+
} catch {
|
|
177
|
+
// Invalid/missing settings must not disable rewind; fall back to defaults.
|
|
178
|
+
cachedSettings = {};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return cachedSettings;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getSilentCheckpointsSetting(): boolean {
|
|
185
|
+
return getSettings().rewind?.silentCheckpoints === true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getRetentionSettings(): RewindRetentionSettings | undefined {
|
|
189
|
+
return getSettings().rewind?.retention;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getRetentionScanMode(): "ancestor-only" | "repo-sessions" {
|
|
193
|
+
return getRetentionSettings()?.scanMode === "repo-sessions" ? "repo-sessions" : "ancestor-only";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getStartupSweepBudgetMs(): number | undefined {
|
|
197
|
+
const value = getRetentionSettings()?.startupBudgetMs;
|
|
198
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isRewindTurnData(value: unknown): value is RewindTurnData {
|
|
205
|
+
if (!value || typeof value !== "object") return false;
|
|
206
|
+
const data = value as Partial<RewindTurnData>;
|
|
207
|
+
return data.v === 2 && Array.isArray(data.snapshots) && Array.isArray(data.bindings);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isRewindOpData(value: unknown): value is RewindOpData {
|
|
211
|
+
if (!value || typeof value !== "object") return false;
|
|
212
|
+
const data = value as Partial<RewindOpData>;
|
|
213
|
+
return data.v === 2 && Array.isArray(data.snapshots);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isRewindForkPendingData(value: unknown): value is RewindForkPendingData {
|
|
217
|
+
if (!value || typeof value !== "object") return false;
|
|
218
|
+
const data = value as Partial<RewindForkPendingData>;
|
|
219
|
+
return data.v === 2 && typeof data.current === "string" && data.current.length > 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function canonicalizePath(value: string): string {
|
|
223
|
+
const resolvedValue = resolve(value);
|
|
224
|
+
try {
|
|
225
|
+
return realpathSync.native(resolvedValue);
|
|
226
|
+
} catch {
|
|
227
|
+
// Path may not exist yet; compare with the resolved path directly.
|
|
228
|
+
return resolvedValue;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isInsidePath(targetPath: string, parentPath: string): boolean {
|
|
233
|
+
const resolvedTarget = canonicalizePath(targetPath);
|
|
234
|
+
const resolvedParent = canonicalizePath(parentPath);
|
|
235
|
+
const rel = relative(resolvedParent, resolvedTarget);
|
|
236
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function toTimestamp(value: string | undefined): number {
|
|
240
|
+
if (!value) return 0;
|
|
241
|
+
const parsed = Date.parse(value);
|
|
242
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getTextContent(content: unknown): string {
|
|
246
|
+
if (typeof content === "string") return content;
|
|
247
|
+
if (!Array.isArray(content)) return "";
|
|
248
|
+
return content
|
|
249
|
+
.filter((block): block is { type: string; text?: string } => !!block && typeof block === "object")
|
|
250
|
+
.filter((block) => block.type === "text")
|
|
251
|
+
.map((block) => block.text ?? "")
|
|
252
|
+
.join("\n");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function updateLabelSet(labelIds: Set<string>, entry: SessionLikeLabelEntry) {
|
|
256
|
+
if (!entry.targetId) return;
|
|
257
|
+
if (entry.label && entry.label.trim()) {
|
|
258
|
+
labelIds.add(entry.targetId);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
labelIds.delete(entry.targetId);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function applyBindings(target: Map<string, string>, snapshots: string[], bindings?: BindingTuple[]) {
|
|
265
|
+
if (!bindings) return;
|
|
266
|
+
for (const [entryId, snapshotIndex] of bindings) {
|
|
267
|
+
const commitSha = snapshots[snapshotIndex];
|
|
268
|
+
if (entryId && commitSha) {
|
|
269
|
+
target.set(entryId, commitSha);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function addReferences(target: ParsedLedgerReference[], snapshots: string[], timestamp: number, data: RewindTurnData | RewindOpData) {
|
|
275
|
+
if ("bindings" in data && data.bindings) {
|
|
276
|
+
for (const [entryId, snapshotIndex] of data.bindings) {
|
|
277
|
+
const commitSha = snapshots[snapshotIndex];
|
|
278
|
+
if (!commitSha) continue;
|
|
279
|
+
target.push({ commitSha, entryId, timestamp, kind: "binding" });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if ("current" in data && typeof data.current === "number") {
|
|
284
|
+
const commitSha = snapshots[data.current];
|
|
285
|
+
if (commitSha) {
|
|
286
|
+
target.push({ commitSha, timestamp, kind: "current" });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if ("undo" in data && typeof data.undo === "number") {
|
|
291
|
+
const commitSha = snapshots[data.undo];
|
|
292
|
+
if (commitSha) {
|
|
293
|
+
target.push({ commitSha, timestamp, kind: "undo" });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function resolveBindingSnapshotIndex(snapshots: string[], commitSha: string): number {
|
|
299
|
+
const existingIndex = snapshots.indexOf(commitSha);
|
|
300
|
+
if (existingIndex >= 0) return existingIndex;
|
|
301
|
+
snapshots.push(commitSha);
|
|
302
|
+
return snapshots.length - 1;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function addBindingToCollector(collector: ActivePromptCollector, entryId: string, commitSha: string) {
|
|
306
|
+
const snapshotIndex = resolveBindingSnapshotIndex(collector.snapshots, commitSha);
|
|
307
|
+
collector.bindings.push([entryId, snapshotIndex]);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function getCommitFromData(data: RewindOpData, indexKey: "current" | "undo"): string | undefined {
|
|
311
|
+
const snapshotIndex = data[indexKey];
|
|
312
|
+
return typeof snapshotIndex === "number" ? data.snapshots[snapshotIndex] : undefined;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function isRestorableTreeEntry(entry: SessionLikeEntry | undefined): boolean {
|
|
316
|
+
if (!entry) return false;
|
|
317
|
+
if (entry.type === "message") {
|
|
318
|
+
return entry.message.role === "user" || entry.message.role === "assistant";
|
|
319
|
+
}
|
|
320
|
+
return entry.type === "branch_summary" || entry.type === "compaction" || entry.type === "custom_message";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function isAssistantMessageEntry(entry: SessionLikeEntry): entry is SessionLikeMessageEntry {
|
|
324
|
+
return entry.type === "message" && entry.message.role === "assistant";
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function findLatestUserMessageEntry(entries: SessionLikeEntry[]): SessionLikeMessageEntry | null {
|
|
328
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
329
|
+
const entry = entries[index];
|
|
330
|
+
if (entry.type === "message" && entry.message.role === "user") {
|
|
331
|
+
return entry;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function findLatestMatchingUserMessageEntry(
|
|
338
|
+
entries: SessionLikeEntry[],
|
|
339
|
+
promptText: string | null | undefined,
|
|
340
|
+
): SessionLikeMessageEntry | null {
|
|
341
|
+
if (!promptText) return null;
|
|
342
|
+
|
|
343
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
344
|
+
const entry = entries[index];
|
|
345
|
+
if (entry.type !== "message" || entry.message.role !== "user") continue;
|
|
346
|
+
if (getTextContent(entry.message.content) === promptText) {
|
|
347
|
+
return entry;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function findAssistantEntryForTurn(entries: SessionLikeEntry[], message: { timestamp?: number; content?: unknown }): SessionLikeMessageEntry | null {
|
|
355
|
+
const targetTimestamp = message.timestamp;
|
|
356
|
+
const targetText = getTextContent(message.content);
|
|
357
|
+
|
|
358
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
359
|
+
const entry = entries[index];
|
|
360
|
+
if (!isAssistantMessageEntry(entry)) continue;
|
|
361
|
+
|
|
362
|
+
if (targetTimestamp !== undefined && entry.message.timestamp === targetTimestamp) {
|
|
363
|
+
return entry;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (targetText && getTextContent(entry.message.content) === targetText) {
|
|
367
|
+
return entry;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export default function rewindExtension(pi: ExtensionAPI) {
|
|
375
|
+
const entryToCommit = new Map<string, string>();
|
|
376
|
+
const parsedSessionCache = new Map<string, { mtimeMs: number; ledger: ParsedSessionLedger }>();
|
|
377
|
+
|
|
378
|
+
let repoRoot: string | null = null;
|
|
379
|
+
let sessionId: string | null = null;
|
|
380
|
+
let currentSessionFile: string | undefined;
|
|
381
|
+
let currentParentSession: string | undefined;
|
|
382
|
+
let currentSessionCwd: string | undefined;
|
|
383
|
+
let isGitRepo = false;
|
|
384
|
+
let lastExact: ExactState | null = null;
|
|
385
|
+
let activeBranchState: ActiveBranchState = {};
|
|
386
|
+
let promptCollector: ActivePromptCollector | null = null;
|
|
387
|
+
let pendingTreeState: PendingResultingState | null = null;
|
|
388
|
+
let activePromptText: string | null = null;
|
|
389
|
+
let newSnapshotsSinceSweep = 0;
|
|
390
|
+
let sweepRunning = false;
|
|
391
|
+
let sweepCompletedThisSession = false;
|
|
392
|
+
let forceConversationOnlyOnNextFork = false;
|
|
393
|
+
let forceConversationOnlySource: string | null = null;
|
|
394
|
+
|
|
395
|
+
function notify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info") {
|
|
396
|
+
if (!ctx.hasUI) return;
|
|
397
|
+
if (level === "info" && getSilentCheckpointsSetting()) return;
|
|
398
|
+
ctx.ui.notify(message, level);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function updateStatus(ctx: ExtensionContext) {
|
|
402
|
+
if (!ctx.hasUI) return;
|
|
403
|
+
if (!isGitRepo || getSilentCheckpointsSetting()) {
|
|
404
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const uniqueSnapshots = new Set(entryToCommit.values()).size;
|
|
409
|
+
const theme = ctx.ui.theme;
|
|
410
|
+
ctx.ui.setStatus(
|
|
411
|
+
STATUS_KEY,
|
|
412
|
+
theme.fg("dim", "◆ ") + theme.fg("muted", `${entryToCommit.size} points / ${uniqueSnapshots} snapshots`),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function resetState() {
|
|
417
|
+
entryToCommit.clear();
|
|
418
|
+
parsedSessionCache.clear();
|
|
419
|
+
repoRoot = null;
|
|
420
|
+
sessionId = null;
|
|
421
|
+
currentSessionFile = undefined;
|
|
422
|
+
currentParentSession = undefined;
|
|
423
|
+
currentSessionCwd = undefined;
|
|
424
|
+
isGitRepo = false;
|
|
425
|
+
lastExact = null;
|
|
426
|
+
activeBranchState = {};
|
|
427
|
+
promptCollector = null;
|
|
428
|
+
pendingTreeState = null;
|
|
429
|
+
activePromptText = null;
|
|
430
|
+
newSnapshotsSinceSweep = 0;
|
|
431
|
+
sweepCompletedThisSession = false;
|
|
432
|
+
forceConversationOnlyOnNextFork = false;
|
|
433
|
+
forceConversationOnlySource = null;
|
|
434
|
+
cachedSettings = null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function syncSessionIdentity(ctx: ExtensionContext) {
|
|
438
|
+
sessionId = ctx.sessionManager.getSessionId();
|
|
439
|
+
currentSessionFile = ctx.sessionManager.getSessionFile();
|
|
440
|
+
currentParentSession = ctx.sessionManager.getHeader()?.parentSession;
|
|
441
|
+
currentSessionCwd = ctx.sessionManager.getCwd();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function execGitChecked(args: string[]): Promise<GitExecResult> {
|
|
445
|
+
const result = await pi.exec("git", args);
|
|
446
|
+
if (result.code !== 0) {
|
|
447
|
+
const stderr = result.stderr.trim();
|
|
448
|
+
throw new Error(stderr || `git ${args.join(" ")} failed with code ${result.code}`);
|
|
449
|
+
}
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function getRepoRoot(exec: ExecFn): Promise<string> {
|
|
454
|
+
if (repoRoot) return repoRoot;
|
|
455
|
+
const result = await exec("git", ["rev-parse", "--show-toplevel"]);
|
|
456
|
+
if (result.code !== 0) {
|
|
457
|
+
const stderr = result.stderr.trim();
|
|
458
|
+
throw new Error(stderr || `git rev-parse --show-toplevel failed with code ${result.code}`);
|
|
459
|
+
}
|
|
460
|
+
repoRoot = result.stdout.trim();
|
|
461
|
+
return repoRoot;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function captureWorktreeTree(): Promise<{ treeSha: string }> {
|
|
465
|
+
const root = await getRepoRoot(pi.exec);
|
|
466
|
+
const tempDir = await mkdtemp(join(tmpdir(), "pi-rewind-"));
|
|
467
|
+
const tempIndex = join(tempDir, "index");
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
471
|
+
await execAsync("git add -A", { cwd: root, env });
|
|
472
|
+
const { stdout } = await execAsync("git write-tree", { cwd: root, env });
|
|
473
|
+
return { treeSha: stdout.trim() };
|
|
474
|
+
} finally {
|
|
475
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
476
|
+
// Best effort cleanup for temporary index directory.
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function getCommitTreeSha(commitSha: string): Promise<string> {
|
|
482
|
+
const result = await execGitChecked(["show", "-s", "--format=%T", commitSha]);
|
|
483
|
+
return result.stdout.trim();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function commitExists(commitSha: string): Promise<boolean> {
|
|
487
|
+
const result = await pi.exec("git", ["cat-file", "-e", `${commitSha}^{commit}`]);
|
|
488
|
+
return result.code === 0;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function getStoreHead(): Promise<string | undefined> {
|
|
492
|
+
const result = await pi.exec("git", ["rev-parse", "--verify", STORE_REF]);
|
|
493
|
+
if (result.code !== 0) {
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
const value = result.stdout.trim();
|
|
497
|
+
return value || undefined;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function createStoreKeepaliveCommit(snapshotCommitSha: string, previousStoreHead?: string): Promise<string> {
|
|
501
|
+
const args = ["commit-tree", EMPTY_TREE_SHA];
|
|
502
|
+
|
|
503
|
+
if (previousStoreHead) {
|
|
504
|
+
args.push("-p", previousStoreHead);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
args.push("-p", snapshotCommitSha, "-m", "pi rewind store");
|
|
508
|
+
const result = await execGitChecked(args);
|
|
509
|
+
return result.stdout.trim();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function appendSnapshotToStore(commitSha: string): Promise<void> {
|
|
513
|
+
let attempts = 0;
|
|
514
|
+
let lastError: unknown;
|
|
515
|
+
|
|
516
|
+
while (attempts < 5) {
|
|
517
|
+
attempts += 1;
|
|
518
|
+
const oldHead = await getStoreHead();
|
|
519
|
+
const keepaliveCommit = await createStoreKeepaliveCommit(commitSha, oldHead);
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
if (oldHead) {
|
|
523
|
+
await execGitChecked(["update-ref", STORE_REF, keepaliveCommit, oldHead]);
|
|
524
|
+
} else {
|
|
525
|
+
await execGitChecked(["update-ref", STORE_REF, keepaliveCommit, LEGACY_ZERO_SHA]);
|
|
526
|
+
}
|
|
527
|
+
return;
|
|
528
|
+
} catch (error) {
|
|
529
|
+
// Retry if another process updated the store ref concurrently.
|
|
530
|
+
// Keep the most recent error for actionable failure context.
|
|
531
|
+
lastError = error;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const detail = lastError instanceof Error ? lastError.message : String(lastError);
|
|
536
|
+
throw new Error(`failed to update rewind store ref: ${detail}`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function rewriteStoreToLiveSet(liveCommitShas: string[]): Promise<"rewritten" | "preserved-empty"> {
|
|
540
|
+
const uniqueLiveCommits = [...new Set(liveCommitShas.filter(Boolean))];
|
|
541
|
+
if (uniqueLiveCommits.length === 0) {
|
|
542
|
+
return "preserved-empty";
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
let head: string | undefined;
|
|
546
|
+
for (const commitSha of uniqueLiveCommits) {
|
|
547
|
+
head = await createStoreKeepaliveCommit(commitSha, head);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const oldHead = await getStoreHead();
|
|
551
|
+
if (oldHead) {
|
|
552
|
+
await execGitChecked(["update-ref", STORE_REF, head!, oldHead]);
|
|
553
|
+
return "rewritten";
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
await execGitChecked(["update-ref", STORE_REF, head!, LEGACY_ZERO_SHA]);
|
|
557
|
+
return "rewritten";
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function ensureSnapshotForTree(treeSha: string): Promise<string> {
|
|
561
|
+
if (lastExact && lastExact.treeSha === treeSha) {
|
|
562
|
+
return lastExact.commitSha;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const result = await execGitChecked(["commit-tree", treeSha, "-m", "pi rewind snapshot"]);
|
|
566
|
+
const commitSha = result.stdout.trim();
|
|
567
|
+
await appendSnapshotToStore(commitSha);
|
|
568
|
+
lastExact = { commitSha, treeSha };
|
|
569
|
+
newSnapshotsSinceSweep += 1;
|
|
570
|
+
return commitSha;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function ensureSnapshotForCurrentWorktree(): Promise<string> {
|
|
574
|
+
const { treeSha } = await captureWorktreeTree();
|
|
575
|
+
return ensureSnapshotForTree(treeSha);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function deletePathsFromWorkingTree(paths: string[]) {
|
|
579
|
+
if (paths.length === 0) return;
|
|
580
|
+
const root = await getRepoRoot(pi.exec);
|
|
581
|
+
|
|
582
|
+
for (const repoRelativePath of paths) {
|
|
583
|
+
const absolutePath = resolve(root, repoRelativePath);
|
|
584
|
+
if (!isInsidePath(absolutePath, root)) {
|
|
585
|
+
throw new Error(`refusing to delete path outside repo root: ${repoRelativePath}`);
|
|
586
|
+
}
|
|
587
|
+
await rm(absolutePath, { recursive: true, force: true });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function getDeletedPaths(currentTreeSha: string, targetTreeSha: string): Promise<string[]> {
|
|
592
|
+
const result = await execGitChecked([
|
|
593
|
+
"diff",
|
|
594
|
+
"--name-only",
|
|
595
|
+
"--diff-filter=D",
|
|
596
|
+
"-z",
|
|
597
|
+
currentTreeSha,
|
|
598
|
+
targetTreeSha,
|
|
599
|
+
"--",
|
|
600
|
+
]);
|
|
601
|
+
|
|
602
|
+
return result.stdout.split("\0").filter(Boolean);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function restoreCommitExactly(targetCommitSha: string): Promise<{ changed: boolean; undoCommitSha?: string; targetTreeSha: string }> {
|
|
606
|
+
const { treeSha: currentTreeSha } = await captureWorktreeTree();
|
|
607
|
+
const targetTreeSha = await getCommitTreeSha(targetCommitSha);
|
|
608
|
+
|
|
609
|
+
if (currentTreeSha === targetTreeSha) {
|
|
610
|
+
lastExact = { commitSha: targetCommitSha, treeSha: targetTreeSha };
|
|
611
|
+
return { changed: false, targetTreeSha };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const undoCommitSha = await ensureSnapshotForTree(currentTreeSha);
|
|
615
|
+
const pathsToDelete = await getDeletedPaths(currentTreeSha, targetTreeSha);
|
|
616
|
+
await deletePathsFromWorkingTree(pathsToDelete);
|
|
617
|
+
await execGitChecked(["restore", `--source=${targetCommitSha}`, "--worktree", "--", "."]);
|
|
618
|
+
lastExact = { commitSha: targetCommitSha, treeSha: targetTreeSha };
|
|
619
|
+
return { changed: true, undoCommitSha, targetTreeSha };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function bindPendingPromptUser(entries: SessionLikeEntry[], collector: ActivePromptCollector) {
|
|
623
|
+
if (!collector.pendingUserCommitSha) return;
|
|
624
|
+
|
|
625
|
+
const userEntry = findLatestMatchingUserMessageEntry(entries, collector.promptText) ?? findLatestUserMessageEntry(entries);
|
|
626
|
+
if (!userEntry) return;
|
|
627
|
+
if (collector.bindings.some(([entryId]) => entryId === userEntry.id)) {
|
|
628
|
+
collector.pendingUserCommitSha = undefined;
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
addBindingToCollector(collector, userEntry.id, collector.pendingUserCommitSha);
|
|
633
|
+
collector.pendingUserCommitSha = undefined;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function appendRewindTurn(ctx: ExtensionContext, collector: ActivePromptCollector) {
|
|
637
|
+
if (collector.bindings.length === 0) return;
|
|
638
|
+
|
|
639
|
+
const data: RewindTurnData = {
|
|
640
|
+
v: RETENTION_VERSION,
|
|
641
|
+
snapshots: collector.snapshots,
|
|
642
|
+
bindings: collector.bindings,
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
pi.appendEntry("rewind-turn", data);
|
|
646
|
+
applyBindings(entryToCommit, data.snapshots, data.bindings);
|
|
647
|
+
|
|
648
|
+
const latestBinding = data.bindings[data.bindings.length - 1];
|
|
649
|
+
if (latestBinding) {
|
|
650
|
+
activeBranchState.currentCommitSha = data.snapshots[latestBinding[1]];
|
|
651
|
+
activeBranchState.currentTreeSha = lastExact?.commitSha === activeBranchState.currentCommitSha ? lastExact.treeSha : undefined;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
updateStatus(ctx);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function appendRewindOp(ctx: ExtensionContext, data: RewindOpData) {
|
|
658
|
+
const hasBindings = Boolean(data.bindings?.length);
|
|
659
|
+
const hasCurrent = typeof data.current === "number";
|
|
660
|
+
const hasUndo = typeof data.undo === "number";
|
|
661
|
+
if (!hasBindings && !hasCurrent && !hasUndo) return;
|
|
662
|
+
|
|
663
|
+
pi.appendEntry("rewind-op", data);
|
|
664
|
+
applyBindings(entryToCommit, data.snapshots, data.bindings);
|
|
665
|
+
|
|
666
|
+
const currentCommitSha = getCommitFromData(data, "current");
|
|
667
|
+
if (currentCommitSha) {
|
|
668
|
+
activeBranchState.currentCommitSha = currentCommitSha;
|
|
669
|
+
activeBranchState.currentTreeSha = lastExact?.commitSha === currentCommitSha ? lastExact.treeSha : undefined;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const undoCommitSha = getCommitFromData(data, "undo");
|
|
673
|
+
if (undoCommitSha) {
|
|
674
|
+
activeBranchState.undoCommitSha = undoCommitSha;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
updateStatus(ctx);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function appendForkPendingState(data: PendingResultingState) {
|
|
681
|
+
const forkPending: RewindForkPendingData = {
|
|
682
|
+
v: RETENTION_VERSION,
|
|
683
|
+
current: data.currentCommitSha,
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
if (data.undoCommitSha) {
|
|
687
|
+
forkPending.undo = data.undoCommitSha;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
pi.appendEntry("rewind-fork-pending", forkPending);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function buildCurrentSessionLedger(ctx: ExtensionContext): ParsedSessionLedger {
|
|
694
|
+
const ledger: ParsedSessionLedger = {
|
|
695
|
+
sessionFile: currentSessionFile ?? "",
|
|
696
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
697
|
+
cwd: ctx.sessionManager.getCwd(),
|
|
698
|
+
parentSession: ctx.sessionManager.getHeader()?.parentSession,
|
|
699
|
+
entryToCommit: new Map<string, string>(),
|
|
700
|
+
labeledEntryIds: new Set<string>(),
|
|
701
|
+
references: [],
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
for (const rawEntry of ctx.sessionManager.getEntries() as SessionLikeEntry[]) {
|
|
705
|
+
if (rawEntry.type === "custom" && rawEntry.customType === "rewind-turn" && isRewindTurnData(rawEntry.data)) {
|
|
706
|
+
applyBindings(ledger.entryToCommit, rawEntry.data.snapshots, rawEntry.data.bindings);
|
|
707
|
+
addReferences(ledger.references, rawEntry.data.snapshots, toTimestamp(rawEntry.timestamp), rawEntry.data);
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (rawEntry.type === "custom" && rawEntry.customType === "rewind-op" && isRewindOpData(rawEntry.data)) {
|
|
712
|
+
applyBindings(ledger.entryToCommit, rawEntry.data.snapshots, rawEntry.data.bindings);
|
|
713
|
+
addReferences(ledger.references, rawEntry.data.snapshots, toTimestamp(rawEntry.timestamp), rawEntry.data);
|
|
714
|
+
const currentCommitSha = getCommitFromData(rawEntry.data, "current");
|
|
715
|
+
if (currentCommitSha) ledger.latestCurrentCommitSha = currentCommitSha;
|
|
716
|
+
const undoCommitSha = getCommitFromData(rawEntry.data, "undo");
|
|
717
|
+
if (undoCommitSha) ledger.latestUndoCommitSha = undoCommitSha;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (rawEntry.type === "custom" && rawEntry.customType === "rewind-fork-pending" && isRewindForkPendingData(rawEntry.data)) {
|
|
722
|
+
ledger.latestForkPending = rawEntry.data;
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (rawEntry.type === "label") {
|
|
727
|
+
updateLabelSet(ledger.labeledEntryIds, rawEntry);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return ledger;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function parseSessionLedgerFile(sessionFile: string): Promise<ParsedSessionLedger | null> {
|
|
735
|
+
try {
|
|
736
|
+
const fileStat = await stat(sessionFile);
|
|
737
|
+
const cached = parsedSessionCache.get(sessionFile);
|
|
738
|
+
if (cached && cached.mtimeMs === fileStat.mtimeMs) {
|
|
739
|
+
return cached.ledger;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const content = await readFile(sessionFile, "utf-8");
|
|
743
|
+
const ledger: ParsedSessionLedger = {
|
|
744
|
+
sessionFile,
|
|
745
|
+
entryToCommit: new Map<string, string>(),
|
|
746
|
+
labeledEntryIds: new Set<string>(),
|
|
747
|
+
references: [],
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
const hasRewindEntries = content.includes('"rewind-');
|
|
751
|
+
|
|
752
|
+
if (!hasRewindEntries) {
|
|
753
|
+
// Fast path: extract session header only, skip line-by-line JSON parsing
|
|
754
|
+
let pos = 0;
|
|
755
|
+
for (let i = 0; i < 5 && pos < content.length; i++) {
|
|
756
|
+
const nextNewline = content.indexOf("\n", pos);
|
|
757
|
+
const line = nextNewline >= 0 ? content.substring(pos, nextNewline) : content.substring(pos);
|
|
758
|
+
pos = nextNewline >= 0 ? nextNewline + 1 : content.length;
|
|
759
|
+
if (!line) continue;
|
|
760
|
+
try {
|
|
761
|
+
const entry = JSON.parse(line);
|
|
762
|
+
if (entry?.type === "session") {
|
|
763
|
+
ledger.sessionId = entry.id;
|
|
764
|
+
ledger.cwd = entry.cwd;
|
|
765
|
+
ledger.parentSession = entry.parentSession;
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
} catch {
|
|
769
|
+
// Ignore malformed header lines from partial/corrupt session files.
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
parsedSessionCache.set(sessionFile, { mtimeMs: fileStat.mtimeMs, ledger });
|
|
774
|
+
return ledger;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const lines = content.split("\n").filter(Boolean);
|
|
778
|
+
|
|
779
|
+
for (const line of lines) {
|
|
780
|
+
let entry: SessionLikeGenericEntry | null = null;
|
|
781
|
+
try {
|
|
782
|
+
const parsed = JSON.parse(line);
|
|
783
|
+
if (parsed && typeof parsed === "object") {
|
|
784
|
+
entry = parsed as SessionLikeGenericEntry;
|
|
785
|
+
}
|
|
786
|
+
} catch {
|
|
787
|
+
// Ignore malformed JSONL lines; retention discovery is best-effort.
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (!entry) continue;
|
|
792
|
+
|
|
793
|
+
if (entry?.type === "session") {
|
|
794
|
+
ledger.sessionId = entry.id;
|
|
795
|
+
ledger.cwd = entry.cwd;
|
|
796
|
+
ledger.parentSession = entry.parentSession;
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (entry?.type === "custom" && entry?.customType === "rewind-turn" && isRewindTurnData(entry.data)) {
|
|
801
|
+
applyBindings(ledger.entryToCommit, entry.data.snapshots, entry.data.bindings);
|
|
802
|
+
addReferences(ledger.references, entry.data.snapshots, toTimestamp(entry.timestamp), entry.data);
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (entry?.type === "custom" && entry?.customType === "rewind-op" && isRewindOpData(entry.data)) {
|
|
807
|
+
applyBindings(ledger.entryToCommit, entry.data.snapshots, entry.data.bindings);
|
|
808
|
+
addReferences(ledger.references, entry.data.snapshots, toTimestamp(entry.timestamp), entry.data);
|
|
809
|
+
const currentCommitSha = getCommitFromData(entry.data, "current");
|
|
810
|
+
if (currentCommitSha) ledger.latestCurrentCommitSha = currentCommitSha;
|
|
811
|
+
const undoCommitSha = getCommitFromData(entry.data, "undo");
|
|
812
|
+
if (undoCommitSha) ledger.latestUndoCommitSha = undoCommitSha;
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (entry?.type === "custom" && entry?.customType === "rewind-fork-pending" && isRewindForkPendingData(entry.data)) {
|
|
817
|
+
ledger.latestForkPending = entry.data;
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (entry?.type === "label") {
|
|
822
|
+
updateLabelSet(ledger.labeledEntryIds, entry);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
parsedSessionCache.set(sessionFile, { mtimeMs: fileStat.mtimeMs, ledger });
|
|
827
|
+
return ledger;
|
|
828
|
+
} catch {
|
|
829
|
+
// Unreadable/missing session file: skip it and continue lineage/discovery.
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function resolveEntrySnapshotWithLineage(entryId: string, sessionFile = currentSessionFile): Promise<string | undefined> {
|
|
835
|
+
let cursor = sessionFile;
|
|
836
|
+
|
|
837
|
+
while (cursor) {
|
|
838
|
+
const ledger = cursor === currentSessionFile ? buildCurrentSessionLedgerFromMemory() : await parseSessionLedgerFile(cursor);
|
|
839
|
+
if (!ledger) break;
|
|
840
|
+
|
|
841
|
+
const commitSha = ledger.entryToCommit.get(entryId);
|
|
842
|
+
if (commitSha && (await commitExists(commitSha))) {
|
|
843
|
+
return commitSha;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
cursor = ledger.parentSession;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return undefined;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function buildCurrentSessionLedgerFromMemory(): ParsedSessionLedger {
|
|
853
|
+
return {
|
|
854
|
+
sessionFile: currentSessionFile ?? "",
|
|
855
|
+
sessionId: sessionId ?? undefined,
|
|
856
|
+
cwd: currentSessionCwd,
|
|
857
|
+
parentSession: currentParentSession,
|
|
858
|
+
entryToCommit: new Map(entryToCommit),
|
|
859
|
+
labeledEntryIds: new Set(),
|
|
860
|
+
references: [],
|
|
861
|
+
latestCurrentCommitSha: activeBranchState.currentCommitSha,
|
|
862
|
+
latestUndoCommitSha: activeBranchState.undoCommitSha,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function reconstructState(ctx: ExtensionContext) {
|
|
867
|
+
entryToCommit.clear();
|
|
868
|
+
activeBranchState = {};
|
|
869
|
+
lastExact = null;
|
|
870
|
+
|
|
871
|
+
const currentLedger = buildCurrentSessionLedger(ctx);
|
|
872
|
+
for (const [entryId, commitSha] of currentLedger.entryToCommit.entries()) {
|
|
873
|
+
entryToCommit.set(entryId, commitSha);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
let latestVisibleBindingCommitSha: string | undefined;
|
|
877
|
+
for (const entry of ctx.sessionManager.getBranch() as SessionLikeEntry[]) {
|
|
878
|
+
const boundCommitSha = entry.id ? entryToCommit.get(entry.id) : undefined;
|
|
879
|
+
if (boundCommitSha && isRestorableTreeEntry(entry)) {
|
|
880
|
+
latestVisibleBindingCommitSha = boundCommitSha;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (entry.type === "custom" && entry.customType === "rewind-op" && isRewindOpData(entry.data)) {
|
|
884
|
+
const currentCommitSha = getCommitFromData(entry.data, "current");
|
|
885
|
+
if (currentCommitSha) {
|
|
886
|
+
activeBranchState.currentCommitSha = currentCommitSha;
|
|
887
|
+
}
|
|
888
|
+
const undoCommitSha = getCommitFromData(entry.data, "undo");
|
|
889
|
+
if (undoCommitSha) {
|
|
890
|
+
activeBranchState.undoCommitSha = undoCommitSha;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (!activeBranchState.currentCommitSha) {
|
|
896
|
+
activeBranchState.currentCommitSha = latestVisibleBindingCommitSha;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (activeBranchState.currentCommitSha && (await commitExists(activeBranchState.currentCommitSha))) {
|
|
900
|
+
activeBranchState.currentTreeSha = await getCommitTreeSha(activeBranchState.currentCommitSha);
|
|
901
|
+
const { treeSha: worktreeTreeSha } = await captureWorktreeTree();
|
|
902
|
+
|
|
903
|
+
if (activeBranchState.currentTreeSha === worktreeTreeSha) {
|
|
904
|
+
lastExact = {
|
|
905
|
+
commitSha: activeBranchState.currentCommitSha,
|
|
906
|
+
treeSha: activeBranchState.currentTreeSha,
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
async function discoverSessionFiles(scanMode: "ancestor-only" | "repo-sessions"): Promise<string[]> {
|
|
913
|
+
const discovered = new Set<string>();
|
|
914
|
+
|
|
915
|
+
if (scanMode === "repo-sessions") {
|
|
916
|
+
const roots = new Set<string>();
|
|
917
|
+
const defaultSessionsDir = getDefaultSessionsDir();
|
|
918
|
+
if (existsSync(defaultSessionsDir)) {
|
|
919
|
+
roots.add(defaultSessionsDir);
|
|
920
|
+
}
|
|
921
|
+
if (currentSessionFile) {
|
|
922
|
+
roots.add(dirname(currentSessionFile));
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const stack = [...roots];
|
|
926
|
+
while (stack.length > 0) {
|
|
927
|
+
const dir = stack.pop();
|
|
928
|
+
if (!dir) continue;
|
|
929
|
+
|
|
930
|
+
let entries: Awaited<ReturnType<typeof readdir>>;
|
|
931
|
+
try {
|
|
932
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
933
|
+
} catch {
|
|
934
|
+
// Skip unreadable directories during best-effort discovery.
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
for (const entry of entries) {
|
|
939
|
+
const fullPath = join(dir, entry.name);
|
|
940
|
+
if (entry.isDirectory()) {
|
|
941
|
+
stack.push(fullPath);
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
945
|
+
discovered.add(fullPath);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
let ancestorCursor = currentSessionFile;
|
|
952
|
+
while (ancestorCursor) {
|
|
953
|
+
discovered.add(ancestorCursor);
|
|
954
|
+
const ledger = ancestorCursor === currentSessionFile ? buildCurrentSessionLedgerFromMemory() : await parseSessionLedgerFile(ancestorCursor);
|
|
955
|
+
ancestorCursor = ledger?.parentSession;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
return [...discovered];
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
async function maybeSweepRetention(ctx: ExtensionContext, reason: "startup" | "new-snapshots" | "shutdown") {
|
|
962
|
+
const retention = getRetentionSettings();
|
|
963
|
+
if (!retention) return;
|
|
964
|
+
if (reason === "new-snapshots" && newSnapshotsSinceSweep < RETENTION_SWEEP_THRESHOLD) return;
|
|
965
|
+
if (reason === "shutdown" && sweepCompletedThisSession && newSnapshotsSinceSweep < RETENTION_SWEEP_THRESHOLD) return;
|
|
966
|
+
if (!repoRoot) return;
|
|
967
|
+
if (sweepRunning) return;
|
|
968
|
+
sweepRunning = true;
|
|
969
|
+
try {
|
|
970
|
+
await runRetentionSweep(ctx, reason);
|
|
971
|
+
} finally {
|
|
972
|
+
sweepRunning = false;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function runRetentionSweep(ctx: ExtensionContext, reason: "startup" | "new-snapshots" | "shutdown") {
|
|
977
|
+
const retention = getRetentionSettings();
|
|
978
|
+
if (!retention) return;
|
|
979
|
+
|
|
980
|
+
const scanMode = getRetentionScanMode();
|
|
981
|
+
const startupBudgetMs = reason === "startup" ? getStartupSweepBudgetMs() : undefined;
|
|
982
|
+
const startedAt = Date.now();
|
|
983
|
+
|
|
984
|
+
const budgetExceeded = () => typeof startupBudgetMs === "number" && Date.now() - startedAt > startupBudgetMs;
|
|
985
|
+
|
|
986
|
+
const sessionFiles = await discoverSessionFiles(scanMode);
|
|
987
|
+
if (budgetExceeded()) {
|
|
988
|
+
notify(ctx, `Rewind retention startup sweep skipped after exceeding ${startupBudgetMs}ms budget`, "warning");
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const ledgers: ParsedSessionLedger[] = [];
|
|
993
|
+
|
|
994
|
+
for (const sessionFile of sessionFiles) {
|
|
995
|
+
if (budgetExceeded()) {
|
|
996
|
+
notify(ctx, `Rewind retention startup sweep skipped after exceeding ${startupBudgetMs}ms budget`, "warning");
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const ledger = sessionFile === currentSessionFile ? buildCurrentSessionLedger(ctx) : await parseSessionLedgerFile(sessionFile);
|
|
1001
|
+
if (!ledger?.cwd) continue;
|
|
1002
|
+
if (!isInsidePath(ledger.cwd, repoRoot)) continue;
|
|
1003
|
+
ledgers.push(ledger);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const latestReferenceByCommit = new Map<string, number>();
|
|
1007
|
+
const pinnedCommits = new Set<string>();
|
|
1008
|
+
const currentCommits = new Set<string>();
|
|
1009
|
+
const undoCommits = new Set<string>();
|
|
1010
|
+
|
|
1011
|
+
for (const ledger of ledgers) {
|
|
1012
|
+
if (budgetExceeded()) {
|
|
1013
|
+
notify(ctx, `Rewind retention startup sweep skipped after exceeding ${startupBudgetMs}ms budget`, "warning");
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
for (const reference of ledger.references) {
|
|
1018
|
+
const prev = latestReferenceByCommit.get(reference.commitSha) ?? 0;
|
|
1019
|
+
if (reference.timestamp > prev) {
|
|
1020
|
+
latestReferenceByCommit.set(reference.commitSha, reference.timestamp);
|
|
1021
|
+
}
|
|
1022
|
+
if (reference.kind === "binding" && retention.pinLabeledEntries && reference.entryId && ledger.labeledEntryIds.has(reference.entryId)) {
|
|
1023
|
+
pinnedCommits.add(reference.commitSha);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (ledger.latestCurrentCommitSha) {
|
|
1028
|
+
currentCommits.add(ledger.latestCurrentCommitSha);
|
|
1029
|
+
}
|
|
1030
|
+
if (ledger.latestUndoCommitSha) {
|
|
1031
|
+
undoCommits.add(ledger.latestUndoCommitSha);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
for (const commitSha of [...currentCommits, ...undoCommits]) {
|
|
1036
|
+
if (budgetExceeded()) {
|
|
1037
|
+
notify(ctx, `Rewind retention startup sweep skipped after exceeding ${startupBudgetMs}ms budget`, "warning");
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (await commitExists(commitSha)) {
|
|
1041
|
+
pinnedCommits.add(commitSha);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
let candidates = [...latestReferenceByCommit.entries()]
|
|
1046
|
+
.filter(([commitSha]) => !pinnedCommits.has(commitSha))
|
|
1047
|
+
.sort((left, right) => right[1] - left[1]);
|
|
1048
|
+
|
|
1049
|
+
if (typeof retention.maxAgeDays === "number" && retention.maxAgeDays >= 0) {
|
|
1050
|
+
const cutoff = Date.now() - retention.maxAgeDays * 24 * 60 * 60 * 1000;
|
|
1051
|
+
candidates = candidates.filter(([, timestamp]) => timestamp >= cutoff);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (typeof retention.maxSnapshots === "number" && retention.maxSnapshots >= 0 && candidates.length > retention.maxSnapshots) {
|
|
1055
|
+
candidates = candidates.slice(0, retention.maxSnapshots);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const liveSet = [...new Set([...pinnedCommits, ...candidates.map(([commitSha]) => commitSha)])];
|
|
1059
|
+
const existingLiveSet: string[] = [];
|
|
1060
|
+
for (const commitSha of liveSet) {
|
|
1061
|
+
if (budgetExceeded()) {
|
|
1062
|
+
notify(ctx, `Rewind retention startup sweep skipped after exceeding ${startupBudgetMs}ms budget`, "warning");
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
if (await commitExists(commitSha)) {
|
|
1066
|
+
existingLiveSet.push(commitSha);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const rewriteResult = await rewriteStoreToLiveSet(existingLiveSet);
|
|
1071
|
+
if (rewriteResult === "preserved-empty") {
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Skip gc on background startup sweeps to avoid racing with concurrent snapshot creation
|
|
1076
|
+
if (reason !== "startup") {
|
|
1077
|
+
try {
|
|
1078
|
+
const result = await pi.exec("git", ["gc", "--auto"]);
|
|
1079
|
+
if (result.code !== 0) {
|
|
1080
|
+
throw new Error(result.stderr.trim() || `git gc --auto failed with code ${result.code}`);
|
|
1081
|
+
}
|
|
1082
|
+
} catch {
|
|
1083
|
+
// Best effort only; retention reachability rewrite already completed.
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
newSnapshotsSinceSweep = 0;
|
|
1088
|
+
sweepCompletedThisSession = true;
|
|
1089
|
+
updateStatus(ctx);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
let activeContext: ExtensionContext | undefined;
|
|
1093
|
+
|
|
1094
|
+
async function checkpointEntry(ctx: ExtensionContext, entryId: string) {
|
|
1095
|
+
syncSessionIdentity(ctx);
|
|
1096
|
+
if (!isGitRepo) return;
|
|
1097
|
+
const entry = ctx.sessionManager.getEntry(entryId) as SessionLikeEntry | undefined;
|
|
1098
|
+
if (!isRestorableTreeEntry(entry)) return;
|
|
1099
|
+
if (entryToCommit.has(entryId)) return;
|
|
1100
|
+
|
|
1101
|
+
const currentCommitSha = await ensureSnapshotForCurrentWorktree();
|
|
1102
|
+
appendRewindOp(ctx, {
|
|
1103
|
+
v: RETENTION_VERSION,
|
|
1104
|
+
snapshots: [currentCommitSha],
|
|
1105
|
+
bindings: [[entryId, 0]],
|
|
1106
|
+
});
|
|
1107
|
+
await reconstructState(ctx);
|
|
1108
|
+
updateStatus(ctx);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async function initializeForSession(ctx: ExtensionContext) {
|
|
1112
|
+
activeContext = ctx;
|
|
1113
|
+
resetState();
|
|
1114
|
+
syncSessionIdentity(ctx);
|
|
1115
|
+
|
|
1116
|
+
try {
|
|
1117
|
+
const result = await pi.exec("git", ["rev-parse", "--is-inside-work-tree"]);
|
|
1118
|
+
isGitRepo = result.code === 0 && result.stdout.trim() === "true";
|
|
1119
|
+
} catch {
|
|
1120
|
+
// Treat git probing failures as non-git context for this session.
|
|
1121
|
+
isGitRepo = false;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (!isGitRepo) {
|
|
1125
|
+
updateStatus(ctx);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
await getRepoRoot(pi.exec);
|
|
1130
|
+
await reconstructState(ctx);
|
|
1131
|
+
updateStatus(ctx);
|
|
1132
|
+
maybeSweepRetention(ctx, "startup").catch((error) => {
|
|
1133
|
+
notify(ctx, `Rewind retention startup sweep failed: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
pi.events.on("rewind:fork-preference", (data) => {
|
|
1138
|
+
if (!data || typeof data !== "object") return;
|
|
1139
|
+
if (!("mode" in data) || data.mode !== "conversation-only") return;
|
|
1140
|
+
if (!("source" in data) || typeof data.source !== "string") return;
|
|
1141
|
+
if (!FORK_PREFERENCE_SOURCE_ALLOWLIST.has(data.source)) return;
|
|
1142
|
+
forceConversationOnlyOnNextFork = true;
|
|
1143
|
+
forceConversationOnlySource = data.source;
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
pi.events.on("rewind:checkpoint-entry", (data) => {
|
|
1147
|
+
if (!data || typeof data !== "object") return;
|
|
1148
|
+
if (!("source" in data) || typeof data.source !== "string") return;
|
|
1149
|
+
if (!CHECKPOINT_SOURCE_ALLOWLIST.has(data.source)) return;
|
|
1150
|
+
if (!("entryId" in data) || typeof data.entryId !== "string") return;
|
|
1151
|
+
const ctx = activeContext;
|
|
1152
|
+
if (!ctx) return;
|
|
1153
|
+
|
|
1154
|
+
checkpointEntry(ctx, data.entryId).catch((error) => {
|
|
1155
|
+
notify(ctx, `Rewind: failed to checkpoint ${data.entryId} (${error instanceof Error ? error.message : String(error)})`, "warning");
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
pi.on("before_agent_start", async (event) => {
|
|
1160
|
+
activePromptText = event.prompt;
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
pi.on("session_start", async (event, ctx) => {
|
|
1164
|
+
await initializeForSession(ctx);
|
|
1165
|
+
|
|
1166
|
+
if (event.reason !== "fork" || !event.previousSessionFile || !isGitRepo) {
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const previousLedger = await parseSessionLedgerFile(event.previousSessionFile);
|
|
1171
|
+
const pendingFork = previousLedger?.latestForkPending;
|
|
1172
|
+
if (!pendingFork) {
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (!(await commitExists(pendingFork.current))) {
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const snapshots = [pendingFork.current];
|
|
1181
|
+
const data: RewindOpData = { v: RETENTION_VERSION, snapshots, current: 0 };
|
|
1182
|
+
|
|
1183
|
+
if (pendingFork.undo && (await commitExists(pendingFork.undo))) {
|
|
1184
|
+
data.snapshots.push(pendingFork.undo);
|
|
1185
|
+
data.undo = 1;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
appendRewindOp(ctx, data);
|
|
1189
|
+
await reconstructState(ctx);
|
|
1190
|
+
updateStatus(ctx);
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
pi.on("session_tree", async (event, ctx) => {
|
|
1194
|
+
syncSessionIdentity(ctx);
|
|
1195
|
+
if (!isGitRepo || !pendingTreeState) {
|
|
1196
|
+
await reconstructState(ctx);
|
|
1197
|
+
updateStatus(ctx);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const snapshots = [pendingTreeState.currentCommitSha];
|
|
1202
|
+
const data: RewindOpData = { v: RETENTION_VERSION, snapshots, current: 0 };
|
|
1203
|
+
if (pendingTreeState.undoCommitSha) {
|
|
1204
|
+
data.snapshots.push(pendingTreeState.undoCommitSha);
|
|
1205
|
+
data.undo = 1;
|
|
1206
|
+
}
|
|
1207
|
+
if (event.summaryEntry?.id) {
|
|
1208
|
+
data.bindings = [[event.summaryEntry.id, 0]];
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
appendRewindOp(ctx, data);
|
|
1212
|
+
pendingTreeState = null;
|
|
1213
|
+
await reconstructState(ctx);
|
|
1214
|
+
updateStatus(ctx);
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
pi.on("session_compact", async (event, ctx) => {
|
|
1218
|
+
syncSessionIdentity(ctx);
|
|
1219
|
+
if (!isGitRepo) return;
|
|
1220
|
+
|
|
1221
|
+
let currentCommitSha = activeBranchState.currentCommitSha;
|
|
1222
|
+
if (!currentCommitSha) {
|
|
1223
|
+
currentCommitSha = await ensureSnapshotForCurrentWorktree();
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
appendRewindOp(ctx, {
|
|
1227
|
+
v: RETENTION_VERSION,
|
|
1228
|
+
snapshots: [currentCommitSha],
|
|
1229
|
+
bindings: [[event.compactionEntry.id, 0]],
|
|
1230
|
+
});
|
|
1231
|
+
await reconstructState(ctx);
|
|
1232
|
+
updateStatus(ctx);
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
1236
|
+
syncSessionIdentity(ctx);
|
|
1237
|
+
if (!isGitRepo) return;
|
|
1238
|
+
await maybeSweepRetention(ctx, "shutdown");
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
pi.on("turn_start", async (event, ctx) => {
|
|
1242
|
+
if (!isGitRepo) return;
|
|
1243
|
+
if (event.turnIndex !== 0) return;
|
|
1244
|
+
|
|
1245
|
+
try {
|
|
1246
|
+
const { treeSha } = await captureWorktreeTree();
|
|
1247
|
+
const commitSha = await ensureSnapshotForTree(treeSha);
|
|
1248
|
+
promptCollector = {
|
|
1249
|
+
snapshots: [],
|
|
1250
|
+
bindings: [],
|
|
1251
|
+
promptText: activePromptText ?? undefined,
|
|
1252
|
+
pendingUserCommitSha: commitSha,
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
bindPendingPromptUser(ctx.sessionManager.getBranch() as SessionLikeEntry[], promptCollector);
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
promptCollector = null;
|
|
1258
|
+
notify(ctx, `Rewind: failed to capture start snapshot (${error instanceof Error ? error.message : String(error)})`, "warning");
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
1263
|
+
if (!isGitRepo || !promptCollector) return;
|
|
1264
|
+
|
|
1265
|
+
try {
|
|
1266
|
+
const branchEntries = ctx.sessionManager.getBranch() as SessionLikeEntry[];
|
|
1267
|
+
bindPendingPromptUser(branchEntries, promptCollector);
|
|
1268
|
+
|
|
1269
|
+
if (event.message.role !== "assistant") return;
|
|
1270
|
+
|
|
1271
|
+
const assistantEntry = findAssistantEntryForTurn(branchEntries, event.message);
|
|
1272
|
+
if (!assistantEntry) return;
|
|
1273
|
+
|
|
1274
|
+
const { treeSha } = await captureWorktreeTree();
|
|
1275
|
+
const commitSha = await ensureSnapshotForTree(treeSha);
|
|
1276
|
+
addBindingToCollector(promptCollector, assistantEntry.id, commitSha);
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
notify(ctx, `Rewind: failed to capture assistant snapshot (${error instanceof Error ? error.message : String(error)})`, "warning");
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
1283
|
+
if (!isGitRepo || !promptCollector) return;
|
|
1284
|
+
|
|
1285
|
+
try {
|
|
1286
|
+
bindPendingPromptUser(ctx.sessionManager.getBranch() as SessionLikeEntry[], promptCollector);
|
|
1287
|
+
appendRewindTurn(ctx, promptCollector);
|
|
1288
|
+
await reconstructState(ctx);
|
|
1289
|
+
updateStatus(ctx);
|
|
1290
|
+
await maybeSweepRetention(ctx, "new-snapshots");
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
notify(ctx, `Rewind: failed to finalize rewind turn (${error instanceof Error ? error.message : String(error)})`, "warning");
|
|
1293
|
+
} finally {
|
|
1294
|
+
promptCollector = null;
|
|
1295
|
+
activePromptText = null;
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
pi.on("session_before_fork", async (event, ctx) => {
|
|
1300
|
+
const shouldForceConversationOnly = forceConversationOnlyOnNextFork;
|
|
1301
|
+
const forcedBySource = forceConversationOnlySource;
|
|
1302
|
+
forceConversationOnlyOnNextFork = false;
|
|
1303
|
+
forceConversationOnlySource = null;
|
|
1304
|
+
|
|
1305
|
+
if (!isGitRepo) return;
|
|
1306
|
+
|
|
1307
|
+
try {
|
|
1308
|
+
if (!ctx.hasUI) {
|
|
1309
|
+
const nextState = { currentCommitSha: await ensureSnapshotForCurrentWorktree() };
|
|
1310
|
+
appendForkPendingState(nextState);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const targetCommitSha = await resolveEntrySnapshotWithLineage(event.entryId);
|
|
1315
|
+
const hasUndo = Boolean(activeBranchState.undoCommitSha && (await commitExists(activeBranchState.undoCommitSha)));
|
|
1316
|
+
|
|
1317
|
+
if (shouldForceConversationOnly) {
|
|
1318
|
+
const nextState = { currentCommitSha: await ensureSnapshotForCurrentWorktree() };
|
|
1319
|
+
appendForkPendingState(nextState);
|
|
1320
|
+
notify(ctx, `Rewind: using conversation-only fork (keep current files)${forcedBySource ? ` (${forcedBySource})` : ""}`);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
const options = ["Conversation only (keep current files)"];
|
|
1325
|
+
if (targetCommitSha) {
|
|
1326
|
+
options.push("Restore all (files + conversation)", "Code only (restore files, keep conversation)");
|
|
1327
|
+
}
|
|
1328
|
+
if (hasUndo) {
|
|
1329
|
+
options.push("Undo last file rewind");
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const choice = await ctx.ui.select("Restore Options", options);
|
|
1333
|
+
if (!choice) {
|
|
1334
|
+
notify(ctx, "Rewind cancelled");
|
|
1335
|
+
return { cancel: true };
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (choice === "Undo last file rewind") {
|
|
1339
|
+
const restore = await restoreCommitExactly(activeBranchState.undoCommitSha!);
|
|
1340
|
+
const nextState = {
|
|
1341
|
+
currentCommitSha: activeBranchState.undoCommitSha!,
|
|
1342
|
+
undoCommitSha: restore.undoCommitSha,
|
|
1343
|
+
};
|
|
1344
|
+
appendForkPendingState(nextState);
|
|
1345
|
+
notify(ctx, "Files restored to before last rewind");
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (choice === "Conversation only (keep current files)") {
|
|
1350
|
+
const nextState = { currentCommitSha: await ensureSnapshotForCurrentWorktree() };
|
|
1351
|
+
appendForkPendingState(nextState);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if (!targetCommitSha) {
|
|
1356
|
+
notify(ctx, "No exact rewind point available for that entry", "error");
|
|
1357
|
+
return { cancel: true };
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const restore = await restoreCommitExactly(targetCommitSha);
|
|
1361
|
+
const nextState = {
|
|
1362
|
+
currentCommitSha: targetCommitSha,
|
|
1363
|
+
undoCommitSha: restore.undoCommitSha,
|
|
1364
|
+
};
|
|
1365
|
+
appendForkPendingState(nextState);
|
|
1366
|
+
notify(ctx, "Files restored from rewind point");
|
|
1367
|
+
|
|
1368
|
+
if (choice === "Code only (restore files, keep conversation)") {
|
|
1369
|
+
return { skipConversationRestore: true };
|
|
1370
|
+
}
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
notify(ctx, `Rewind failed before fork: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1373
|
+
return { cancel: true };
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
pi.on("session_before_tree", async (event, ctx) => {
|
|
1378
|
+
if (!isGitRepo) return;
|
|
1379
|
+
|
|
1380
|
+
try {
|
|
1381
|
+
if ((globalThis as typeof globalThis & { __boomerangCollapseInProgress?: boolean }).__boomerangCollapseInProgress === true) {
|
|
1382
|
+
pendingTreeState = { currentCommitSha: await ensureSnapshotForCurrentWorktree() };
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if (!ctx.hasUI) return;
|
|
1387
|
+
|
|
1388
|
+
const targetEntry = ctx.sessionManager.getEntry(event.preparation.targetId) as SessionLikeEntry | undefined;
|
|
1389
|
+
const targetCommitSha = isRestorableTreeEntry(targetEntry)
|
|
1390
|
+
? await resolveEntrySnapshotWithLineage(event.preparation.targetId, currentSessionFile)
|
|
1391
|
+
: undefined;
|
|
1392
|
+
const hasUndo = Boolean(activeBranchState.undoCommitSha && (await commitExists(activeBranchState.undoCommitSha)));
|
|
1393
|
+
|
|
1394
|
+
const options = ["Keep current files"];
|
|
1395
|
+
if (targetCommitSha) {
|
|
1396
|
+
options.push("Restore files to that point");
|
|
1397
|
+
}
|
|
1398
|
+
if (hasUndo) {
|
|
1399
|
+
options.push("Undo last file rewind");
|
|
1400
|
+
}
|
|
1401
|
+
options.push("Cancel navigation");
|
|
1402
|
+
|
|
1403
|
+
const choice = await ctx.ui.select("Restore Options", options);
|
|
1404
|
+
if (!choice || choice === "Cancel navigation") {
|
|
1405
|
+
notify(ctx, "Navigation cancelled");
|
|
1406
|
+
return { cancel: true };
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
if (choice === "Undo last file rewind") {
|
|
1410
|
+
const restore = await restoreCommitExactly(activeBranchState.undoCommitSha!);
|
|
1411
|
+
const snapshots = [activeBranchState.undoCommitSha!];
|
|
1412
|
+
const data: RewindOpData = { v: RETENTION_VERSION, snapshots, current: 0 };
|
|
1413
|
+
if (restore.undoCommitSha) {
|
|
1414
|
+
data.snapshots.push(restore.undoCommitSha);
|
|
1415
|
+
data.undo = 1;
|
|
1416
|
+
}
|
|
1417
|
+
appendRewindOp(ctx, data);
|
|
1418
|
+
notify(ctx, "Files restored to before last rewind");
|
|
1419
|
+
await reconstructState(ctx);
|
|
1420
|
+
return { cancel: true };
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
if (choice === "Keep current files") {
|
|
1424
|
+
pendingTreeState = { currentCommitSha: await ensureSnapshotForCurrentWorktree() };
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
if (!targetCommitSha) {
|
|
1429
|
+
notify(ctx, "Exact file rewind is only available for user, assistant, custom message, compaction, and summary nodes", "error");
|
|
1430
|
+
return { cancel: true };
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
const restore = await restoreCommitExactly(targetCommitSha);
|
|
1434
|
+
pendingTreeState = {
|
|
1435
|
+
currentCommitSha: targetCommitSha,
|
|
1436
|
+
undoCommitSha: restore.undoCommitSha,
|
|
1437
|
+
};
|
|
1438
|
+
notify(ctx, "Files restored to rewind point");
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
pendingTreeState = null;
|
|
1441
|
+
notify(ctx, `Rewind failed before tree navigation: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1442
|
+
return { cancel: true };
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
}
|