@armadra/agent 0.4.0 → 0.5.0
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 +174 -1
- package/README.md +201 -82
- package/THIRD_PARTY_NOTICES.md +34 -0
- package/dist/acp.d.ts +18 -0
- package/dist/acp.js +19 -0
- package/dist/agent/limits.d.ts +38 -0
- package/dist/agent/limits.js +127 -0
- package/dist/agent/loop-guard.d.ts +38 -0
- package/dist/agent/loop-guard.js +95 -0
- package/dist/agent/loop.d.ts +4 -0
- package/dist/agent/loop.js +7 -0
- package/dist/agent/prompt-rules.d.ts +12 -0
- package/dist/agent/prompt-rules.js +18 -0
- package/dist/agent/reminders.d.ts +59 -0
- package/dist/agent/reminders.js +327 -0
- package/dist/agent/session-cache.d.ts +5 -0
- package/dist/agent/session-cache.js +17 -1
- package/dist/agent/session-classifier.d.ts +1 -1
- package/dist/agent/session-classifier.js +3 -1
- package/dist/agent/session-compaction.d.ts +35 -6
- package/dist/agent/session-compaction.js +157 -31
- package/dist/agent/session-core.d.ts +27 -1
- package/dist/agent/session-extensions.d.ts +61 -0
- package/dist/agent/session-extensions.js +129 -0
- package/dist/agent/session-images.d.ts +23 -0
- package/dist/agent/session-images.js +97 -0
- package/dist/agent/session-plan.d.ts +21 -0
- package/dist/agent/session-plan.js +524 -0
- package/dist/agent/session-rewind.d.ts +81 -0
- package/dist/agent/session-rewind.js +354 -0
- package/dist/agent/session-run.d.ts +28 -1
- package/dist/agent/session-run.js +157 -6
- package/dist/agent/session-settings.d.ts +49 -0
- package/dist/agent/session-settings.js +95 -0
- package/dist/agent/session-subagent.d.ts +36 -24
- package/dist/agent/session-subagent.js +259 -129
- package/dist/agent/session-sync.d.ts +10 -3
- package/dist/agent/session-sync.js +37 -8
- package/dist/agent/session-telemetry.d.ts +39 -0
- package/dist/agent/session-telemetry.js +231 -0
- package/dist/agent/session-tools.js +40 -1
- package/dist/agent/session.d.ts +30 -29
- package/dist/agent/session.js +79 -123
- package/dist/agent/subagent-registry.d.ts +91 -0
- package/dist/agent/subagent-registry.js +494 -0
- package/dist/agent/system-prompt.d.ts +6 -3
- package/dist/agent/system-prompt.js +7 -2
- package/dist/agent/tool-runner.d.ts +8 -1
- package/dist/agent/tool-runner.js +57 -4
- package/dist/agent/types-w5.d.ts +192 -0
- package/dist/agent/types-w5.js +10 -0
- package/dist/agent/types.d.ts +47 -4
- package/dist/agent/types.js +2 -0
- package/dist/agent/worktree.d.ts +36 -0
- package/dist/agent/worktree.js +86 -0
- package/dist/agents/builtin.d.ts +14 -0
- package/dist/agents/builtin.js +39 -0
- package/dist/agents/catalog.d.ts +43 -0
- package/dist/agents/catalog.js +82 -0
- package/dist/agents/discover.d.ts +35 -0
- package/dist/agents/discover.js +83 -0
- package/dist/agents/external.d.ts +82 -0
- package/dist/agents/external.js +326 -0
- package/dist/agents/parse.d.ts +18 -0
- package/dist/agents/parse.js +116 -0
- package/dist/agents/result.d.ts +32 -0
- package/dist/agents/result.js +67 -0
- package/dist/agents/task-control.d.ts +17 -0
- package/dist/agents/task-control.js +17 -0
- package/dist/agents/task-record.d.ts +78 -0
- package/dist/agents/task-record.js +141 -0
- package/dist/agents/types.d.ts +46 -0
- package/dist/agents/types.js +8 -0
- package/dist/ai/apis/anthropic-compat.d.ts +36 -0
- package/dist/ai/apis/anthropic-compat.js +92 -0
- package/dist/ai/apis/anthropic-request.d.ts +7 -6
- package/dist/ai/apis/anthropic-request.js +10 -28
- package/dist/ai/apis/cache-params.d.ts +11 -2
- package/dist/ai/apis/cache-params.js +25 -6
- package/dist/ai/apis/openai-compat.d.ts +4 -1
- package/dist/ai/apis/openai-compat.js +8 -1
- package/dist/ai/image-limits.d.ts +45 -0
- package/dist/ai/image-limits.js +68 -0
- package/dist/ai/providers/builtin.d.ts +18 -6
- package/dist/ai/providers/builtin.js +135 -6
- package/dist/ai/providers/catalog-data.js +17 -13
- package/dist/ai/providers/catalog.d.ts +60 -7
- package/dist/ai/providers/catalog.js +197 -38
- package/dist/ai/providers/channels.d.ts +33 -3
- package/dist/ai/providers/channels.js +108 -7
- package/dist/ai/providers/enrich.d.ts +6 -3
- package/dist/ai/providers/enrich.js +19 -4
- package/dist/ai/providers/models-dev-cache.d.ts +28 -24
- package/dist/ai/providers/models-dev-cache.js +92 -78
- package/dist/ai/providers/models-dev-data.d.ts +7 -0
- package/dist/ai/providers/models-dev-data.js +32 -0
- package/dist/ai/providers/models-dev-snapshot.d.ts +50 -0
- package/dist/ai/providers/models-dev-snapshot.js +137 -0
- package/dist/ai/providers/models-dev.d.ts +51 -10
- package/dist/ai/providers/models-dev.js +132 -39
- package/dist/ai/providers/registry.d.ts +20 -10
- package/dist/ai/providers/registry.js +82 -43
- package/dist/ai/types.d.ts +28 -2
- package/dist/ai/types.js +5 -0
- package/dist/bundle/ama.cjs +36203 -20499
- package/dist/checkpoints/backend.d.ts +61 -0
- package/dist/checkpoints/backend.js +135 -0
- package/dist/checkpoints/blobs.d.ts +60 -0
- package/dist/checkpoints/blobs.js +203 -0
- package/dist/checkpoints/gc.d.ts +47 -0
- package/dist/checkpoints/gc.js +144 -0
- package/dist/checkpoints/git-head.d.ts +15 -0
- package/dist/checkpoints/git-head.js +89 -0
- package/dist/checkpoints/index.d.ts +17 -0
- package/dist/checkpoints/index.js +17 -0
- package/dist/checkpoints/replay.d.ts +48 -0
- package/dist/checkpoints/replay.js +154 -0
- package/dist/checkpoints/restore.d.ts +86 -0
- package/dist/checkpoints/restore.js +263 -0
- package/dist/checkpoints/settings.d.ts +14 -0
- package/dist/checkpoints/settings.js +23 -0
- package/dist/checkpoints/shadow-git.d.ts +108 -0
- package/dist/checkpoints/shadow-git.js +491 -0
- package/dist/checkpoints/shadow-restore.d.ts +25 -0
- package/dist/checkpoints/shadow-restore.js +128 -0
- package/dist/checkpoints/tracker.d.ts +64 -0
- package/dist/checkpoints/tracker.js +192 -0
- package/dist/checkpoints/types.d.ts +99 -0
- package/dist/checkpoints/types.js +12 -0
- package/dist/cli/args.d.ts +8 -4
- package/dist/cli/args.js +17 -97
- package/dist/cli/bootstrap.js +13 -1
- package/dist/cli/codemode-notice.d.ts +3 -2
- package/dist/cli/codemode-notice.js +3 -2
- package/dist/cli/compose-agents.d.ts +27 -0
- package/dist/cli/compose-agents.js +111 -0
- package/dist/cli/compose-extensions.d.ts +29 -0
- package/dist/cli/compose-extensions.js +44 -0
- package/dist/cli/compose-session.d.ts +3 -0
- package/dist/cli/compose-session.js +47 -4
- package/dist/cli/compose.d.ts +3 -0
- package/dist/cli/compose.js +33 -6
- package/dist/cli/deps.d.ts +6 -0
- package/dist/cli/exit-codes.d.ts +10 -0
- package/dist/cli/exit-codes.js +12 -0
- package/dist/cli/help-text.d.ts +4 -0
- package/dist/cli/help-text.js +102 -0
- package/dist/cli/startup-screen.js +5 -3
- package/dist/cli/startup-steps.js +7 -1
- package/dist/cli/subcommands/config.d.ts +4 -1
- package/dist/cli/subcommands/config.js +17 -6
- package/dist/cli/subcommands/doctor.js +21 -0
- package/dist/cli/subcommands/models-discover.js +5 -6
- package/dist/cli/subcommands/models.d.ts +2 -0
- package/dist/cli/subcommands/models.js +33 -14
- package/dist/cli/subcommands/providers.js +5 -6
- package/dist/cli/subcommands/sessions.d.ts +3 -1
- package/dist/cli/subcommands/sessions.js +36 -2
- package/dist/codemode/capability.d.ts +24 -7
- package/dist/codemode/capability.js +38 -10
- package/dist/codemode/host-side.d.ts +14 -0
- package/dist/codemode/host-side.js +24 -1
- package/dist/codemode/tool.d.ts +1 -1
- package/dist/codemode/tool.js +6 -4
- package/dist/compaction/breaker.d.ts +25 -9
- package/dist/compaction/breaker.js +47 -17
- package/dist/compaction/estimate.d.ts +11 -3
- package/dist/compaction/estimate.js +64 -22
- package/dist/compaction/image-budget.d.ts +45 -0
- package/dist/compaction/image-budget.js +115 -0
- package/dist/compaction/post-compact.d.ts +38 -0
- package/dist/compaction/post-compact.js +152 -0
- package/dist/compaction/protect.d.ts +27 -0
- package/dist/compaction/protect.js +56 -0
- package/dist/compaction/prune-tier.d.ts +58 -12
- package/dist/compaction/prune-tier.js +120 -54
- package/dist/compaction/summarize-tier.d.ts +23 -4
- package/dist/compaction/summarize-tier.js +100 -29
- package/dist/config/checker.d.ts +28 -0
- package/dist/config/checker.js +97 -0
- package/dist/config/json-schema.js +63 -2
- package/dist/config/key-docs.js +72 -3
- package/dist/config/merge.d.ts +5 -3
- package/dist/config/merge.js +81 -7
- package/dist/config/profile.d.ts +2 -0
- package/dist/config/profile.js +2 -0
- package/dist/config/schema-w5.d.ts +18 -0
- package/dist/config/schema-w5.js +109 -0
- package/dist/config/schema.d.ts +5 -9
- package/dist/config/schema.js +68 -114
- package/dist/config/types-w5.d.ts +118 -0
- package/dist/config/types-w5.js +20 -0
- package/dist/config/types.d.ts +50 -1
- package/dist/config/types.js +12 -0
- package/dist/drivers/acp/client.d.ts +57 -0
- package/dist/drivers/acp/client.js +160 -0
- package/dist/drivers/acp/driver.d.ts +31 -0
- package/dist/drivers/acp/driver.js +274 -0
- package/dist/drivers/acp/testing/fake-agent-main.d.ts +5 -0
- package/dist/drivers/acp/testing/fake-agent-main.js +7 -0
- package/dist/drivers/acp/testing/fake-agent.d.ts +27 -0
- package/dist/drivers/acp/testing/fake-agent.js +211 -0
- package/dist/drivers/acp/types.d.ts +285 -0
- package/dist/drivers/acp/types.js +36 -0
- package/dist/drivers/agents.d.ts +58 -0
- package/dist/drivers/agents.js +142 -0
- package/dist/drivers/base.d.ts +35 -0
- package/dist/drivers/base.js +59 -0
- package/dist/drivers/catalog.d.ts +46 -0
- package/dist/drivers/catalog.js +167 -0
- package/dist/drivers/env.d.ts +20 -0
- package/dist/drivers/env.js +46 -0
- package/dist/drivers/host-runners.d.ts +18 -0
- package/dist/drivers/host-runners.js +45 -0
- package/dist/drivers/jsonrpc.d.ts +58 -0
- package/dist/drivers/jsonrpc.js +180 -0
- package/dist/drivers/native/claude-normalize.d.ts +46 -0
- package/dist/drivers/native/claude-normalize.js +106 -0
- package/dist/drivers/native/claude-stream.d.ts +29 -0
- package/dist/drivers/native/claude-stream.js +456 -0
- package/dist/drivers/native/codex-app-server.d.ts +26 -0
- package/dist/drivers/native/codex-app-server.js +407 -0
- package/dist/drivers/native/codex-normalize.d.ts +38 -0
- package/dist/drivers/native/codex-normalize.js +124 -0
- package/dist/drivers/native/oneshot.d.ts +28 -0
- package/dist/drivers/native/oneshot.js +312 -0
- package/dist/drivers/permissions.d.ts +40 -0
- package/dist/drivers/permissions.js +92 -0
- package/dist/drivers/pids.d.ts +32 -0
- package/dist/drivers/pids.js +93 -0
- package/dist/drivers/pool.d.ts +28 -0
- package/dist/drivers/pool.js +96 -0
- package/dist/drivers/probe.d.ts +47 -0
- package/dist/drivers/probe.js +172 -0
- package/dist/drivers/process.d.ts +48 -0
- package/dist/drivers/process.js +100 -0
- package/dist/drivers/runner.d.ts +61 -0
- package/dist/drivers/runner.js +388 -0
- package/dist/drivers/store.d.ts +49 -0
- package/dist/drivers/store.js +91 -0
- package/dist/drivers/turn.d.ts +41 -0
- package/dist/drivers/turn.js +99 -0
- package/dist/drivers/types.d.ts +122 -0
- package/dist/drivers/types.js +11 -0
- package/dist/git/info.d.ts +75 -0
- package/dist/git/info.js +191 -0
- package/dist/hooks/protocol.js +6 -1
- package/dist/hooks/types.d.ts +10 -1
- package/dist/hooks/types.js +2 -0
- package/dist/host/api-impl.d.ts +7 -0
- package/dist/host/api-impl.js +15 -0
- package/dist/host/types.d.ts +24 -2
- package/dist/host/types.js +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/modes/acp/acp-events.d.ts +46 -0
- package/dist/modes/acp/acp-events.js +233 -0
- package/dist/modes/acp/acp-mode.d.ts +17 -0
- package/dist/modes/acp/acp-mode.js +47 -0
- package/dist/modes/acp/acp-server.d.ts +79 -0
- package/dist/modes/acp/acp-server.js +315 -0
- package/dist/modes/commands-core.d.ts +7 -1
- package/dist/modes/commands-core.js +55 -0
- package/dist/modes/image-input.d.ts +12 -4
- package/dist/modes/image-input.js +22 -5
- package/dist/modes/interactive/agent-panels.d.ts +14 -0
- package/dist/modes/interactive/agent-panels.js +103 -0
- package/dist/modes/interactive/agent-ui.d.ts +73 -0
- package/dist/modes/interactive/agent-ui.js +234 -0
- package/dist/modes/interactive/approval-dialog.d.ts +21 -4
- package/dist/modes/interactive/approval-dialog.js +98 -20
- package/dist/modes/interactive/approval-merge.d.ts +41 -0
- package/dist/modes/interactive/approval-merge.js +89 -0
- package/dist/modes/interactive/clipboard-paste.d.ts +18 -0
- package/dist/modes/interactive/clipboard-paste.js +28 -0
- package/dist/modes/interactive/commands.d.ts +9 -1
- package/dist/modes/interactive/commands.js +19 -0
- package/dist/modes/interactive/double-esc.d.ts +20 -0
- package/dist/modes/interactive/double-esc.js +34 -0
- package/dist/modes/interactive/event-notices.d.ts +32 -0
- package/dist/modes/interactive/event-notices.js +79 -0
- package/dist/modes/interactive/external-editor.d.ts +16 -0
- package/dist/modes/interactive/external-editor.js +39 -0
- package/dist/modes/interactive/interactive-mode.d.ts +14 -1
- package/dist/modes/interactive/interactive-mode.js +134 -136
- package/dist/modes/interactive/key-dispatch.d.ts +16 -2
- package/dist/modes/interactive/key-dispatch.js +49 -1
- package/dist/modes/interactive/line/line-mode.d.ts +2 -1
- package/dist/modes/interactive/line/line-mode.js +22 -5
- package/dist/modes/interactive/line/line-render.d.ts +2 -1
- package/dist/modes/interactive/line/line-render.js +40 -4
- package/dist/modes/interactive/message-view.d.ts +1 -1
- package/dist/modes/interactive/message-view.js +24 -2
- package/dist/modes/interactive/panels.d.ts +18 -1
- package/dist/modes/interactive/panels.js +19 -5
- package/dist/modes/interactive/plan-command.d.ts +40 -0
- package/dist/modes/interactive/plan-command.js +129 -0
- package/dist/modes/interactive/plan-dialog.d.ts +98 -0
- package/dist/modes/interactive/plan-dialog.js +290 -0
- package/dist/modes/interactive/plan-flow.d.ts +34 -0
- package/dist/modes/interactive/plan-flow.js +98 -0
- package/dist/modes/interactive/rewind-command.d.ts +31 -0
- package/dist/modes/interactive/rewind-command.js +106 -0
- package/dist/modes/interactive/rewind-flow.d.ts +37 -0
- package/dist/modes/interactive/rewind-flow.js +151 -0
- package/dist/modes/interactive/rewind-list.d.ts +36 -0
- package/dist/modes/interactive/rewind-list.js +123 -0
- package/dist/modes/interactive/rewind-panel.d.ts +77 -0
- package/dist/modes/interactive/rewind-panel.js +309 -0
- package/dist/modes/interactive/rewind-text.d.ts +40 -0
- package/dist/modes/interactive/rewind-text.js +139 -0
- package/dist/modes/interactive/session-events.d.ts +24 -0
- package/dist/modes/interactive/session-events.js +96 -0
- package/dist/modes/interactive/status-area.d.ts +73 -0
- package/dist/modes/interactive/status-area.js +190 -0
- package/dist/modes/interactive/status-bar.d.ts +99 -11
- package/dist/modes/interactive/status-bar.js +273 -90
- package/dist/modes/interactive/status-line.d.ts +33 -0
- package/dist/modes/interactive/status-line.js +113 -0
- package/dist/modes/interactive/subagent-view.d.ts +56 -0
- package/dist/modes/interactive/subagent-view.js +154 -0
- package/dist/modes/interactive/tasks-report.d.ts +27 -0
- package/dist/modes/interactive/tasks-report.js +114 -0
- package/dist/modes/interactive/tool-view.d.ts +12 -2
- package/dist/modes/interactive/tool-view.js +34 -4
- package/dist/modes/print/print-mode.d.ts +13 -4
- package/dist/modes/print/print-mode.js +56 -12
- package/dist/modes/rpc/commands.d.ts +9 -0
- package/dist/modes/rpc/commands.js +44 -0
- package/dist/modes/rpc/rpc-mode.js +7 -1
- package/dist/modes/session-report.d.ts +11 -0
- package/dist/modes/session-report.js +48 -1
- package/dist/permissions/auto-safe.d.ts +5 -0
- package/dist/permissions/auto-safe.js +18 -0
- package/dist/permissions/classifier.d.ts +4 -0
- package/dist/permissions/classifier.js +2 -0
- package/dist/permissions/modes.js +1 -1
- package/dist/permissions/pipeline.d.ts +43 -4
- package/dist/permissions/pipeline.js +114 -9
- package/dist/permissions/readonly-bash.d.ts +30 -0
- package/dist/permissions/readonly-bash.js +118 -0
- package/dist/permissions/types.d.ts +42 -0
- package/dist/plan/compose.d.ts +9 -0
- package/dist/plan/compose.js +19 -0
- package/dist/plan/controller.d.ts +71 -0
- package/dist/plan/controller.js +27 -0
- package/dist/plan/done-markers.d.ts +17 -0
- package/dist/plan/done-markers.js +51 -0
- package/dist/plan/extract.d.ts +29 -0
- package/dist/plan/extract.js +160 -0
- package/dist/plan/prompts.d.ts +36 -0
- package/dist/plan/prompts.js +89 -0
- package/dist/plan/store.d.ts +37 -0
- package/dist/plan/store.js +90 -0
- package/dist/rpc.d.ts +58 -2
- package/dist/rpc.js +7 -0
- package/dist/sandbox/bash.d.ts +74 -0
- package/dist/sandbox/bash.js +126 -0
- package/dist/sandbox/detect.d.ts +48 -0
- package/dist/sandbox/detect.js +149 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/profile.d.ts +33 -0
- package/dist/sandbox/profile.js +76 -0
- package/dist/sandbox/wrap.d.ts +29 -0
- package/dist/sandbox/wrap.js +117 -0
- package/dist/sdk.d.ts +33 -2
- package/dist/sdk.js +24 -2
- package/dist/session/types.d.ts +3 -1
- package/dist/session/types.js +1 -0
- package/dist/skills/builtin.d.ts +31 -0
- package/dist/skills/builtin.js +109 -0
- package/dist/skills/discover.d.ts +2 -1
- package/dist/skills/index-prompt.js +5 -8
- package/dist/tools/background-jobs.d.ts +71 -0
- package/dist/tools/background-jobs.js +198 -0
- package/dist/tools/bash.d.ts +53 -1
- package/dist/tools/bash.js +158 -7
- package/dist/tools/clipboard-image.d.ts +44 -0
- package/dist/tools/clipboard-image.js +148 -0
- package/dist/tools/edit.d.ts +2 -0
- package/dist/tools/edit.js +18 -3
- package/dist/tools/image-file.d.ts +38 -3
- package/dist/tools/image-file.js +81 -10
- package/dist/tools/image-resize.d.ts +58 -0
- package/dist/tools/image-resize.js +97 -0
- package/dist/tools/presets.d.ts +7 -2
- package/dist/tools/presets.js +11 -4
- package/dist/tools/read.d.ts +5 -2
- package/dist/tools/read.js +13 -10
- package/dist/tools/registry.d.ts +7 -1
- package/dist/tools/registry.js +27 -4
- package/dist/tools/task-ctl.d.ts +22 -0
- package/dist/tools/task-ctl.js +116 -0
- package/dist/tools/task.d.ts +22 -10
- package/dist/tools/task.js +66 -26
- package/dist/tools/todo.d.ts +22 -2
- package/dist/tools/todo.js +58 -14
- package/dist/tools/truncate.d.ts +15 -0
- package/dist/tools/truncate.js +25 -0
- package/dist/tools/types.d.ts +113 -0
- package/dist/tools/types.js +5 -0
- package/dist/tools/write.js +3 -0
- package/dist/tui/glyphs.d.ts +4 -0
- package/dist/tui/glyphs.js +4 -0
- package/dist/tui/keybindings.d.ts +5 -0
- package/dist/tui/keybindings.js +5 -0
- package/docs/acp.md +78 -0
- package/docs/agents.md +250 -0
- package/docs/codemode.md +12 -8
- package/docs/hooks.md +26 -22
- package/docs/permissions.md +77 -13
- package/docs/plan.md +107 -0
- package/docs/providers.md +228 -52
- package/docs/rewind-plan.md +181 -0
- package/docs/rpc.md +92 -5
- package/docs/sandbox.md +215 -0
- package/docs/session-format.md +41 -21
- package/docs/sessions.md +41 -1
- package/docs/tui-design.md +751 -0
- package/docs/tui.md +125 -6
- package/package.json +13 -2
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话侧的检查点后端(与 RW-B 约定的接口)。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* 会话(`src/agent/session-rewind.ts`)只经这里的接口使用检查点:工厂拿到会话上下文后返回后端;
|
|
5
|
+
* 内存会话不调工厂,`mode: "off"` 时工厂返回 undefined。条目重放在第一次使用时惰性执行。
|
|
6
|
+
*
|
|
7
|
+
* `shadow-git`(§6,RW-D):在 tools 的基础上,每个新回合先做影子快照,提交 id 记进
|
|
8
|
+
* `CheckpointData.shadowCommit`;恢复时目标有影子提交就以影子差异为准(同时覆盖 edit / write 跟踪的文件),
|
|
9
|
+
* 否则回退 tools。git 不可用、cwd 是家目录或根目录、文件数或快照耗时超限 → 本会话降级为 tools 并 warn 一次。
|
|
10
|
+
*/
|
|
11
|
+
import type { SessionEntry } from "../session/types.js";
|
|
12
|
+
import { type ShadowGitOptions } from "./shadow-git.js";
|
|
13
|
+
import type { CheckpointHooks, CheckpointMode, CodeRestoreResult } from "./types.js";
|
|
14
|
+
export interface CheckpointBackendContext {
|
|
15
|
+
cwd: string;
|
|
16
|
+
/** 全部会话条目(重放 ama.checkpoint* 用;不限活动分支)。 */
|
|
17
|
+
entries(): readonly SessionEntry[];
|
|
18
|
+
/** 追加 custom 条目(ama.checkpoint / ama.checkpoint-track)。 */
|
|
19
|
+
appendCustom(customType: string, data: unknown): void;
|
|
20
|
+
/** 当前回合的用户消息 id(beforeWrite 追加 checkpoint-track 时用;无回合时 undefined)。 */
|
|
21
|
+
currentTurn(): string | undefined;
|
|
22
|
+
log(level: "debug" | "info" | "warn" | "error", message: string): void;
|
|
23
|
+
}
|
|
24
|
+
export interface CheckpointBackend {
|
|
25
|
+
/** 交给 ToolContext.checkpoint(子会话共用父会话的这一份)。 */
|
|
26
|
+
readonly hooks: CheckpointHooks;
|
|
27
|
+
/** 新回合用户消息落盘后调用。 */
|
|
28
|
+
snapshot(userEntryId: string): Promise<void>;
|
|
29
|
+
/** 该用户消息有可用检查点(含 keep 上限)。 */
|
|
30
|
+
hasCheckpoint(userEntryId: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* 恢复到 userEntryId 之前;dryRun 只做 §3.2 1–4 + diff。
|
|
33
|
+
* touched = 被恢复 / 删除文件的绝对路径(从 readFiles 移除;dryRun 时为空)。
|
|
34
|
+
*/
|
|
35
|
+
restore(userEntryId: string, options: {
|
|
36
|
+
dryRun: boolean;
|
|
37
|
+
onConflict: "skip" | "overwrite";
|
|
38
|
+
}): Promise<{
|
|
39
|
+
result: CodeRestoreResult;
|
|
40
|
+
touched: string[];
|
|
41
|
+
}>;
|
|
42
|
+
/** 记录的 HEAD 与当前不同时返回。 */
|
|
43
|
+
gitHint(userEntryId: string): Promise<{
|
|
44
|
+
recordedHead: string;
|
|
45
|
+
currentHead: string;
|
|
46
|
+
} | undefined>;
|
|
47
|
+
}
|
|
48
|
+
export type CheckpointBackendFactory = (ctx: CheckpointBackendContext) => CheckpointBackend | undefined;
|
|
49
|
+
export interface CheckpointBackendSettings {
|
|
50
|
+
mode: CheckpointMode;
|
|
51
|
+
dataDir: string;
|
|
52
|
+
maxFileBytes?: number;
|
|
53
|
+
keep?: number;
|
|
54
|
+
/** 会话根目录:登记进 file-history/roots.json,GC 标记时会扫描它。 */
|
|
55
|
+
sessionsRoot?: string;
|
|
56
|
+
/** 影子 git 的参数(测试注入阈值、git 路径、环境)。 */
|
|
57
|
+
shadow?: ShadowGitOptions & {
|
|
58
|
+
homeDir?: string;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export declare function createCheckpointBackendFactory(settings: CheckpointBackendSettings): CheckpointBackendFactory;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话侧的检查点后端(与 RW-B 约定的接口)。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* 会话(`src/agent/session-rewind.ts`)只经这里的接口使用检查点:工厂拿到会话上下文后返回后端;
|
|
5
|
+
* 内存会话不调工厂,`mode: "off"` 时工厂返回 undefined。条目重放在第一次使用时惰性执行。
|
|
6
|
+
*
|
|
7
|
+
* `shadow-git`(§6,RW-D):在 tools 的基础上,每个新回合先做影子快照,提交 id 记进
|
|
8
|
+
* `CheckpointData.shadowCommit`;恢复时目标有影子提交就以影子差异为准(同时覆盖 edit / write 跟踪的文件),
|
|
9
|
+
* 否则回退 tools。git 不可用、cwd 是家目录或根目录、文件数或快照耗时超限 → 本会话降级为 tools 并 warn 一次。
|
|
10
|
+
*/
|
|
11
|
+
import { registerSessionRoot } from "./gc.js";
|
|
12
|
+
import { loadCheckpoints, isRewindable } from "./replay.js";
|
|
13
|
+
import { emptyCodeResult, gitHintFor, restoreCheckpoint } from "./restore.js";
|
|
14
|
+
import { GitMissingError, ShadowRepo, unsafeShadowCwd, } from "./shadow-git.js";
|
|
15
|
+
import { latestShadowCommit, restoreFromShadow } from "./shadow-restore.js";
|
|
16
|
+
import { CheckpointTracker } from "./tracker.js";
|
|
17
|
+
export function createCheckpointBackendFactory(settings) {
|
|
18
|
+
return (ctx) => {
|
|
19
|
+
if (settings.mode === "off")
|
|
20
|
+
return undefined;
|
|
21
|
+
let tracker;
|
|
22
|
+
let registered = false;
|
|
23
|
+
const warn = (message) => ctx.log("warn", message);
|
|
24
|
+
const get = () => {
|
|
25
|
+
tracker ??= new CheckpointTracker({
|
|
26
|
+
cwd: ctx.cwd,
|
|
27
|
+
dataDir: settings.dataDir,
|
|
28
|
+
...(settings.maxFileBytes !== undefined ? { maxFileBytes: settings.maxFileBytes } : {}),
|
|
29
|
+
append: (customType, data) => ctx.appendCustom(customType, data),
|
|
30
|
+
currentTurn: () => ctx.currentTurn(),
|
|
31
|
+
warn,
|
|
32
|
+
initial: loadCheckpoints(ctx.entries()),
|
|
33
|
+
});
|
|
34
|
+
if (!registered && settings.sessionsRoot !== undefined) {
|
|
35
|
+
registered = true;
|
|
36
|
+
registerSessionRoot(settings.dataDir, settings.sessionsRoot).catch((error) => ctx.log("debug", `检查点:登记会话目录失败(${String(error)})`));
|
|
37
|
+
}
|
|
38
|
+
return tracker;
|
|
39
|
+
};
|
|
40
|
+
// 影子 git:undefined = 未建;null = 本会话不用(模式不是 shadow-git 或已降级)
|
|
41
|
+
let shadow = settings.mode === "shadow-git" ? undefined : null;
|
|
42
|
+
const degrade = (message) => {
|
|
43
|
+
shadow = null;
|
|
44
|
+
warn(`检查点:影子 git 不可用(${message}),本会话改用 tools 模式`);
|
|
45
|
+
};
|
|
46
|
+
const getShadow = () => {
|
|
47
|
+
if (shadow !== undefined)
|
|
48
|
+
return shadow;
|
|
49
|
+
const unsafe = unsafeShadowCwd(ctx.cwd, settings.shadow?.homeDir);
|
|
50
|
+
if (unsafe !== undefined) {
|
|
51
|
+
degrade(unsafe);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
shadow = new ShadowRepo(settings.dataDir, ctx.cwd, settings.shadow ?? {});
|
|
55
|
+
return shadow;
|
|
56
|
+
};
|
|
57
|
+
const shadowSnapshot = async (userEntryId) => {
|
|
58
|
+
const repo = getShadow();
|
|
59
|
+
if (repo === null)
|
|
60
|
+
return undefined;
|
|
61
|
+
try {
|
|
62
|
+
const parent = latestShadowCommit(get().state);
|
|
63
|
+
const snap = await repo.snapshot(parent, `ama checkpoint ${userEntryId}`);
|
|
64
|
+
if (snap.degrade !== undefined)
|
|
65
|
+
degrade(snap.degrade.message);
|
|
66
|
+
return snap.commit;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
degrade(error instanceof GitMissingError ? "PATH 里找不到 git" : describe(error));
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const hooks = {
|
|
74
|
+
beforeWrite: (absolutePath) => get().beforeWrite(absolutePath),
|
|
75
|
+
afterWrite: (absolutePath, content) => get().afterWrite(absolutePath, content),
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
hooks,
|
|
79
|
+
snapshot: async (userEntryId) => {
|
|
80
|
+
try {
|
|
81
|
+
const shadowCommit = await shadowSnapshot(userEntryId);
|
|
82
|
+
await get().snapshot(userEntryId, shadowCommit !== undefined ? { shadowCommit } : {});
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
warn(`检查点:建立失败(${describe(error)})`);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
hasCheckpoint: (userEntryId) => isRewindable(get().state, userEntryId, settings.keep),
|
|
89
|
+
restore: async (userEntryId, options) => {
|
|
90
|
+
const t = get();
|
|
91
|
+
const target = t.state.byUserEntry.get(userEntryId);
|
|
92
|
+
if (target === undefined || !isRewindable(t.state, userEntryId, settings.keep)) {
|
|
93
|
+
return { result: emptyCodeResult(), touched: [] };
|
|
94
|
+
}
|
|
95
|
+
const base = {
|
|
96
|
+
cwd: ctx.cwd,
|
|
97
|
+
dataDir: settings.dataDir,
|
|
98
|
+
state: t.state,
|
|
99
|
+
target,
|
|
100
|
+
lastWritten: t.lastWritten,
|
|
101
|
+
dryRun: options.dryRun,
|
|
102
|
+
onConflict: options.onConflict,
|
|
103
|
+
...(settings.maxFileBytes !== undefined ? { maxDiffBytes: settings.maxFileBytes } : {}),
|
|
104
|
+
};
|
|
105
|
+
// 有影子提交就以影子差异为准(降级前留下的提交照样可用);影子不可用时回退 tools
|
|
106
|
+
if (target.shadowCommit !== undefined && settings.mode === "shadow-git") {
|
|
107
|
+
const unsafe = unsafeShadowCwd(ctx.cwd, settings.shadow?.homeDir);
|
|
108
|
+
if (unsafe === undefined) {
|
|
109
|
+
const repo = shadow ?? new ShadowRepo(settings.dataDir, ctx.cwd, settings.shadow ?? {});
|
|
110
|
+
try {
|
|
111
|
+
return await restoreFromShadow({
|
|
112
|
+
...base,
|
|
113
|
+
repo,
|
|
114
|
+
...(settings.maxFileBytes !== undefined
|
|
115
|
+
? { maxFileBytes: settings.maxFileBytes }
|
|
116
|
+
: {}),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
warn(`检查点:按影子提交恢复失败(${describe(error)}),改按 tools 记录恢复`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return restoreCheckpoint(base);
|
|
125
|
+
},
|
|
126
|
+
gitHint: async (userEntryId) => {
|
|
127
|
+
const target = get().state.byUserEntry.get(userEntryId);
|
|
128
|
+
return target === undefined ? undefined : gitHintFor(ctx.cwd, target);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function describe(error) {
|
|
134
|
+
return error instanceof Error ? error.message : String(error);
|
|
135
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 检查点备份存储(docs/rewind-plan.md §1.1)。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* - 位置 `<dataDir>/file-history/blobs/<sha256 前 2 位>/<sha256>`,内容为文件原字节;
|
|
5
|
+
* 先写 `<name>.tmp-<pid>-<序号>` 再 rename,已存在即跳过(内容寻址,同内容只存一份)。
|
|
6
|
+
* - `recordFile` 把磁盘上一个路径变成 `FileRecord`:不存在记 `blob: null`;符号链接与非普通文件记
|
|
7
|
+
* `skipped: "not_regular"`;超过上限记 `skipped: "too_large"`(都不写 blob)。
|
|
8
|
+
* - 路径键(§1.2):cwd 内为相对路径(`/` 分隔),cwd 外为绝对路径。
|
|
9
|
+
*/
|
|
10
|
+
import type { FileRecord } from "./types.js";
|
|
11
|
+
export declare const FILE_HISTORY_DIR = "file-history";
|
|
12
|
+
export declare const BLOBS_DIR = "blobs";
|
|
13
|
+
export declare const DEFAULT_MAX_FILE_BYTES: number;
|
|
14
|
+
export declare function fileHistoryDir(dataDir: string): string;
|
|
15
|
+
export declare function blobsDir(dataDir: string): string;
|
|
16
|
+
export declare function isBlobHash(value: unknown): value is string;
|
|
17
|
+
/** blob 的路径;哈希格式不对时抛(防止拼出目录穿越)。 */
|
|
18
|
+
export declare function blobPath(dataDir: string, hash: string): string;
|
|
19
|
+
export declare function hashBytes(bytes: Uint8Array | string): string;
|
|
20
|
+
/** 流式算文件 sha256(不受 maxFileBytes 限制,冲突检测用)。 */
|
|
21
|
+
export declare function hashFile(path: string): Promise<string>;
|
|
22
|
+
/** 写入 blob(已存在跳过),返回哈希。 */
|
|
23
|
+
export declare function putBlob(dataDir: string, bytes: Uint8Array, hash?: string): Promise<string>;
|
|
24
|
+
/** 读 blob;不存在返回 undefined。 */
|
|
25
|
+
export declare function readBlob(dataDir: string, hash: string): Promise<Buffer | undefined>;
|
|
26
|
+
export declare function hasBlob(dataDir: string, hash: string): Promise<boolean>;
|
|
27
|
+
export interface RecordFileOptions {
|
|
28
|
+
dataDir: string;
|
|
29
|
+
maxFileBytes?: number;
|
|
30
|
+
}
|
|
31
|
+
/** 记录时一并给出的 lstat 信息(快照沿用判断用)。 */
|
|
32
|
+
export interface RecordedFile {
|
|
33
|
+
record: FileRecord;
|
|
34
|
+
/** 记录开始的时间(毫秒);mtime 不晚于它(减去粒度余量)才可沿用。 */
|
|
35
|
+
at: number;
|
|
36
|
+
}
|
|
37
|
+
/** 父目录 realpath;父目录不存在时 undefined。 */
|
|
38
|
+
export declare function realParentOf(absolutePath: string): Promise<string | undefined>;
|
|
39
|
+
/** 把磁盘上的一个路径记成 FileRecord(需要时写 blob)。 */
|
|
40
|
+
export declare function recordFile(absolutePath: string, options: RecordFileOptions): Promise<RecordedFile>;
|
|
41
|
+
/** 绝对路径 → 记录键(cwd 内相对、`/` 分隔;cwd 外绝对)。 */
|
|
42
|
+
export declare function pathKey(cwd: string, absolutePath: string): string;
|
|
43
|
+
/** 记录键 → 绝对路径。 */
|
|
44
|
+
export declare function keyToPath(cwd: string, key: string): string;
|
|
45
|
+
export interface BlobUsage {
|
|
46
|
+
blobs: number;
|
|
47
|
+
bytes: number;
|
|
48
|
+
}
|
|
49
|
+
/** file-history 的 blob 数与字节数(不含临时文件)。 */
|
|
50
|
+
export declare function blobUsage(dataDir: string): Promise<BlobUsage>;
|
|
51
|
+
export interface BlobFile {
|
|
52
|
+
path: string;
|
|
53
|
+
/** 哈希;临时文件为 undefined。 */
|
|
54
|
+
hash?: string;
|
|
55
|
+
size: number;
|
|
56
|
+
mtimeMs: number;
|
|
57
|
+
}
|
|
58
|
+
/** 列出 blob 目录下的文件(含残留的临时文件)。 */
|
|
59
|
+
export declare function listBlobs(dataDir: string, includeTemp?: boolean): Promise<BlobFile[]>;
|
|
60
|
+
export declare function isNotFound(error: unknown): boolean;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 检查点备份存储(docs/rewind-plan.md §1.1)。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* - 位置 `<dataDir>/file-history/blobs/<sha256 前 2 位>/<sha256>`,内容为文件原字节;
|
|
5
|
+
* 先写 `<name>.tmp-<pid>-<序号>` 再 rename,已存在即跳过(内容寻址,同内容只存一份)。
|
|
6
|
+
* - `recordFile` 把磁盘上一个路径变成 `FileRecord`:不存在记 `blob: null`;符号链接与非普通文件记
|
|
7
|
+
* `skipped: "not_regular"`;超过上限记 `skipped: "too_large"`(都不写 blob)。
|
|
8
|
+
* - 路径键(§1.2):cwd 内为相对路径(`/` 分隔),cwd 外为绝对路径。
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { createReadStream } from "node:fs";
|
|
12
|
+
import { lstat, mkdir, readFile, readdir, realpath, rename, rm, stat, utimes, writeFile, } from "node:fs/promises";
|
|
13
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
|
+
export const FILE_HISTORY_DIR = "file-history";
|
|
15
|
+
export const BLOBS_DIR = "blobs";
|
|
16
|
+
export const DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
17
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
18
|
+
export function fileHistoryDir(dataDir) {
|
|
19
|
+
return join(dataDir, FILE_HISTORY_DIR);
|
|
20
|
+
}
|
|
21
|
+
export function blobsDir(dataDir) {
|
|
22
|
+
return join(fileHistoryDir(dataDir), BLOBS_DIR);
|
|
23
|
+
}
|
|
24
|
+
export function isBlobHash(value) {
|
|
25
|
+
return typeof value === "string" && SHA256_HEX.test(value);
|
|
26
|
+
}
|
|
27
|
+
/** blob 的路径;哈希格式不对时抛(防止拼出目录穿越)。 */
|
|
28
|
+
export function blobPath(dataDir, hash) {
|
|
29
|
+
if (!isBlobHash(hash))
|
|
30
|
+
throw new Error(`invalid blob hash: ${hash}`);
|
|
31
|
+
return join(blobsDir(dataDir), hash.slice(0, 2), hash);
|
|
32
|
+
}
|
|
33
|
+
export function hashBytes(bytes) {
|
|
34
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
35
|
+
}
|
|
36
|
+
/** 流式算文件 sha256(不受 maxFileBytes 限制,冲突检测用)。 */
|
|
37
|
+
export function hashFile(path) {
|
|
38
|
+
return new Promise((resolvePromise, reject) => {
|
|
39
|
+
const hash = createHash("sha256");
|
|
40
|
+
const stream = createReadStream(path);
|
|
41
|
+
stream.on("error", reject);
|
|
42
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
43
|
+
stream.on("end", () => resolvePromise(hash.digest("hex")));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
let tmpCounter = 0;
|
|
47
|
+
/** 写入 blob(已存在跳过),返回哈希。 */
|
|
48
|
+
export async function putBlob(dataDir, bytes, hash = hashBytes(bytes)) {
|
|
49
|
+
const target = blobPath(dataDir, hash);
|
|
50
|
+
if (await exists(target)) {
|
|
51
|
+
// 复用旧 blob:刷新 mtime,避免并发的 GC 在新条目落盘前把它当作「未引用且超过 1 天」删掉。
|
|
52
|
+
const now = new Date();
|
|
53
|
+
await utimes(target, now, now).catch(() => undefined);
|
|
54
|
+
return hash;
|
|
55
|
+
}
|
|
56
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
57
|
+
const tmp = `${target}.tmp-${process.pid}-${tmpCounter++}`;
|
|
58
|
+
try {
|
|
59
|
+
await writeFile(tmp, bytes, { mode: 0o600 });
|
|
60
|
+
await rename(tmp, target);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
await rm(tmp, { force: true });
|
|
64
|
+
if (await exists(target))
|
|
65
|
+
return hash;
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
return hash;
|
|
69
|
+
}
|
|
70
|
+
/** 读 blob;不存在返回 undefined。 */
|
|
71
|
+
export async function readBlob(dataDir, hash) {
|
|
72
|
+
try {
|
|
73
|
+
return await readFile(blobPath(dataDir, hash));
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (isNotFound(error))
|
|
77
|
+
return undefined;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export async function hasBlob(dataDir, hash) {
|
|
82
|
+
return exists(blobPath(dataDir, hash));
|
|
83
|
+
}
|
|
84
|
+
/** 父目录 realpath;父目录不存在时 undefined。 */
|
|
85
|
+
export async function realParentOf(absolutePath) {
|
|
86
|
+
try {
|
|
87
|
+
return await realpath(dirname(absolutePath));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (isNotFound(error))
|
|
91
|
+
return undefined;
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** 把磁盘上的一个路径记成 FileRecord(需要时写 blob)。 */
|
|
96
|
+
export async function recordFile(absolutePath, options) {
|
|
97
|
+
const at = Date.now();
|
|
98
|
+
const max = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
99
|
+
let info;
|
|
100
|
+
try {
|
|
101
|
+
info = await lstat(absolutePath);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (!isNotFound(error))
|
|
105
|
+
throw error;
|
|
106
|
+
const realParentDir = await realParentOf(absolutePath);
|
|
107
|
+
return {
|
|
108
|
+
record: { blob: null, ...(realParentDir !== undefined ? { realParentDir } : {}) },
|
|
109
|
+
at,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (!info.isFile())
|
|
113
|
+
return { record: { blob: null, skipped: "not_regular" }, at };
|
|
114
|
+
const mode = info.mode & 0o7777;
|
|
115
|
+
const realParentDir = await realParentOf(absolutePath);
|
|
116
|
+
const base = { mode, size: info.size, ...(realParentDir !== undefined ? { realParentDir } : {}) };
|
|
117
|
+
if (info.size > max)
|
|
118
|
+
return { record: { blob: null, ...base, skipped: "too_large" }, at };
|
|
119
|
+
const bytes = await readFile(absolutePath);
|
|
120
|
+
if (bytes.length > max)
|
|
121
|
+
return { record: { blob: null, ...base, size: bytes.length, skipped: "too_large" }, at };
|
|
122
|
+
const blob = await putBlob(options.dataDir, bytes);
|
|
123
|
+
return { record: { blob, ...base, size: bytes.length }, at };
|
|
124
|
+
}
|
|
125
|
+
/** 绝对路径 → 记录键(cwd 内相对、`/` 分隔;cwd 外绝对)。 */
|
|
126
|
+
export function pathKey(cwd, absolutePath) {
|
|
127
|
+
const abs = resolve(absolutePath);
|
|
128
|
+
const rel = relative(resolve(cwd), abs);
|
|
129
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel))
|
|
130
|
+
return abs;
|
|
131
|
+
return rel.split(sep).join("/");
|
|
132
|
+
}
|
|
133
|
+
/** 记录键 → 绝对路径。 */
|
|
134
|
+
export function keyToPath(cwd, key) {
|
|
135
|
+
return isAbsolute(key) ? resolve(key) : resolve(cwd, ...key.split("/"));
|
|
136
|
+
}
|
|
137
|
+
/** file-history 的 blob 数与字节数(不含临时文件)。 */
|
|
138
|
+
export async function blobUsage(dataDir) {
|
|
139
|
+
const usage = { blobs: 0, bytes: 0 };
|
|
140
|
+
for (const blob of await listBlobs(dataDir)) {
|
|
141
|
+
usage.blobs++;
|
|
142
|
+
usage.bytes += blob.size;
|
|
143
|
+
}
|
|
144
|
+
return usage;
|
|
145
|
+
}
|
|
146
|
+
/** 列出 blob 目录下的文件(含残留的临时文件)。 */
|
|
147
|
+
export async function listBlobs(dataDir, includeTemp = false) {
|
|
148
|
+
const root = blobsDir(dataDir);
|
|
149
|
+
let shards;
|
|
150
|
+
try {
|
|
151
|
+
shards = await readdir(root);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (isNotFound(error))
|
|
155
|
+
return [];
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
const out = [];
|
|
159
|
+
for (const shard of shards) {
|
|
160
|
+
if (!/^[0-9a-f]{2}$/.test(shard))
|
|
161
|
+
continue;
|
|
162
|
+
let names;
|
|
163
|
+
try {
|
|
164
|
+
names = await readdir(join(root, shard));
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
for (const name of names) {
|
|
170
|
+
const isBlob = isBlobHash(name) && name.startsWith(shard);
|
|
171
|
+
if (!isBlob && !(includeTemp && name.includes(".tmp-")))
|
|
172
|
+
continue;
|
|
173
|
+
const path = join(root, shard, name);
|
|
174
|
+
try {
|
|
175
|
+
const info = await stat(path);
|
|
176
|
+
if (!info.isFile())
|
|
177
|
+
continue;
|
|
178
|
+
out.push({
|
|
179
|
+
path,
|
|
180
|
+
...(isBlob ? { hash: name } : {}),
|
|
181
|
+
size: info.size,
|
|
182
|
+
mtimeMs: info.mtimeMs,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// 并发删除:忽略
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
export function isNotFound(error) {
|
|
193
|
+
return error?.code === "ENOENT";
|
|
194
|
+
}
|
|
195
|
+
async function exists(path) {
|
|
196
|
+
try {
|
|
197
|
+
await stat(path);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 备份清理(docs/rewind-plan.md §1.4)。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* - 标记:递归扫描会话根目录下全部 `*.jsonl`(含 `.trash/`,trash 里的会话还能找回),凡含
|
|
5
|
+
* `ama.checkpoint` 的行,取其中所有 64 位十六进制串作为引用。不解析 JSON:损坏或未来格式的行也只会
|
|
6
|
+
* 多保留,不会误删。任何会话文件读不了 → 整次 GC 放弃(抛错),不删除任何东西。
|
|
7
|
+
* - 清除:未被引用且 mtime 早于 `minAgeMs`(缺省 1 天)的 blob;同样年龄的残留临时文件一并删除。
|
|
8
|
+
* - 会话根目录除调用方给出的以外,还包括 `<dataDir>/file-history/roots.json` 里登记过的(宿主 profile
|
|
9
|
+
* 的 `sessionDir` 不在缺省位置时,备份仍被看到)。
|
|
10
|
+
*/
|
|
11
|
+
export declare const GC_MIN_AGE_MS: number;
|
|
12
|
+
export declare const ROOTS_FILE = "roots.json";
|
|
13
|
+
/** 已登记的会话根目录。 */
|
|
14
|
+
export declare function readSessionRoots(dataDir: string): Promise<string[]>;
|
|
15
|
+
/** 登记会话根目录(已登记则不写)。 */
|
|
16
|
+
export declare function registerSessionRoot(dataDir: string, sessionsRoot: string): Promise<void>;
|
|
17
|
+
export interface BlobRefs {
|
|
18
|
+
refs: Set<string>;
|
|
19
|
+
sessionFiles: number;
|
|
20
|
+
}
|
|
21
|
+
/** 标记:会话根目录下全部会话文件引用的 blob。 */
|
|
22
|
+
export declare function collectBlobRefs(sessionRoots: readonly string[]): Promise<BlobRefs>;
|
|
23
|
+
export interface GcOptions {
|
|
24
|
+
dataDir: string;
|
|
25
|
+
/** 要扫描的会话根目录;登记过的根目录自动并入。 */
|
|
26
|
+
sessionRoots: readonly string[];
|
|
27
|
+
dryRun?: boolean;
|
|
28
|
+
/** 缺省 1 天。 */
|
|
29
|
+
minAgeMs?: number;
|
|
30
|
+
now?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface GcResult {
|
|
33
|
+
sessionFiles: number;
|
|
34
|
+
referenced: number;
|
|
35
|
+
/** 删除(dryRun 时为将删除)的 blob 与临时文件路径。 */
|
|
36
|
+
removed: string[];
|
|
37
|
+
removedBytes: number;
|
|
38
|
+
/** 保留的 blob 数与字节数。 */
|
|
39
|
+
kept: {
|
|
40
|
+
blobs: number;
|
|
41
|
+
bytes: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** 标记 + 清除。 */
|
|
45
|
+
export declare function gcBlobs(options: GcOptions): Promise<GcResult>;
|
|
46
|
+
/** 人读的字节数(doctor / prune 输出)。 */
|
|
47
|
+
export declare function formatBytes(bytes: number): string;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 备份清理(docs/rewind-plan.md §1.4)。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* - 标记:递归扫描会话根目录下全部 `*.jsonl`(含 `.trash/`,trash 里的会话还能找回),凡含
|
|
5
|
+
* `ama.checkpoint` 的行,取其中所有 64 位十六进制串作为引用。不解析 JSON:损坏或未来格式的行也只会
|
|
6
|
+
* 多保留,不会误删。任何会话文件读不了 → 整次 GC 放弃(抛错),不删除任何东西。
|
|
7
|
+
* - 清除:未被引用且 mtime 早于 `minAgeMs`(缺省 1 天)的 blob;同样年龄的残留临时文件一并删除。
|
|
8
|
+
* - 会话根目录除调用方给出的以外,还包括 `<dataDir>/file-history/roots.json` 里登记过的(宿主 profile
|
|
9
|
+
* 的 `sessionDir` 不在缺省位置时,备份仍被看到)。
|
|
10
|
+
*/
|
|
11
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
12
|
+
import { join, resolve } from "node:path";
|
|
13
|
+
import { fileHistoryDir, isNotFound, listBlobs } from "./blobs.js";
|
|
14
|
+
import { CHECKPOINT_CUSTOM_TYPE } from "./types.js";
|
|
15
|
+
export const GC_MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
16
|
+
export const ROOTS_FILE = "roots.json";
|
|
17
|
+
const HASH_IN_LINE = /\b[0-9a-f]{64}\b/g;
|
|
18
|
+
function rootsFile(dataDir) {
|
|
19
|
+
return join(fileHistoryDir(dataDir), ROOTS_FILE);
|
|
20
|
+
}
|
|
21
|
+
/** 已登记的会话根目录。 */
|
|
22
|
+
export async function readSessionRoots(dataDir) {
|
|
23
|
+
try {
|
|
24
|
+
const value = JSON.parse(await readFile(rootsFile(dataDir), "utf8"));
|
|
25
|
+
const roots = value?.roots;
|
|
26
|
+
return Array.isArray(roots) ? roots.filter((r) => typeof r === "string") : [];
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** 登记会话根目录(已登记则不写)。 */
|
|
33
|
+
export async function registerSessionRoot(dataDir, sessionsRoot) {
|
|
34
|
+
const root = resolve(sessionsRoot);
|
|
35
|
+
const roots = await readSessionRoots(dataDir);
|
|
36
|
+
if (roots.includes(root))
|
|
37
|
+
return;
|
|
38
|
+
const dir = fileHistoryDir(dataDir);
|
|
39
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
40
|
+
const tmp = `${rootsFile(dataDir)}.tmp-${process.pid}`;
|
|
41
|
+
await writeFile(tmp, `${JSON.stringify({ roots: [...roots, root] }, null, 2)}\n`, {
|
|
42
|
+
mode: 0o600,
|
|
43
|
+
});
|
|
44
|
+
await rename(tmp, rootsFile(dataDir));
|
|
45
|
+
}
|
|
46
|
+
async function* sessionFiles(dir) {
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (isNotFound(error))
|
|
53
|
+
return;
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const full = join(dir, entry.name);
|
|
58
|
+
if (entry.isDirectory())
|
|
59
|
+
yield* sessionFiles(full);
|
|
60
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
61
|
+
yield full;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** 标记:会话根目录下全部会话文件引用的 blob。 */
|
|
65
|
+
export async function collectBlobRefs(sessionRoots) {
|
|
66
|
+
const refs = new Set();
|
|
67
|
+
let count = 0;
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
for (const root of sessionRoots) {
|
|
70
|
+
const abs = resolve(root);
|
|
71
|
+
if (seen.has(abs))
|
|
72
|
+
continue;
|
|
73
|
+
seen.add(abs);
|
|
74
|
+
for await (const file of sessionFiles(abs)) {
|
|
75
|
+
let text;
|
|
76
|
+
try {
|
|
77
|
+
text = await readFile(file, "utf8");
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (isNotFound(error))
|
|
81
|
+
continue; // 扫描期间被移走:移到的位置(trash)也在扫描范围内或已删
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
count++;
|
|
85
|
+
if (!text.includes(CHECKPOINT_CUSTOM_TYPE))
|
|
86
|
+
continue;
|
|
87
|
+
for (const line of text.split("\n")) {
|
|
88
|
+
if (!line.includes(CHECKPOINT_CUSTOM_TYPE))
|
|
89
|
+
continue;
|
|
90
|
+
for (const match of line.matchAll(HASH_IN_LINE))
|
|
91
|
+
refs.add(match[0]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { refs, sessionFiles: count };
|
|
96
|
+
}
|
|
97
|
+
/** 标记 + 清除。 */
|
|
98
|
+
export async function gcBlobs(options) {
|
|
99
|
+
const minAge = options.minAgeMs ?? GC_MIN_AGE_MS;
|
|
100
|
+
const now = options.now ?? Date.now();
|
|
101
|
+
const roots = [...options.sessionRoots, ...(await readSessionRoots(options.dataDir))];
|
|
102
|
+
const { refs, sessionFiles: count } = await collectBlobRefs(roots);
|
|
103
|
+
const result = {
|
|
104
|
+
sessionFiles: count,
|
|
105
|
+
referenced: refs.size,
|
|
106
|
+
removed: [],
|
|
107
|
+
removedBytes: 0,
|
|
108
|
+
kept: { blobs: 0, bytes: 0 },
|
|
109
|
+
};
|
|
110
|
+
for (const blob of await listBlobs(options.dataDir, true)) {
|
|
111
|
+
const old = now - blob.mtimeMs > minAge;
|
|
112
|
+
const unreferenced = blob.hash === undefined || !refs.has(blob.hash);
|
|
113
|
+
if (old && unreferenced) {
|
|
114
|
+
if (options.dryRun !== true) {
|
|
115
|
+
try {
|
|
116
|
+
await rm(blob.path, { force: true });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
result.removed.push(blob.path);
|
|
123
|
+
result.removedBytes += blob.size;
|
|
124
|
+
}
|
|
125
|
+
else if (blob.hash !== undefined) {
|
|
126
|
+
result.kept.blobs++;
|
|
127
|
+
result.kept.bytes += blob.size;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
/** 人读的字节数(doctor / prune 输出)。 */
|
|
133
|
+
export function formatBytes(bytes) {
|
|
134
|
+
if (bytes < 1024)
|
|
135
|
+
return `${bytes} B`;
|
|
136
|
+
const units = ["KiB", "MiB", "GiB", "TiB"];
|
|
137
|
+
let value = bytes / 1024;
|
|
138
|
+
let unit = 0;
|
|
139
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
140
|
+
value /= 1024;
|
|
141
|
+
unit++;
|
|
142
|
+
}
|
|
143
|
+
return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unit]}`;
|
|
144
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 读 git HEAD(docs/rewind-plan.md §1.2、D6)。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* 不起 git 进程:从 cwd 向上找 `.git`(目录,或 worktree / 子模块的 `gitdir: <路径>` 文件),读 `HEAD`;
|
|
5
|
+
* 符号引用先查 gitdir 再查 `commondir`(worktree 的分支引用在主仓库),最后查 `packed-refs`。
|
|
6
|
+
* 读不到(不在仓库、未提交过的分支、格式不认识)返回 undefined。
|
|
7
|
+
*/
|
|
8
|
+
export interface GitHead {
|
|
9
|
+
head: string;
|
|
10
|
+
branch?: string;
|
|
11
|
+
}
|
|
12
|
+
/** 找到 cwd 所在仓库的 gitdir;不在仓库返回 undefined。 */
|
|
13
|
+
export declare function findGitDir(cwd: string): Promise<string | undefined>;
|
|
14
|
+
/** cwd 所在仓库的 HEAD 提交与分支;detached 时没有 branch。 */
|
|
15
|
+
export declare function readGitHead(cwd: string): Promise<GitHead | undefined>;
|