@dyyz1993/pi-coding-agent 0.70.5 → 0.74.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +266 -80
- package/README.md +48 -20
- package/dist/bun/cli.d.ts.map +1 -1
- package/dist/bun/cli.js +4 -2
- package/dist/bun/cli.js.map +1 -1
- package/dist/bun/restore-sandbox-env.d.ts +13 -0
- package/dist/bun/restore-sandbox-env.d.ts.map +1 -0
- package/dist/bun/restore-sandbox-env.js +32 -0
- package/dist/bun/restore-sandbox-env.js.map +1 -0
- package/dist/cli/args.d.ts +2 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +34 -22
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/list-models.d.ts.map +1 -1
- package/dist/cli/list-models.js +2 -1
- package/dist/cli/list-models.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +16 -8
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +238 -66
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts +10 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +14 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +2 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +25 -26
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1042 -1116
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-types.d.ts +58 -0
- package/dist/core/agent-types.d.ts.map +1 -0
- package/dist/core/agent-types.js +203 -0
- package/dist/core/agent-types.js.map +1 -0
- package/dist/core/auth-guidance.d.ts +5 -0
- package/dist/core/auth-guidance.d.ts.map +1 -0
- package/dist/core/auth-guidance.js +21 -0
- package/dist/core/auth-guidance.js.map +1 -0
- package/dist/core/auth-storage.d.ts +9 -0
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +20 -1
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +9 -6
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +0 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/export-html/ansi-to-html.d.ts.map +1 -1
- package/dist/core/export-html/ansi-to-html.js +1 -1
- package/dist/core/export-html/ansi-to-html.js.map +1 -1
- package/dist/core/export-html/template.css +53 -4
- package/dist/core/export-html/template.js +84 -20
- package/dist/core/export-html/tool-renderer.d.ts +0 -6
- package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
- package/dist/core/export-html/tool-renderer.js +15 -2
- package/dist/core/export-html/tool-renderer.js.map +1 -1
- package/dist/core/extensions/channel-factory.d.ts +13 -0
- package/dist/core/extensions/channel-factory.d.ts.map +1 -0
- package/dist/core/extensions/channel-factory.js +19 -0
- package/dist/core/extensions/channel-factory.js.map +1 -0
- package/dist/core/extensions/channel-registry.d.ts +28 -0
- package/dist/core/extensions/channel-registry.d.ts.map +1 -0
- package/dist/core/extensions/channel-registry.js +12 -0
- package/dist/core/extensions/channel-registry.js.map +1 -0
- package/dist/core/extensions/index.d.ts +4 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -0
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +0 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +49 -137
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +24 -20
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +128 -253
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/server-channel.d.ts +8 -8
- package/dist/core/extensions/server-channel.d.ts.map +1 -1
- package/dist/core/extensions/server-channel.js.map +1 -1
- package/dist/core/extensions/types.d.ts +88 -60
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js +10 -0
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/file-store/file-snapshot-manager.d.ts +95 -0
- package/dist/core/file-store/file-snapshot-manager.d.ts.map +1 -0
- package/dist/core/file-store/file-snapshot-manager.js +508 -0
- package/dist/core/file-store/file-snapshot-manager.js.map +1 -0
- package/dist/core/file-store/index.d.ts +5 -0
- package/dist/core/file-store/index.d.ts.map +1 -0
- package/dist/core/file-store/index.js +3 -0
- package/dist/core/file-store/index.js.map +1 -0
- package/dist/core/messages.d.ts +10 -2
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +23 -6
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-registry.d.ts +19 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +97 -16
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +24 -15
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/package-manager.d.ts +1 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +61 -35
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/provider-display-names.d.ts +2 -0
- package/dist/core/provider-display-names.d.ts.map +1 -0
- package/dist/core/provider-display-names.js +32 -0
- package/dist/core/provider-display-names.js.map +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +9 -21
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +9 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +39 -18
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +27 -17
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +133 -47
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +21 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +51 -6
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +3 -8
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +4 -3
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/bash.d.ts +0 -2
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +108 -154
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +3 -2
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +4 -3
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +1 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/output-accumulator.d.ts +50 -0
- package/dist/core/tools/output-accumulator.d.ts.map +1 -0
- package/dist/core/tools/output-accumulator.js +178 -0
- package/dist/core/tools/output-accumulator.js.map +1 -0
- package/dist/core/tools/output-collector.d.ts +35 -0
- package/dist/core/tools/output-collector.d.ts.map +1 -0
- package/dist/core/tools/output-collector.js +79 -0
- package/dist/core/tools/output-collector.js.map +1 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +70 -13
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/spawn-managed.d.ts +18 -0
- package/dist/core/tools/spawn-managed.d.ts.map +1 -0
- package/dist/core/tools/spawn-managed.js +52 -0
- package/dist/core/tools/spawn-managed.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +17 -39
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +3 -3
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +3 -1
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +1 -4
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +14 -56
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +5 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +19 -4
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +1 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts +18 -6
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +93 -25
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +3 -7
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +5 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +53 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +20 -4
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +423 -186
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +1 -1
- package/dist/modes/interactive/theme/light.json +1 -1
- package/dist/modes/print-mode.d.ts +3 -0
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +62 -19
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +80 -60
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +108 -93
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +106 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +115 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +238 -12
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/child-process.d.ts +1 -0
- package/dist/utils/child-process.d.ts.map +1 -1
- package/dist/utils/child-process.js +8 -0
- package/dist/utils/child-process.js.map +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +2 -2
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +84 -45
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/paths.d.ts +9 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +31 -0
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/pi-user-agent.d.ts +2 -0
- package/dist/utils/pi-user-agent.d.ts.map +1 -0
- package/dist/utils/pi-user-agent.js +5 -0
- package/dist/utils/pi-user-agent.js.map +1 -0
- package/dist/utils/structured-output.d.ts +10 -0
- package/dist/utils/structured-output.d.ts.map +1 -0
- package/dist/utils/structured-output.js +57 -0
- package/dist/utils/structured-output.js.map +1 -0
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +6 -2
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts +14 -0
- package/dist/utils/version-check.d.ts.map +1 -0
- package/dist/utils/version-check.js +77 -0
- package/dist/utils/version-check.js.map +1 -0
- package/docs/compaction.md +14 -14
- package/docs/custom-provider.md +40 -31
- package/docs/development.md +1 -1
- package/docs/docs.json +148 -0
- package/docs/extensions.md +116 -56
- package/docs/index.md +70 -0
- package/docs/json.md +4 -4
- package/docs/models.md +150 -3
- package/docs/packages.md +10 -5
- package/docs/providers.md +62 -17
- package/docs/quickstart.md +142 -0
- package/docs/rollback-architecture.md +693 -0
- package/docs/rollback-test-cases.md +412 -0
- package/docs/rpc.md +1 -1
- package/docs/sdk.md +26 -26
- package/docs/{session.md → session-format.md} +6 -6
- package/docs/sessions.md +137 -0
- package/docs/settings.md +52 -9
- package/docs/termux.md +1 -1
- package/docs/themes.md +2 -2
- package/docs/tui.md +20 -20
- package/docs/usage.md +277 -0
- package/examples/extensions/README.md +2 -4
- package/examples/extensions/border-status-editor.ts +150 -0
- package/examples/extensions/commands.ts +2 -2
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
- package/examples/extensions/dynamic-resources/dynamic.json +1 -1
- package/examples/extensions/git-checkpoint.ts +1 -1
- package/examples/extensions/handoff.ts +49 -11
- package/examples/extensions/plan-mode/index.ts +1 -1
- package/examples/extensions/sandbox/package-lock.json +5 -5
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/agents.ts +126 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/README.md +2 -2
- package/package.json +7 -15
- package/docs/tree.md +0 -233
- package/examples/extensions/antigravity-image-gen.ts +0 -418
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
# Rollback Architecture — Design Document
|
|
2
|
+
|
|
3
|
+
## 1. Overview
|
|
4
|
+
|
|
5
|
+
The session tree supports branching and navigation via `navigateTree()`, but file system state does not always follow. The `file-snapshot` extension creates per-turn snapshots using content-addressable storage and restores files on `session_tree` events, but it has gaps: no selective rollback, no API for querying modified files, no per-message diff, and a preview-mode bug where results are discarded.
|
|
6
|
+
|
|
7
|
+
This document designs a unified rollback architecture that:
|
|
8
|
+
|
|
9
|
+
1. Supports **selective rollback**: message-only or both (messages + files)
|
|
10
|
+
2. Exposes a **get modified files** API for the entire session (or any range)
|
|
11
|
+
3. Exposes a **per-message file diff** API (before/after snapshot diff)
|
|
12
|
+
4. Fixes the preview-mode bug
|
|
13
|
+
5. Integrates snapshot logic into core for better testability and API surface
|
|
14
|
+
6. Maintains append-only semantics — `session.jsonl` is never mutated
|
|
15
|
+
|
|
16
|
+
### Design Decisions
|
|
17
|
+
|
|
18
|
+
| Decision | Choice | Rationale |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| Extension vs core | Integrate into core | `InternalGit` already lives in `src/core/file-store/`. Snapshot logic is fundamental to session integrity. Bugs in extension code (preview void) would be caught by core tests. Selective rollback needs deep session access. |
|
|
21
|
+
| Extension after integration | Thin adapter | Extension stays as a hook-registration shim (registers `session_start`, `turn_end`, `session_tree` handlers that delegate to `FileSnapshotManager`). |
|
|
22
|
+
| Entry format | No new entry types | Continue using `step-snapshot` custom entries. Unrevert-point entries stay as-is. |
|
|
23
|
+
| Storage format | No change | FNV-1a content-addressable objects under `~/.pi/agent/file-store/<projectHash>/objects/`. |
|
|
24
|
+
| Rollback modes | 2 modes: "message" and "both" | "code"-only mode removed. Users can edit files directly if they only want file changes. |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. Current Architecture
|
|
29
|
+
|
|
30
|
+
### 2.1 Components
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
InternalGit (src/core/file-store/internal-git.ts)
|
|
34
|
+
- Content-addressable object store (FNV-1a hashing)
|
|
35
|
+
- Tree snapshots: writeTree(), readTree()
|
|
36
|
+
- Diff computation: computeDiff(), diffTrees()
|
|
37
|
+
- Working directory scanning: scanWorkingDir()
|
|
38
|
+
- Ignores .git, node_modules, .pi, etc. via `ignore` package
|
|
39
|
+
|
|
40
|
+
file-snapshot extension (extensions/file-snapshot/index.ts)
|
|
41
|
+
- Duplicate ObjectStore class ( reimplements InternalGit with simpler ignore logic)
|
|
42
|
+
- Hooks: session_start, turn_end, session_tree
|
|
43
|
+
- Creates step-snapshot custom entries per turn
|
|
44
|
+
- Restores files on session_tree event
|
|
45
|
+
- Creates unrevert-point entries before restoration
|
|
46
|
+
- BUG: preview mode discards result via `void { ... }`
|
|
47
|
+
|
|
48
|
+
AgentSession.navigateTree() (src/core/agent-session.ts)
|
|
49
|
+
- Moves leaf pointer via sessionManager.branch()
|
|
50
|
+
- Emits session_before_tree → session_tree events
|
|
51
|
+
- skipFiles option passes through to session_tree event
|
|
52
|
+
- previewRollback() emits session_tree with preview: true
|
|
53
|
+
- BUG: previewRollback() doesn't get result back from extension
|
|
54
|
+
|
|
55
|
+
SessionTreeEvent (src/core/extensions/types.ts)
|
|
56
|
+
- { type, newLeafId, oldLeafId, summaryEntry?, skipFiles?, preview? }
|
|
57
|
+
- No rollbackMode field
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2.2 Data Flow (current)
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
session_start
|
|
64
|
+
→ ObjectStore.scanWorkingDir() → sessionStartTreeHash
|
|
65
|
+
|
|
66
|
+
turn_end
|
|
67
|
+
→ ObjectStore.scanWorkingDir() → snapshotTreeHash
|
|
68
|
+
→ computeTreeDiff(lastCommittedTreeHash, snapshotTreeHash)
|
|
69
|
+
→ if hasChanges: pi.appendEntry("step-snapshot", { baselineTreeHash, snapshotTreeHash, diff, turnIndex })
|
|
70
|
+
|
|
71
|
+
navigateTree (user triggers /tree)
|
|
72
|
+
→ AgentSession.navigateTree(targetId, { skipFiles? })
|
|
73
|
+
→ sessionManager.branch(targetId)
|
|
74
|
+
→ emit session_tree event
|
|
75
|
+
→ extension reads step-snapshot entries on path
|
|
76
|
+
→ finds target tree hash vs current tree hash
|
|
77
|
+
→ if !skipFiles: restoreFiles() + deleteFiles()
|
|
78
|
+
→ appends unrevert-point entry
|
|
79
|
+
|
|
80
|
+
previewRollback
|
|
81
|
+
→ emit session_tree with preview: true
|
|
82
|
+
→ extension: void { restored, deleted } ← BUG: discards result
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 2.3 Known Issues
|
|
86
|
+
|
|
87
|
+
| Issue | Location | Description |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| Duplicate storage logic | `extensions/file-snapshot/index.ts:101-234` | `ObjectStore` duplicates `InternalGit` with simpler ignore patterns (no `ignore` package, custom `matchGlob`). |
|
|
90
|
+
| Preview bug | `extensions/file-snapshot/index.ts:356-359` | `void { restored, deleted }` discards result instead of returning it. |
|
|
91
|
+
| No selective restore | `extensions/file-snapshot/index.ts:309-372` | `session_tree` handler restores ALL files or none (via `skipFiles`). No partial restore. |
|
|
92
|
+
| skipFiles not user-facing | `src/core/agent-session.ts:3178` | `skipFiles` exists but is not exposed via RPC or TUI. |
|
|
93
|
+
| No modified-files query | — | No API to list files changed between any two points. |
|
|
94
|
+
| No per-file diff | — | No API to get unified diff for a specific file between snapshots. |
|
|
95
|
+
| 1MB file limit only in extension | `extensions/file-snapshot/index.ts:163` | Extension skips files > 1MB but `InternalGit` has no such limit. Inconsistent behavior. |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 3. Target Architecture
|
|
100
|
+
|
|
101
|
+
### 3.1 Component Diagram
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
AgentSession
|
|
105
|
+
│
|
|
106
|
+
├── SessionManager (session.jsonl tree, entry CRUD)
|
|
107
|
+
│
|
|
108
|
+
├── FileSnapshotManager (NEW — src/core/file-store/file-snapshot-manager.ts)
|
|
109
|
+
│ │
|
|
110
|
+
│ ├── InternalGit (existing — content-addressable storage)
|
|
111
|
+
│ │
|
|
112
|
+
│ ├── snapshotWorkingDir() — scan + store tree, return hash
|
|
113
|
+
│ ├── getSnapshotAtTurn(n) — query snapshot by turn index
|
|
114
|
+
│ ├── getSnapshotAtEntry(id) — query snapshot by entry ID
|
|
115
|
+
│ ├── getModifiedFiles(opts) — list files changed between two points
|
|
116
|
+
│ ├── getFileDiff(opts) — unified diff for a specific file
|
|
117
|
+
│ ├── restoreFiles(opts) — selective file restoration
|
|
118
|
+
│ │ opts: { targetEntryId?, snapshotHash?, files?: string[], preview? }
|
|
119
|
+
│ └── buildSnapshotIndex() — rebuild in-memory index from entries
|
|
120
|
+
│
|
|
121
|
+
├── navigateTree(targetId, opts) — ENHANCED
|
|
122
|
+
│ opts.skipFiles: boolean (default: false)
|
|
123
|
+
│
|
|
124
|
+
└── ExtensionRunner
|
|
125
|
+
└── file-snapshot extension (THIN adapter)
|
|
126
|
+
delegates to FileSnapshotManager
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 3.2 FileSnapshotManager
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
class FileSnapshotManager {
|
|
133
|
+
private git: InternalGit;
|
|
134
|
+
private sessionStartTreeHash: string | null;
|
|
135
|
+
private lastCommittedTreeHash: string | null;
|
|
136
|
+
private turnIndex: number;
|
|
137
|
+
private snapshotIndex: Map<string, StepSnapshotData>; // entryId → snapshot
|
|
138
|
+
private turnIndexMap: Map<number, string>; // turnIndex → entryId
|
|
139
|
+
|
|
140
|
+
constructor(git: InternalGit);
|
|
141
|
+
|
|
142
|
+
// Called on session_start
|
|
143
|
+
initialize(cwd: string): Promise<void>;
|
|
144
|
+
|
|
145
|
+
// Called on turn_end
|
|
146
|
+
onTurnEnd(cwd: string, appendEntry: (data: StepSnapshotData) => void): void;
|
|
147
|
+
|
|
148
|
+
// Called on session reload to rebuild index from custom entries
|
|
149
|
+
rebuildIndex(entries: SessionEntry[]): void;
|
|
150
|
+
|
|
151
|
+
// Query
|
|
152
|
+
getSnapshotAtTurn(turnIndex: number): StepSnapshotData | null;
|
|
153
|
+
getSnapshotAtEntry(entryId: string): StepSnapshotData | null;
|
|
154
|
+
getLatestSnapshotOnPath(entries: SessionEntry[], leafId: string | null): StepSnapshotData | null;
|
|
155
|
+
|
|
156
|
+
// Modified files API
|
|
157
|
+
getModifiedFiles(options?: {
|
|
158
|
+
fromEntryId?: string;
|
|
159
|
+
toEntryId?: string;
|
|
160
|
+
}): ModifiedFileInfo[];
|
|
161
|
+
|
|
162
|
+
// Per-file diff API
|
|
163
|
+
getFileDiff(options: {
|
|
164
|
+
filePath: string;
|
|
165
|
+
fromEntryId?: string;
|
|
166
|
+
toEntryId?: string;
|
|
167
|
+
}): FileDiffInfo | null;
|
|
168
|
+
|
|
169
|
+
// File restoration
|
|
170
|
+
restoreFiles(cwd: string, options: {
|
|
171
|
+
targetEntryId?: string;
|
|
172
|
+
snapshotHash?: string;
|
|
173
|
+
files?: string[];
|
|
174
|
+
preview?: boolean;
|
|
175
|
+
currentLeafId?: string | null;
|
|
176
|
+
entries: SessionEntry[];
|
|
177
|
+
appendEntry: (type: string, data: unknown) => void;
|
|
178
|
+
}): Promise<RestoreResult>;
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 3.3 Rollback Behavior
|
|
183
|
+
|
|
184
|
+
`navigateTree` uses a simple `skipFiles: boolean` option:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
┌─────────────┬──────────────────────┬──────────────────────┐
|
|
188
|
+
│ skipFiles │ Move leaf pointer │ Restore files │
|
|
189
|
+
├─────────────┼──────────────────────┼──────────────────────┤
|
|
190
|
+
│ false │ Yes │ Yes │
|
|
191
|
+
│ true │ Yes │ No │
|
|
192
|
+
└─────────────┴──────────────────────┴──────────────────────┘
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- `skipFiles: false` (default) — move leaf + restore file snapshot. This is the existing behavior.
|
|
196
|
+
- `skipFiles: true` — move leaf only, skip file restoration. Useful when the user wants to rewind the conversation but keep current files on disk.
|
|
197
|
+
|
|
198
|
+
There is no "code"-only mode. Users who want to restore files without moving the conversation can edit files directly.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 4. API Design
|
|
203
|
+
|
|
204
|
+
### 4.1 ModifiedFileInfo
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
interface ModifiedFileInfo {
|
|
208
|
+
path: string;
|
|
209
|
+
status: "added" | "modified" | "deleted";
|
|
210
|
+
turnIndex: number;
|
|
211
|
+
entryId: string; // step-snapshot entry that recorded this change
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 4.2 FileDiffInfo
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
interface FileDiffInfo {
|
|
219
|
+
path: string;
|
|
220
|
+
oldContent: string | null; // null if file didn't exist at fromSnapshot
|
|
221
|
+
newContent: string | null; // null if file was deleted at toSnapshot
|
|
222
|
+
oldHash: string | null;
|
|
223
|
+
newHash: string | null;
|
|
224
|
+
unifiedDiff: string; // standard unified diff format
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 4.3 RestoreResult
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
interface RestoreResult {
|
|
232
|
+
restored: string[]; // files written back
|
|
233
|
+
deleted: string[]; // files removed
|
|
234
|
+
skipped: string[]; // files that were dirty (externally modified) and skipped
|
|
235
|
+
dirty: string[]; // files that differed from expected current state
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 4.4 StepSnapshotData (entry data format, unchanged)
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
interface StepSnapshotData {
|
|
243
|
+
baselineTreeHash: string | null;
|
|
244
|
+
snapshotTreeHash: string;
|
|
245
|
+
diff: {
|
|
246
|
+
added: string[];
|
|
247
|
+
modified: string[];
|
|
248
|
+
deleted: string[];
|
|
249
|
+
} | null;
|
|
250
|
+
turnIndex: number;
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### 4.5 FileSnapshotManager Methods
|
|
255
|
+
|
|
256
|
+
#### `getModifiedFiles(options?)`
|
|
257
|
+
|
|
258
|
+
Lists all files that changed between two snapshots. Defaults to full session range (session start → current leaf).
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
getModifiedFiles(options?: {
|
|
262
|
+
fromEntryId?: string; // default: session start (no snapshot)
|
|
263
|
+
toEntryId?: string; // default: latest snapshot on current leaf path
|
|
264
|
+
}): ModifiedFileInfo[]
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Algorithm:
|
|
268
|
+
1. Resolve `fromEntryId` to a snapshot (null → session start tree).
|
|
269
|
+
2. Resolve `toEntryId` to a snapshot (null → latest on leaf path).
|
|
270
|
+
3. Use `git.diffTrees()` or walk step-snapshot entries on the path, accumulating changes.
|
|
271
|
+
4. Return aggregated list with earliest turnIndex for each file.
|
|
272
|
+
|
|
273
|
+
#### `getFileDiff(options)`
|
|
274
|
+
|
|
275
|
+
Returns before/after content and unified diff for a single file.
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
getFileDiff(options: {
|
|
279
|
+
filePath: string;
|
|
280
|
+
fromEntryId?: string; // default: session start
|
|
281
|
+
toEntryId?: string; // default: current leaf
|
|
282
|
+
}): FileDiffInfo | null
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Algorithm:
|
|
286
|
+
1. Resolve from/to snapshots.
|
|
287
|
+
2. Read both trees via `git.readTree()`.
|
|
288
|
+
3. Extract file content from each.
|
|
289
|
+
4. Generate unified diff (use `diffLines` from `diff` package or simple line-based diff).
|
|
290
|
+
5. Return `null` if file exists in neither snapshot.
|
|
291
|
+
|
|
292
|
+
#### `restoreFiles(cwd, options)`
|
|
293
|
+
|
|
294
|
+
Restores files to a target snapshot state. Supports selective file list and preview mode.
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
restoreFiles(cwd: string, options: {
|
|
298
|
+
targetEntryId?: string; // resolve to snapshot hash
|
|
299
|
+
snapshotHash?: string; // or provide hash directly
|
|
300
|
+
files?: string[]; // if provided, only restore these files
|
|
301
|
+
preview?: boolean; // if true, return what would happen without writing
|
|
302
|
+
currentLeafId?: string | null;
|
|
303
|
+
entries: SessionEntry[];
|
|
304
|
+
appendEntry: (type: string, data: unknown) => void;
|
|
305
|
+
}): Promise<RestoreResult>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Algorithm:
|
|
309
|
+
1. Resolve target snapshot hash from `targetEntryId` or use `snapshotHash` directly.
|
|
310
|
+
2. Resolve current snapshot hash from latest snapshot on `currentLeafId` path.
|
|
311
|
+
3. Read both trees.
|
|
312
|
+
4. Compute diff: files to restore (content differs) and files to delete (in current but not in target).
|
|
313
|
+
5. If `files` option provided, filter to only those paths.
|
|
314
|
+
6. **Conflict detection**: for each file to restore, read disk content, hash it, compare to current tree hash. If different, mark as `dirty`.
|
|
315
|
+
7. If `preview`: return result without writing.
|
|
316
|
+
8. If not preview: scan working dir, write unrevert-point entry, then write/delete files.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## 5. RPC Commands
|
|
321
|
+
|
|
322
|
+
### 5.1 New Commands
|
|
323
|
+
|
|
324
|
+
#### `get_modified_files`
|
|
325
|
+
|
|
326
|
+
```json
|
|
327
|
+
{
|
|
328
|
+
"id": "req-1",
|
|
329
|
+
"type": "get_modified_files",
|
|
330
|
+
"fromEntryId": "abc123",
|
|
331
|
+
"toEntryId": "def456"
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Both fields optional. Defaults: `fromEntryId` = session start, `toEntryId` = current leaf.
|
|
336
|
+
|
|
337
|
+
Response:
|
|
338
|
+
```json
|
|
339
|
+
{
|
|
340
|
+
"id": "req-1",
|
|
341
|
+
"type": "response",
|
|
342
|
+
"command": "get_modified_files",
|
|
343
|
+
"success": true,
|
|
344
|
+
"data": {
|
|
345
|
+
"files": [
|
|
346
|
+
{ "path": "src/foo.ts", "status": "modified", "turnIndex": 2, "entryId": "snap123" },
|
|
347
|
+
{ "path": "src/bar.ts", "status": "added", "turnIndex": 3, "entryId": "snap456" }
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### `get_file_diff`
|
|
354
|
+
|
|
355
|
+
```json
|
|
356
|
+
{
|
|
357
|
+
"id": "req-2",
|
|
358
|
+
"type": "get_file_diff",
|
|
359
|
+
"filePath": "src/foo.ts",
|
|
360
|
+
"fromEntryId": "abc123",
|
|
361
|
+
"toEntryId": "def456"
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
`fromEntryId` and `toEntryId` optional with same defaults as above.
|
|
366
|
+
|
|
367
|
+
Response:
|
|
368
|
+
```json
|
|
369
|
+
{
|
|
370
|
+
"id": "req-2",
|
|
371
|
+
"type": "response",
|
|
372
|
+
"command": "get_file_diff",
|
|
373
|
+
"success": true,
|
|
374
|
+
"data": {
|
|
375
|
+
"path": "src/foo.ts",
|
|
376
|
+
"oldContent": "original content\n",
|
|
377
|
+
"newContent": "modified content\n",
|
|
378
|
+
"oldHash": "a1b2c3d4",
|
|
379
|
+
"newHash": "e5f6g7h8",
|
|
380
|
+
"unifiedDiff": "--- src/foo.ts\n+++ src/foo.ts\n@@ -1 +1 @@\n-original content\n+modified content\n"
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Returns `null` data if file doesn't exist in either snapshot.
|
|
386
|
+
|
|
387
|
+
### 5.2 Modified Commands
|
|
388
|
+
|
|
389
|
+
#### `navigate_tree`
|
|
390
|
+
|
|
391
|
+
Current:
|
|
392
|
+
```json
|
|
393
|
+
{
|
|
394
|
+
"type": "navigate_tree",
|
|
395
|
+
"targetId": "abc123",
|
|
396
|
+
"summarize": true,
|
|
397
|
+
"skipFiles": false
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`skipFiles: boolean` (default: `false`) remains the option — no `rollbackMode` field:
|
|
402
|
+
```json
|
|
403
|
+
{
|
|
404
|
+
"type": "navigate_tree",
|
|
405
|
+
"targetId": "abc123",
|
|
406
|
+
"summarize": true,
|
|
407
|
+
"skipFiles": false
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Behavior:
|
|
412
|
+
- `skipFiles: false` (default) — move leaf + restore files (current behavior)
|
|
413
|
+
- `skipFiles: true` — move leaf only, skip file restoration
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## 6. Data Model
|
|
418
|
+
|
|
419
|
+
### 6.1 Entry Types (no changes)
|
|
420
|
+
|
|
421
|
+
No new entry types. Existing entries used by the file snapshot system:
|
|
422
|
+
|
|
423
|
+
| Entry Type | customType | Purpose |
|
|
424
|
+
|---|---|---|
|
|
425
|
+
| `custom` | `step-snapshot` | Per-turn tree snapshot + diff |
|
|
426
|
+
| `custom` | `unrevert-point` | Pre-rollback state for undo |
|
|
427
|
+
|
|
428
|
+
### 6.2 step-snapshot Entry
|
|
429
|
+
|
|
430
|
+
Stored as a `custom` entry with `customType: "step-snapshot"`:
|
|
431
|
+
|
|
432
|
+
```json
|
|
433
|
+
{
|
|
434
|
+
"type": "custom",
|
|
435
|
+
"id": "snap1234",
|
|
436
|
+
"parentId": "prev1234",
|
|
437
|
+
"timestamp": "2025-05-07T10:00:00.000Z",
|
|
438
|
+
"customType": "step-snapshot",
|
|
439
|
+
"data": {
|
|
440
|
+
"baselineTreeHash": "a1b2c3d4",
|
|
441
|
+
"snapshotTreeHash": "e5f6g7h8",
|
|
442
|
+
"diff": {
|
|
443
|
+
"added": ["src/new-file.ts"],
|
|
444
|
+
"modified": ["src/existing.ts"],
|
|
445
|
+
"deleted": ["src/old-file.ts"]
|
|
446
|
+
},
|
|
447
|
+
"turnIndex": 3
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
- `baselineTreeHash`: the tree hash this snapshot was compared against (previous snapshot or session start). Null if this is the first snapshot.
|
|
453
|
+
- `snapshotTreeHash`: the tree hash of the working directory at this turn.
|
|
454
|
+
- `diff`: delta from baseline to this snapshot. Null if no changes detected.
|
|
455
|
+
- `turnIndex`: 0-based turn counter.
|
|
456
|
+
|
|
457
|
+
### 6.3 unrevert-point Entry
|
|
458
|
+
|
|
459
|
+
Stored as a `custom` entry with `customType: "unrevert-point"`:
|
|
460
|
+
|
|
461
|
+
```json
|
|
462
|
+
{
|
|
463
|
+
"type": "custom",
|
|
464
|
+
"id": "urv1234",
|
|
465
|
+
"parentId": "snap5678",
|
|
466
|
+
"timestamp": "2025-05-07T10:05:00.000Z",
|
|
467
|
+
"customType": "unrevert-point",
|
|
468
|
+
"data": {
|
|
469
|
+
"preRollbackTreeHash": "c3d4e5f6",
|
|
470
|
+
"rolledBackToLeaf": "abc123",
|
|
471
|
+
"restoredFiles": ["src/foo.ts", "src/bar.ts"]
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### 6.4 BranchSummaryEntry Extension
|
|
477
|
+
|
|
478
|
+
The `BranchSummaryEntry` gains an optional `skipFiles` field to record whether file restoration was skipped at this position:
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
interface BranchSummaryEntry extends SessionEntryBase {
|
|
482
|
+
type: "summary";
|
|
483
|
+
summary: string;
|
|
484
|
+
skipFiles?: boolean; // true if file restoration was skipped during this navigation
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
This field is set when `branchWithSummary()` is called with `skipFiles: true`. Future operations can inspect the entry to determine whether files were restored at that point.
|
|
489
|
+
|
|
490
|
+
For `branch()` calls without a summary, `skipFiles` is not persisted — it's an operational parameter for that invocation only. This is acceptable because the user decides each time they navigate.
|
|
491
|
+
|
|
492
|
+
### 6.5 Object Store Layout
|
|
493
|
+
|
|
494
|
+
```
|
|
495
|
+
~/.pi/agent/file-store/<projectHash>/
|
|
496
|
+
├── objects/
|
|
497
|
+
│ ├── a1/
|
|
498
|
+
│ │ └── b2c3d4e5 # file content or tree data, keyed by FNV-1a hash
|
|
499
|
+
│ ├── e5/
|
|
500
|
+
│ │ └── f6g7h8i9
|
|
501
|
+
│ └── ...
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
- Blob objects: raw file content.
|
|
505
|
+
- Tree objects: `\n`-separated lines of `<path>\0<hash>`, sorted by path.
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## 7. Migration Plan
|
|
510
|
+
|
|
511
|
+
### 7.1 Phase 1: Create FileSnapshotManager (non-breaking)
|
|
512
|
+
|
|
513
|
+
1. Create `src/core/file-store/file-snapshot-manager.ts`.
|
|
514
|
+
2. Move snapshot logic from `extensions/file-snapshot/index.ts` into `FileSnapshotManager`:
|
|
515
|
+
- `scanWorkingDir` → use `InternalGit.scanWorkingDir()` (eliminate duplicate).
|
|
516
|
+
- `writeTree`, `readTree`, `computeTreeDiff` → delegate to `InternalGit`.
|
|
517
|
+
- `session_start` handler → `initialize()`.
|
|
518
|
+
- `turn_end` handler → `onTurnEnd()`.
|
|
519
|
+
- `session_tree` handler → `restoreFiles()`.
|
|
520
|
+
- `findLatestSnapshotOnPath` → `getLatestSnapshotOnPath()`.
|
|
521
|
+
3. Fix preview bug: `restoreFiles()` returns `RestoreResult` in all code paths.
|
|
522
|
+
4. Port the `findCanonicalGitRoot()` and `shouldIgnore()` logic into core.
|
|
523
|
+
5. Harmonize file size limit: add 1MB cap to `InternalGit.scanWorkingDir()` (or make it configurable).
|
|
524
|
+
|
|
525
|
+
### 7.2 Phase 2: Add new APIs to FileSnapshotManager
|
|
526
|
+
|
|
527
|
+
1. Implement `getModifiedFiles()`.
|
|
528
|
+
2. Implement `getFileDiff()`.
|
|
529
|
+
3. Implement `buildSnapshotIndex()` for session reload.
|
|
530
|
+
4. Wire `FileSnapshotManager` into `AgentSession` (lazy init on `session_start`).
|
|
531
|
+
|
|
532
|
+
### 7.3 Phase 3: Enhance navigateTree
|
|
533
|
+
|
|
534
|
+
1. Keep `skipFiles: boolean` as the option on `navigateTree()` (no `rollbackMode`).
|
|
535
|
+
2. When `skipFiles=false`: current behavior — move leaf + restore files via file-snapshot.
|
|
536
|
+
3. When `skipFiles=true`: move leaf only, don't restore files.
|
|
537
|
+
4. Store `skipFiles` on the created `BranchSummaryEntry` if `summarize=true`.
|
|
538
|
+
5. If `summarize=false` and `skipFiles=true`, the flag is not persisted (operational only).
|
|
539
|
+
6. `SessionTreeEvent` passes `skipFiles` through (no `rollbackMode` field).
|
|
540
|
+
|
|
541
|
+
### 7.4 Phase 4: Add RPC commands
|
|
542
|
+
|
|
543
|
+
1. Add `get_modified_files` command to `rpc-types.ts` and `rpc-mode.ts`.
|
|
544
|
+
2. Add `get_file_diff` command.
|
|
545
|
+
3. No `restore_files` command — users edit files directly.
|
|
546
|
+
4. Keep `navigate_tree` command with `skipFiles: boolean` option.
|
|
547
|
+
|
|
548
|
+
### 7.5 Phase 5: Thin the extension
|
|
549
|
+
|
|
550
|
+
1. Update `extensions/file-snapshot/index.ts` to delegate to `AgentSession.fileSnapshotManager` instead of its own `ObjectStore`.
|
|
551
|
+
2. Extension becomes ~50 lines: register `session_start`/`turn_end`/`session_tree` hooks that call `ctx.sessionManager.fileSnapshotManager.*()`.
|
|
552
|
+
3. Extension can be disabled without losing core functionality (FileSnapshotManager always present).
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## 8. Testing Strategy
|
|
557
|
+
|
|
558
|
+
### 8.1 Unit Tests — FileSnapshotManager
|
|
559
|
+
|
|
560
|
+
File: `test/file-store/file-snapshot-manager.test.ts`
|
|
561
|
+
|
|
562
|
+
| Test | Description |
|
|
563
|
+
|---|---|
|
|
564
|
+
| initializes with working dir snapshot | `initialize()` creates session-start tree hash |
|
|
565
|
+
| skips snapshot when no changes | `onTurnEnd()` with unchanged dir doesn't append entry |
|
|
566
|
+
| creates snapshot on file change | `onTurnEnd()` after file write produces step-snapshot data |
|
|
567
|
+
| creates snapshot on file add | New file detected in diff |
|
|
568
|
+
| creates snapshot on file delete | Removed file detected in diff |
|
|
569
|
+
| gets snapshot at turn index | `getSnapshotAtTurn(0)` returns first snapshot |
|
|
570
|
+
| gets snapshot at entry ID | `getSnapshotAtEntry(id)` returns correct snapshot |
|
|
571
|
+
| rebuilds index from entries | `rebuildIndex()` restores snapshot map from custom entries |
|
|
572
|
+
| handles empty session | No snapshots, no crashes |
|
|
573
|
+
|
|
574
|
+
### 8.2 Unit Tests — getModifiedFiles
|
|
575
|
+
|
|
576
|
+
| Test | Description |
|
|
577
|
+
|---|---|
|
|
578
|
+
| returns all changes for session | Full range from start to current |
|
|
579
|
+
| returns changes between two entries | Scoped range |
|
|
580
|
+
| returns empty for no changes | Identical snapshots |
|
|
581
|
+
| aggregates across multiple turns | Multiple snapshots consolidated |
|
|
582
|
+
| tracks per-file earliest change | Status reflects first occurrence |
|
|
583
|
+
|
|
584
|
+
### 8.3 Unit Tests — getFileDiff
|
|
585
|
+
|
|
586
|
+
| Test | Description |
|
|
587
|
+
|---|---|
|
|
588
|
+
| returns diff for modified file | oldContent vs newContent with unified diff |
|
|
589
|
+
| returns diff for added file | oldContent = null |
|
|
590
|
+
| returns diff for deleted file | newContent = null |
|
|
591
|
+
| returns null for non-existent file | File not in either snapshot |
|
|
592
|
+
| handles default range | Session start to current |
|
|
593
|
+
|
|
594
|
+
### 8.4 Unit Tests — restoreFiles
|
|
595
|
+
|
|
596
|
+
| Test | Description |
|
|
597
|
+
|---|---|
|
|
598
|
+
| restores modified files | Content written to disk |
|
|
599
|
+
| deletes files not in target | Files removed from disk |
|
|
600
|
+
| preview mode returns plan without writing | Disk unchanged |
|
|
601
|
+
| selective restore with files filter | Only specified files restored |
|
|
602
|
+
| conflict detection marks dirty files | Externally modified files in `dirty` list |
|
|
603
|
+
| appends unrevert-point entry | Pre-rollback state recorded |
|
|
604
|
+
| no-op when snapshots identical | No writes, empty result |
|
|
605
|
+
|
|
606
|
+
### 8.5 Integration Tests — navigateTree with skipFiles
|
|
607
|
+
|
|
608
|
+
File: `test/suite/navigate-tree-rollback.test.ts`
|
|
609
|
+
|
|
610
|
+
| Test | Description |
|
|
611
|
+
|---|---|
|
|
612
|
+
| skipFiles=false restores files and moves leaf | Default behavior preserved |
|
|
613
|
+
| skipFiles=true moves leaf without restoring files | Files unchanged on disk |
|
|
614
|
+
| skipFiles=true with summarize stores flag on BranchSummaryEntry | Entry has `skipFiles: true` |
|
|
615
|
+
| skipFiles=true without summarize does not persist flag | Operational only |
|
|
616
|
+
| summarize with skipFiles=false does not set flag | BranchSummaryEntry has no `skipFiles` field |
|
|
617
|
+
|
|
618
|
+
### 8.6 Integration Tests — RPC Commands
|
|
619
|
+
|
|
620
|
+
File: `test/suite/rpc-rollback.test.ts`
|
|
621
|
+
|
|
622
|
+
| Test | Description |
|
|
623
|
+
|---|---|
|
|
624
|
+
| get_modified_files returns correct list | RPC round-trip |
|
|
625
|
+
| get_file_diff returns unified diff | RPC round-trip |
|
|
626
|
+
| navigate_tree with skipFiles | RPC round-trip |
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## 9. Risks and Mitigations
|
|
631
|
+
|
|
632
|
+
| Risk | Impact | Likelihood | Mitigation |
|
|
633
|
+
|---|---|---|---|
|
|
634
|
+
| FNV-1a hash collisions | Data corruption (wrong file content) | Low (32-bit hash, ~4B values) | Hash is only used as disk key. Collision means overwriting an object with same-named content. Acceptable for snapshots, not for security. Can upgrade to SHA-256 later if needed. |
|
|
635
|
+
| Large project scan time | Slow `turn_end` handler, blocking agent | Medium | `InternalGit.scanWorkingDir()` reads all files. Mitigate with: (1) 1MB file cap, (2) `skipFiles` patterns, (3) consider incremental scanning in future. |
|
|
636
|
+
| Disk space for object store | Object accumulation over many sessions | Low | Objects are deduplicated by hash. Add periodic garbage collection of orphaned objects (objects not referenced by any session's snapshots). |
|
|
637
|
+
| Race condition: external file change during restore | Inconsistent restore | Low | Scan working dir immediately before restore (already done for unrevert-point). Dirty detection catches this. |
|
|
638
|
+
| Breaking change to extension API | Existing file-snapshot extensions break | Medium | Phase migration: Phase 1 is non-breaking (new core class, extension unchanged). Phase 5 thins extension but old extension still works if present. |
|
|
639
|
+
| Preview mode was broken, clients may not expect it to work | Wrong assumptions about preview reliability | Low | Preview is currently broken (void). Fixing it is a pure improvement. |
|
|
640
|
+
| skipFiles=true leaves conversation behind files | User rewinds messages but files stay at later state | Low | This is intentional behavior — the user explicitly chose to keep files. UI should clearly indicate when file restoration was skipped. |
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## 10. Implementation Order
|
|
645
|
+
|
|
646
|
+
### Phase 1: Core foundation (1-2 days)
|
|
647
|
+
|
|
648
|
+
1. Create `FileSnapshotManager` class in `src/core/file-store/`.
|
|
649
|
+
2. Port logic from extension. Use `InternalGit` directly (no duplicate `ObjectStore`).
|
|
650
|
+
3. Fix preview bug.
|
|
651
|
+
4. Add 1MB file size limit to `InternalGit.scanWorkingDir()`.
|
|
652
|
+
5. Unit tests for `FileSnapshotManager` (initialize, onTurnEnd, restoreFiles basic).
|
|
653
|
+
6. `npm run check` passes.
|
|
654
|
+
|
|
655
|
+
### Phase 2: Query APIs (1 day)
|
|
656
|
+
|
|
657
|
+
1. Implement `getModifiedFiles()`.
|
|
658
|
+
2. Implement `getFileDiff()`.
|
|
659
|
+
3. Add unified diff generation (use existing `diff` package or implement simple line diff).
|
|
660
|
+
4. Unit tests for both APIs.
|
|
661
|
+
5. `npm run check` passes.
|
|
662
|
+
|
|
663
|
+
### Phase 3: skipFiles behavior (1 day)
|
|
664
|
+
|
|
665
|
+
1. Keep `skipFiles: boolean` on `navigateTree()` options.
|
|
666
|
+
2. `SessionTreeEvent` passes `skipFiles` through (no `rollbackMode`).
|
|
667
|
+
3. When `skipFiles=true`: move leaf only, don't restore files.
|
|
668
|
+
4. Add `skipFiles?: boolean` field to `BranchSummaryEntry`.
|
|
669
|
+
5. Store `skipFiles` on `BranchSummaryEntry` when `summarize=true` and `skipFiles=true`.
|
|
670
|
+
6. Integration tests for both modes (skipFiles=false, skipFiles=true).
|
|
671
|
+
7. `npm run check` passes.
|
|
672
|
+
|
|
673
|
+
### Phase 4: RPC surface (1 day)
|
|
674
|
+
|
|
675
|
+
1. Add `get_modified_files` and `get_file_diff` to `rpc-types.ts`.
|
|
676
|
+
2. Implement handlers in `rpc-mode.ts`.
|
|
677
|
+
3. No `restore_files` command.
|
|
678
|
+
4. RPC integration tests.
|
|
679
|
+
5. `npm run check` passes.
|
|
680
|
+
|
|
681
|
+
### Phase 5: Extension thinning (0.5 day)
|
|
682
|
+
|
|
683
|
+
1. Update `file-snapshot` extension to delegate to `ctx.sessionManager.fileSnapshotManager`.
|
|
684
|
+
2. Verify extension tests still pass.
|
|
685
|
+
3. Update `docs/extensions.md` with new architecture note.
|
|
686
|
+
4. `npm run check` passes.
|
|
687
|
+
|
|
688
|
+
### Phase 6: Documentation and polish (0.5 day)
|
|
689
|
+
|
|
690
|
+
1. Update `docs/rpc.md` with new commands.
|
|
691
|
+
2. Update `docs/tree.md` with skipFiles behavior.
|
|
692
|
+
3. Update `docs/session.md` with FileSnapshotManager section.
|
|
693
|
+
4. Final `npm run check`.
|