@selesai/code 0.10.1 → 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.
Files changed (457) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/cli/credential-print.js +4 -4
  3. package/dist/cli.js +0 -0
  4. package/dist/config.d.ts +5 -3
  5. package/dist/config.js +6 -4
  6. package/dist/core/compaction/branch-summarization.js +30 -30
  7. package/dist/core/compaction/compaction.js +81 -81
  8. package/dist/core/compaction/utils.js +2 -2
  9. package/dist/core/export-html/template.css +1066 -1066
  10. package/dist/core/export-html/template.html +55 -55
  11. package/dist/core/export-html/template.js +1864 -1864
  12. package/dist/core/export-html/vendor/highlight.min.js +1212 -1212
  13. package/dist/core/export-html/vendor/marked.min.js +78 -78
  14. package/dist/core/messages.js +7 -7
  15. package/dist/core/skills.d.ts +2 -0
  16. package/dist/core/skills.js +1 -0
  17. package/dist/extensions/context-compaction-reminder.test.ts +82 -82
  18. package/dist/extensions/context-compaction-reminder.ts +28 -28
  19. package/dist/extensions/pi-intercom/LICENSE +21 -21
  20. package/dist/extensions/pi-intercom/broker/client.test.ts +83 -83
  21. package/dist/extensions/pi-intercom/broker/framing.test.ts +114 -114
  22. package/dist/extensions/pi-intercom/broker/paths.ts +134 -134
  23. package/dist/extensions/pi-intercom/broker/runtime-claim.test.ts +34 -34
  24. package/dist/extensions/pi-intercom/broker/runtime-claim.ts +21 -21
  25. package/dist/extensions/pi-intercom/cwd.test.ts +40 -40
  26. package/dist/extensions/pi-intercom/cwd.ts +31 -31
  27. package/dist/extensions/pi-intercom/format-context.test.ts +31 -31
  28. package/dist/extensions/pi-intercom/format-context.ts +32 -32
  29. package/dist/extensions/pi-intercom/test/overlay-width.test.ts +66 -66
  30. package/dist/extensions/pi-intercom/ui/compose.ts +143 -143
  31. package/dist/extensions/pi-intercom/ui/session-list.ts +166 -166
  32. package/dist/extensions/pi-rewind-hook/README.md +245 -245
  33. package/dist/extensions/pi-rewind-hook/index.ts +1445 -1445
  34. package/dist/extensions/pi-rewind-hook/package.json +29 -29
  35. package/dist/extensions/pi-subagents/install.mjs +0 -0
  36. package/dist/extensions/pi-web-agent/package.json +31 -31
  37. package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -205
  38. package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -136
  39. package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -152
  40. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -25
  41. package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -28
  42. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +136 -136
  43. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -946
  44. package/dist/extensions/pi-web-agent/src/extension.ts +126 -126
  45. package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -118
  46. package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -199
  47. package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -100
  48. package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -117
  49. package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -67
  50. package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -60
  51. package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -50
  52. package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -52
  53. package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -105
  54. package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -45
  55. package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -28
  56. package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -47
  57. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -376
  58. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -64
  59. package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -181
  60. package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -101
  61. package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -81
  62. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +210 -210
  63. package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -75
  64. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -61
  65. package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -54
  66. package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -41
  67. package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -20
  68. package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -63
  69. package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -114
  70. package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -72
  71. package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -96
  72. package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -71
  73. package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -31
  74. package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -31
  75. package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -157
  76. package/dist/extensions/pi-web-agent/src/types.ts +88 -88
  77. package/dist/extensions/ponytail/package.json +8 -8
  78. package/dist/extensions/question/constants.ts +30 -30
  79. package/dist/extensions/question/helpers.ts +58 -58
  80. package/dist/extensions/question/navigation.ts +14 -14
  81. package/dist/extensions/question/package.json +19 -19
  82. package/dist/extensions/question/selection-mode.ts +46 -46
  83. package/dist/extensions/question/shortcuts.ts +44 -44
  84. package/dist/extensions/question/types.ts +132 -132
  85. package/dist/extensions/test-resolve-hook-impl.mjs +6 -6
  86. package/dist/extensions/test-resolve-hook.mjs +6 -6
  87. package/dist/extensions/web-agent-onboarding.ts +222 -222
  88. package/dist/modes/interactive/components/startup-box.d.ts +38 -0
  89. package/dist/modes/interactive/components/startup-box.js +90 -0
  90. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  91. package/dist/modes/interactive/interactive-mode.js +71 -122
  92. package/dist/package-manager-cli.js +71 -71
  93. package/dist/rpc-entry.js +0 -0
  94. package/dist/skills/agent-browser/SKILL.md +1 -0
  95. package/dist/skills/batch-grill-me/SKILL.md +1 -0
  96. package/dist/skills/brandkit/SKILL.md +1 -0
  97. package/dist/skills/design-references/SKILL.md +1 -0
  98. package/dist/skills/design-taste-frontend/SKILL.md +1 -0
  99. package/dist/skills/design-taste-frontend-v1/SKILL.md +1 -0
  100. package/dist/skills/full-output-enforcement/SKILL.md +1 -0
  101. package/dist/skills/gpt-taste/SKILL.md +1 -0
  102. package/dist/skills/grill-me/SKILL.md +1 -0
  103. package/dist/skills/handoff/SKILL.md +1 -0
  104. package/dist/skills/handoff-text/SKILL.md +1 -0
  105. package/dist/skills/high-end-visual-design/SKILL.md +1 -0
  106. package/dist/skills/image-to-code/SKILL.md +1 -0
  107. package/dist/skills/imagegen-frontend-mobile/SKILL.md +1 -0
  108. package/dist/skills/imagegen-frontend-web/SKILL.md +1 -0
  109. package/dist/skills/implanger/SKILL.md +1 -0
  110. package/dist/skills/improve-codebase/REFERENCE.md +78 -78
  111. package/dist/skills/improve-codebase/SKILL.md +1 -0
  112. package/dist/skills/industrial-brutalist-ui/SKILL.md +1 -0
  113. package/dist/skills/minimalist-ui/SKILL.md +1 -0
  114. package/dist/skills/pi-subagents/SKILL.md +1 -0
  115. package/dist/skills/planger/SKILL.md +1 -0
  116. package/dist/skills/ponytail/SKILL.md +1 -0
  117. package/dist/skills/ponytail-audit/SKILL.md +1 -0
  118. package/dist/skills/ponytail-debt/SKILL.md +1 -0
  119. package/dist/skills/ponytail-gain/SKILL.md +1 -0
  120. package/dist/skills/ponytail-help/SKILL.md +1 -0
  121. package/dist/skills/ponytail-review/SKILL.md +1 -0
  122. package/dist/skills/redesign-existing-projects/SKILL.md +1 -0
  123. package/dist/skills/selesai-handoff/SKILL.md +1 -0
  124. package/dist/skills/stitch-design-taste/SKILL.md +1 -0
  125. package/dist/skills/unlazy/SKILL.md +1 -0
  126. package/dist/skills/web-design-guidelines/SKILL.md +1 -0
  127. package/dist/skills/workflow/SKILL.md +1 -0
  128. package/docs/compaction.md +396 -396
  129. package/docs/containerization.md +111 -111
  130. package/docs/development.md +71 -71
  131. package/docs/docs.json +164 -164
  132. package/docs/environment-variables.md +86 -86
  133. package/docs/index.md +83 -83
  134. package/docs/json.md +82 -82
  135. package/docs/models.md +502 -502
  136. package/docs/packages.md +227 -227
  137. package/docs/plans/subagent-delegation/phase-0-correctness.md +264 -264
  138. package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +485 -485
  139. package/docs/plans/subagent-delegation/phase-2-context-controls.md +281 -281
  140. package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +361 -361
  141. package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +380 -380
  142. package/docs/prompt-templates.md +95 -95
  143. package/docs/providers.md +293 -293
  144. package/docs/security.md +59 -59
  145. package/docs/session-format.md +414 -414
  146. package/docs/sessions.md +145 -145
  147. package/docs/shared-host-extensions.md +109 -109
  148. package/docs/shell-aliases.md +13 -13
  149. package/docs/skills.md +231 -231
  150. package/docs/terminal-setup.md +142 -142
  151. package/docs/termux.md +127 -127
  152. package/docs/themes.md +295 -295
  153. package/docs/tmux.md +63 -63
  154. package/docs/tui.md +927 -927
  155. package/docs/windows.md +17 -17
  156. package/examples/README.md +25 -25
  157. package/examples/extensions/README.md +211 -211
  158. package/examples/extensions/auto-commit-on-exit.ts +49 -49
  159. package/examples/extensions/bash-spawn-hook.ts +30 -30
  160. package/examples/extensions/bookmark.ts +50 -50
  161. package/examples/extensions/border-status-editor.ts +150 -150
  162. package/examples/extensions/built-in-tool-renderer.ts +249 -249
  163. package/examples/extensions/claude-rules.ts +86 -86
  164. package/examples/extensions/commands.ts +72 -72
  165. package/examples/extensions/confirm-destructive.ts +59 -59
  166. package/examples/extensions/custom-compaction.ts +130 -130
  167. package/examples/extensions/custom-footer.ts +64 -64
  168. package/examples/extensions/custom-header.ts +73 -73
  169. package/examples/extensions/custom-provider-anthropic/index.ts +610 -610
  170. package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -24
  171. package/examples/extensions/custom-provider-anthropic/package.json +19 -19
  172. package/examples/extensions/custom-provider-gitlab-duo/index.ts +404 -404
  173. package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -16
  174. package/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -82
  175. package/examples/extensions/dirty-repo-guard.ts +56 -56
  176. package/examples/extensions/doom-overlay/README.md +46 -46
  177. package/examples/extensions/doom-overlay/doom/build/doom.js +21 -21
  178. package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
  179. package/examples/extensions/doom-overlay/doom/build.sh +152 -152
  180. package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -72
  181. package/examples/extensions/doom-overlay/doom-component.ts +132 -132
  182. package/examples/extensions/doom-overlay/doom-engine.ts +173 -173
  183. package/examples/extensions/doom-overlay/doom-keys.ts +104 -104
  184. package/examples/extensions/doom-overlay/index.ts +74 -74
  185. package/examples/extensions/doom-overlay/wad-finder.ts +51 -51
  186. package/examples/extensions/dynamic-resources/SKILL.md +8 -8
  187. package/examples/extensions/dynamic-resources/dynamic.json +79 -79
  188. package/examples/extensions/dynamic-resources/dynamic.md +5 -5
  189. package/examples/extensions/dynamic-resources/index.ts +15 -15
  190. package/examples/extensions/dynamic-tools.ts +74 -74
  191. package/examples/extensions/event-bus.ts +43 -43
  192. package/examples/extensions/file-trigger.ts +41 -41
  193. package/examples/extensions/git-checkpoint.ts +53 -53
  194. package/examples/extensions/git-merge-and-resolve.ts +115 -115
  195. package/examples/extensions/github-issue-autocomplete.ts +185 -185
  196. package/examples/extensions/gondolin/index.ts +531 -531
  197. package/examples/extensions/gondolin/package-lock.json +185 -185
  198. package/examples/extensions/gondolin/package.json +19 -19
  199. package/examples/extensions/handoff.ts +199 -199
  200. package/examples/extensions/hello.ts +26 -26
  201. package/examples/extensions/hidden-thinking-label.ts +53 -53
  202. package/examples/extensions/inline-bash.ts +94 -94
  203. package/examples/extensions/input-transform-streaming.ts +39 -39
  204. package/examples/extensions/input-transform.ts +43 -43
  205. package/examples/extensions/interactive-shell.ts +196 -196
  206. package/examples/extensions/mac-system-theme.ts +47 -47
  207. package/examples/extensions/message-renderer.ts +59 -59
  208. package/examples/extensions/minimal-mode.ts +426 -426
  209. package/examples/extensions/modal-editor.ts +85 -85
  210. package/examples/extensions/model-status.ts +31 -31
  211. package/examples/extensions/notify.ts +55 -55
  212. package/examples/extensions/overlay-qa-tests.ts +1450 -1450
  213. package/examples/extensions/overlay-test.ts +153 -153
  214. package/examples/extensions/permission-gate.ts +34 -34
  215. package/examples/extensions/pirate.ts +47 -47
  216. package/examples/extensions/plan-mode/README.md +66 -66
  217. package/examples/extensions/plan-mode/index.ts +390 -390
  218. package/examples/extensions/plan-mode/utils.ts +168 -168
  219. package/examples/extensions/preset.ts +436 -436
  220. package/examples/extensions/project-trust.ts +64 -64
  221. package/examples/extensions/prompt-customizer.ts +97 -97
  222. package/examples/extensions/protected-paths.ts +30 -30
  223. package/examples/extensions/provider-payload.ts +18 -18
  224. package/examples/extensions/qna.ts +122 -122
  225. package/examples/extensions/question.ts +285 -285
  226. package/examples/extensions/questionnaire.ts +448 -448
  227. package/examples/extensions/rainbow-editor.ts +88 -88
  228. package/examples/extensions/reload-runtime.ts +37 -37
  229. package/examples/extensions/rpc-demo.ts +118 -118
  230. package/examples/extensions/sandbox/index.ts +321 -321
  231. package/examples/extensions/sandbox/package-lock.json +92 -92
  232. package/examples/extensions/sandbox/package.json +19 -19
  233. package/examples/extensions/send-user-message.ts +97 -97
  234. package/examples/extensions/session-name.ts +27 -27
  235. package/examples/extensions/shutdown-command.ts +63 -63
  236. package/examples/extensions/snake.ts +343 -343
  237. package/examples/extensions/space-invaders.ts +560 -560
  238. package/examples/extensions/ssh.ts +220 -220
  239. package/examples/extensions/status-line.ts +32 -32
  240. package/examples/extensions/structured-output.ts +65 -65
  241. package/examples/extensions/subagent/README.md +175 -175
  242. package/examples/extensions/subagent/agents/planner.md +37 -37
  243. package/examples/extensions/subagent/agents/reviewer.md +35 -35
  244. package/examples/extensions/subagent/agents/scout.md +50 -50
  245. package/examples/extensions/subagent/agents/worker.md +24 -24
  246. package/examples/extensions/subagent/agents.ts +126 -126
  247. package/examples/extensions/subagent/index.ts +1015 -1015
  248. package/examples/extensions/subagent/prompts/implement-and-review.md +10 -10
  249. package/examples/extensions/subagent/prompts/implement.md +10 -10
  250. package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -9
  251. package/examples/extensions/summarize.ts +209 -209
  252. package/examples/extensions/system-prompt-header.ts +17 -17
  253. package/examples/extensions/tic-tac-toe.ts +1008 -1008
  254. package/examples/extensions/timed-confirm.ts +70 -70
  255. package/examples/extensions/titlebar-spinner.ts +58 -58
  256. package/examples/extensions/todo.ts +297 -297
  257. package/examples/extensions/tool-override.ts +144 -144
  258. package/examples/extensions/tools.ts +146 -146
  259. package/examples/extensions/trigger-compact.ts +50 -50
  260. package/examples/extensions/truncated-tool.ts +195 -195
  261. package/examples/extensions/widget-placement.ts +9 -9
  262. package/examples/extensions/with-deps/index.ts +32 -32
  263. package/examples/extensions/with-deps/package-lock.json +31 -31
  264. package/examples/extensions/with-deps/package.json +22 -22
  265. package/examples/extensions/working-indicator.ts +123 -123
  266. package/examples/extensions/working-message-test.ts +25 -25
  267. package/examples/rpc-extension-ui.ts +632 -632
  268. package/examples/sdk/01-minimal.ts +26 -26
  269. package/examples/sdk/02-custom-model.ts +53 -53
  270. package/examples/sdk/03-custom-prompt.ts +75 -75
  271. package/examples/sdk/04-skills.ts +55 -55
  272. package/examples/sdk/05-tools.ts +48 -48
  273. package/examples/sdk/06-extensions.ts +99 -99
  274. package/examples/sdk/07-context-files.ts +47 -47
  275. package/examples/sdk/08-prompt-templates.ts +51 -51
  276. package/examples/sdk/09-api-keys-and-oauth.ts +52 -52
  277. package/examples/sdk/10-settings.ts +53 -53
  278. package/examples/sdk/11-sessions.ts +52 -52
  279. package/examples/sdk/12-full-control.ts +79 -79
  280. package/examples/sdk/13-session-runtime.ts +67 -67
  281. package/examples/sdk/README.md +144 -144
  282. package/package.json +1 -1
  283. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/cli.d.ts +0 -3
  284. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/register-bedrock.d.ts +0 -2
  285. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/restore-sandbox-env.d.ts +0 -17
  286. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/args.d.ts +0 -57
  287. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/config-selector.d.ts +0 -16
  288. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/file-processor.d.ts +0 -15
  289. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/initial-message.d.ts +0 -18
  290. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/list-models.d.ts +0 -9
  291. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/project-trust.d.ts +0 -10
  292. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/session-picker.d.ts +0 -10
  293. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/startup-ui.d.ts +0 -20
  294. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli.d.ts +0 -3
  295. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/config.d.ts +0 -96
  296. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session-runtime.d.ts +0 -119
  297. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session-services.d.ts +0 -85
  298. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session.d.ts +0 -620
  299. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/auth-guidance.d.ts +0 -5
  300. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/auth-storage.d.ts +0 -56
  301. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/bash-executor.d.ts +0 -32
  302. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/cache-stats.d.ts +0 -49
  303. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/branch-summarization.d.ts +0 -93
  304. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/compaction.d.ts +0 -128
  305. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/index.d.ts +0 -7
  306. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/utils.d.ts +0 -38
  307. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/defaults.d.ts +0 -3
  308. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/diagnostics.d.ts +0 -15
  309. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/event-bus.d.ts +0 -9
  310. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/exec.d.ts +0 -29
  311. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/experimental.d.ts +0 -2
  312. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/ansi-to-html.d.ts +0 -22
  313. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/index.d.ts +0 -37
  314. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/tool-renderer.d.ts +0 -34
  315. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/index.d.ts +0 -12
  316. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/loader.d.ts +0 -23
  317. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/runner.d.ts +0 -171
  318. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +0 -1261
  319. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/wrapper.d.ts +0 -20
  320. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/footer-data-provider.d.ts +0 -54
  321. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/http-dispatcher.d.ts +0 -22
  322. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/index.d.ts +0 -13
  323. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/keybindings.d.ts +0 -358
  324. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/messages.d.ts +0 -77
  325. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-config.d.ts +0 -512
  326. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-registry.d.ts +0 -43
  327. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-resolver.d.ts +0 -121
  328. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-runtime.d.ts +0 -82
  329. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/models-store.d.ts +0 -17
  330. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/output-guard.d.ts +0 -7
  331. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/package-manager.d.ts +0 -210
  332. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/project-trust.d.ts +0 -15
  333. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/prompt-templates.d.ts +0 -54
  334. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/provider-attribution.d.ts +0 -4
  335. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/provider-composer.d.ts +0 -55
  336. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/radius.d.ts +0 -2
  337. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/remote-catalog-provider.d.ts +0 -5
  338. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/resolve-config-value.d.ts +0 -30
  339. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/resource-loader.d.ts +0 -206
  340. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/runtime-credentials.d.ts +0 -15
  341. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/sdk.d.ts +0 -106
  342. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/session-cwd.d.ts +0 -19
  343. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/session-manager.d.ts +0 -356
  344. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/settings-manager.d.ts +0 -297
  345. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/skills.d.ts +0 -60
  346. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/slash-commands.d.ts +0 -15
  347. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/source-info.d.ts +0 -18
  348. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/system-prompt.d.ts +0 -28
  349. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/telemetry.d.ts +0 -3
  350. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/timings.d.ts +0 -10
  351. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/bash.d.ts +0 -68
  352. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/edit-diff.d.ts +0 -106
  353. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/edit.d.ts +0 -51
  354. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/file-mutation-queue.d.ts +0 -6
  355. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/find.d.ts +0 -35
  356. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/grep.d.ts +0 -37
  357. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/index.d.ts +0 -40
  358. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/ls.d.ts +0 -37
  359. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/output-accumulator.d.ts +0 -52
  360. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/path-utils.d.ts +0 -10
  361. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/read.d.ts +0 -35
  362. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/render-utils.d.ts +0 -24
  363. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/tool-definition-wrapper.d.ts +0 -14
  364. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/truncate.d.ts +0 -70
  365. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/write.d.ts +0 -26
  366. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/trust-manager.d.ts +0 -36
  367. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/usage-totals.d.ts +0 -19
  368. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/index.d.ts +0 -3
  369. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/client.d.ts +0 -61
  370. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/huggingface.d.ts +0 -23
  371. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/index.d.ts +0 -3
  372. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/provider.d.ts +0 -10
  373. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/ui.d.ts +0 -42
  374. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/index.d.ts +0 -35
  375. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/main.d.ts +0 -12
  376. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/migrations.d.ts +0 -33
  377. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/index.d.ts +0 -9
  378. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/armin.d.ts +0 -34
  379. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/assistant-message.d.ts +0 -22
  380. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/bash-execution.d.ts +0 -34
  381. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/bordered-loader.d.ts +0 -16
  382. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/branch-summary-message.d.ts +0 -16
  383. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/compaction-summary-message.d.ts +0 -16
  384. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/config-selector.d.ts +0 -102
  385. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/countdown-timer.d.ts +0 -14
  386. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-editor.d.ts +0 -21
  387. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-entry.d.ts +0 -19
  388. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-message.d.ts +0 -20
  389. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/daxnuts.d.ts +0 -23
  390. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/diff.d.ts +0 -12
  391. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/dynamic-border.d.ts +0 -15
  392. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/earendil-announcement.d.ts +0 -5
  393. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-editor.d.ts +0 -22
  394. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-input.d.ts +0 -23
  395. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-selector.d.ts +0 -26
  396. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/first-time-setup.d.ts +0 -25
  397. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/footer.d.ts +0 -32
  398. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/index.d.ts +0 -34
  399. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/keybinding-hints.d.ts +0 -13
  400. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/login-dialog.d.ts +0 -52
  401. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/model-selector.d.ts +0 -54
  402. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/oauth-selector.d.ts +0 -33
  403. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/scoped-models-selector.d.ts +0 -42
  404. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/session-selector-search.d.ts +0 -23
  405. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/session-selector.d.ts +0 -95
  406. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/settings-selector.d.ts +0 -77
  407. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/show-images-selector.d.ts +0 -10
  408. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/skill-invocation-message.d.ts +0 -17
  409. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/status-indicator.d.ts +0 -28
  410. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/theme-selector.d.ts +0 -11
  411. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/thinking-selector.d.ts +0 -11
  412. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/tool-execution.d.ts +0 -63
  413. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/tree-selector.d.ts +0 -94
  414. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/trust-selector.d.ts +0 -23
  415. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/user-message-selector.d.ts +0 -30
  416. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/user-message.d.ts +0 -14
  417. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/visual-truncate.d.ts +0 -24
  418. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/interactive-mode.d.ts +0 -394
  419. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/model-search.d.ts +0 -12
  420. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/theme/theme-controller.d.ts +0 -29
  421. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/theme/theme.d.ts +0 -120
  422. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/print-mode.d.ts +0 -28
  423. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/jsonl.d.ts +0 -17
  424. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-client.d.ts +0 -246
  425. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-mode.d.ts +0 -20
  426. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-types.d.ts +0 -457
  427. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/package-manager-cli.d.ts +0 -8
  428. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/rpc-entry.d.ts +0 -3
  429. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/ansi.d.ts +0 -2
  430. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/changelog.d.ts +0 -22
  431. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/child-process.d.ts +0 -18
  432. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard-image.d.ts +0 -11
  433. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard-native.d.ts +0 -11
  434. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard.d.ts +0 -4
  435. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/deprecation.d.ts +0 -4
  436. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/exif-orientation.d.ts +0 -5
  437. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/frontmatter.d.ts +0 -8
  438. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/fs-watch.d.ts +0 -5
  439. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/git.d.ts +0 -26
  440. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/html.d.ts +0 -7
  441. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-convert.d.ts +0 -10
  442. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-process.d.ts +0 -18
  443. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize-core.d.ts +0 -30
  444. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize-worker.d.ts +0 -2
  445. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize.d.ts +0 -16
  446. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/json.d.ts +0 -3
  447. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/mime.d.ts +0 -3
  448. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/open-browser.d.ts +0 -9
  449. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/paths.d.ts +0 -31
  450. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/photon.d.ts +0 -21
  451. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/pi-user-agent.d.ts +0 -2
  452. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/shell.d.ts +0 -31
  453. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/sleep.d.ts +0 -5
  454. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/syntax-highlight.d.ts +0 -12
  455. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/tools-manager.d.ts +0 -3
  456. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/version-check.d.ts +0 -15
  457. 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
+ }