@armadra/agent 0.0.0-stage → 0.3.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 +37 -0
- package/LICENSE +21 -0
- package/README.md +470 -2
- package/dist/agent/agent.d.ts +56 -0
- package/dist/agent/agent.js +129 -0
- package/dist/agent/loop.d.ts +51 -0
- package/dist/agent/loop.js +158 -0
- package/dist/agent/queue.d.ts +25 -0
- package/dist/agent/queue.js +55 -0
- package/dist/agent/retry.d.ts +22 -0
- package/dist/agent/retry.js +92 -0
- package/dist/agent/schema.d.ts +27 -0
- package/dist/agent/schema.js +186 -0
- package/dist/agent/session-cache.d.ts +100 -0
- package/dist/agent/session-cache.js +514 -0
- package/dist/agent/session-compaction.d.ts +49 -0
- package/dist/agent/session-compaction.js +251 -0
- package/dist/agent/session-core.d.ts +103 -0
- package/dist/agent/session-core.js +7 -0
- package/dist/agent/session-run.d.ts +55 -0
- package/dist/agent/session-run.js +246 -0
- package/dist/agent/session-state.d.ts +39 -0
- package/dist/agent/session-state.js +119 -0
- package/dist/agent/session-subagent.d.ts +54 -0
- package/dist/agent/session-subagent.js +157 -0
- package/dist/agent/session-sync.d.ts +20 -0
- package/dist/agent/session-sync.js +111 -0
- package/dist/agent/session-tools.d.ts +24 -0
- package/dist/agent/session-tools.js +277 -0
- package/dist/agent/session.d.ts +117 -0
- package/dist/agent/session.js +485 -0
- package/dist/agent/system-prompt.d.ts +49 -0
- package/dist/agent/system-prompt.js +121 -0
- package/dist/agent/tool-runner.d.ts +64 -0
- package/dist/agent/tool-runner.js +309 -0
- package/dist/agent/transform.d.ts +26 -0
- package/dist/agent/transform.js +171 -0
- package/dist/agent/types.d.ts +378 -0
- package/dist/agent/types.js +21 -0
- package/dist/ai/apis/anthropic-messages.d.ts +26 -0
- package/dist/ai/apis/anthropic-messages.js +214 -0
- package/dist/ai/apis/anthropic-request.d.ts +42 -0
- package/dist/ai/apis/anthropic-request.js +282 -0
- package/dist/ai/apis/api.d.ts +28 -0
- package/dist/ai/apis/api.js +102 -0
- package/dist/ai/apis/cache-params.d.ts +48 -0
- package/dist/ai/apis/cache-params.js +163 -0
- package/dist/ai/apis/google-generative-ai.d.ts +32 -0
- package/dist/ai/apis/google-generative-ai.js +249 -0
- package/dist/ai/apis/google-request.d.ts +40 -0
- package/dist/ai/apis/google-request.js +255 -0
- package/dist/ai/apis/openai-compat.d.ts +35 -0
- package/dist/ai/apis/openai-compat.js +142 -0
- package/dist/ai/apis/openai-completions.d.ts +28 -0
- package/dist/ai/apis/openai-completions.js +246 -0
- package/dist/ai/apis/openai-request.d.ts +35 -0
- package/dist/ai/apis/openai-request.js +303 -0
- package/dist/ai/apis/openai-responses-request.d.ts +52 -0
- package/dist/ai/apis/openai-responses-request.js +266 -0
- package/dist/ai/apis/openai-responses.d.ts +28 -0
- package/dist/ai/apis/openai-responses.js +304 -0
- package/dist/ai/apis/shared.d.ts +56 -0
- package/dist/ai/apis/shared.js +179 -0
- package/dist/ai/cache/economics.d.ts +24 -0
- package/dist/ai/cache/economics.js +69 -0
- package/dist/ai/cache/fingerprint.d.ts +17 -0
- package/dist/ai/cache/fingerprint.js +39 -0
- package/dist/ai/cache/miss.d.ts +49 -0
- package/dist/ai/cache/miss.js +96 -0
- package/dist/ai/cache/reporting.d.ts +39 -0
- package/dist/ai/cache/reporting.js +103 -0
- package/dist/ai/cache/types.d.ts +94 -0
- package/dist/ai/cache/types.js +19 -0
- package/dist/ai/cache/warmer.d.ts +94 -0
- package/dist/ai/cache/warmer.js +224 -0
- package/dist/ai/context.d.ts +35 -0
- package/dist/ai/context.js +114 -0
- package/dist/ai/cost.d.ts +21 -0
- package/dist/ai/cost.js +60 -0
- package/dist/ai/event-stream.d.ts +33 -0
- package/dist/ai/event-stream.js +101 -0
- package/dist/ai/fake/fake-provider.d.ts +68 -0
- package/dist/ai/fake/fake-provider.js +278 -0
- package/dist/ai/fake/fake-script.d.ts +69 -0
- package/dist/ai/fake/fake-script.js +142 -0
- package/dist/ai/http.d.ts +54 -0
- package/dist/ai/http.js +212 -0
- package/dist/ai/json-partial.d.ts +17 -0
- package/dist/ai/json-partial.js +243 -0
- package/dist/ai/overflow.d.ts +22 -0
- package/dist/ai/overflow.js +61 -0
- package/dist/ai/providers/auth.d.ts +71 -0
- package/dist/ai/providers/auth.js +202 -0
- package/dist/ai/providers/builtin.d.ts +23 -0
- package/dist/ai/providers/builtin.js +148 -0
- package/dist/ai/providers/catalog-data.d.ts +5 -0
- package/dist/ai/providers/catalog-data.js +19 -0
- package/dist/ai/providers/catalog.d.ts +28 -0
- package/dist/ai/providers/catalog.js +166 -0
- package/dist/ai/providers/channels.d.ts +51 -0
- package/dist/ai/providers/channels.js +96 -0
- package/dist/ai/providers/enrich.d.ts +34 -0
- package/dist/ai/providers/enrich.js +86 -0
- package/dist/ai/providers/models-dev-cache.d.ts +53 -0
- package/dist/ai/providers/models-dev-cache.js +147 -0
- package/dist/ai/providers/models-dev.d.ts +99 -0
- package/dist/ai/providers/models-dev.js +315 -0
- package/dist/ai/providers/registry.d.ts +114 -0
- package/dist/ai/providers/registry.js +458 -0
- package/dist/ai/sse.d.ts +32 -0
- package/dist/ai/sse.js +140 -0
- package/dist/ai/thinking.d.ts +42 -0
- package/dist/ai/thinking.js +101 -0
- package/dist/ai/types.d.ts +448 -0
- package/dist/ai/types.js +19 -0
- package/dist/bundle/ama-sandbox.cjs +324 -0
- package/dist/bundle/ama.cjs +27837 -0
- package/dist/bundle.d.ts +9 -0
- package/dist/bundle.js +15 -0
- package/dist/cli/args.d.ts +85 -0
- package/dist/cli/args.js +408 -0
- package/dist/cli/bootstrap.d.ts +18 -0
- package/dist/cli/bootstrap.js +399 -0
- package/dist/cli/compose-providers.d.ts +37 -0
- package/dist/cli/compose-providers.js +107 -0
- package/dist/cli/compose-session.d.ts +72 -0
- package/dist/cli/compose-session.js +370 -0
- package/dist/cli/compose-store.d.ts +24 -0
- package/dist/cli/compose-store.js +125 -0
- package/dist/cli/compose.d.ts +108 -0
- package/dist/cli/compose.js +221 -0
- package/dist/cli/default-model.d.ts +16 -0
- package/dist/cli/default-model.js +24 -0
- package/dist/cli/deps.d.ts +207 -0
- package/dist/cli/deps.js +12 -0
- package/dist/cli/exit-codes.d.ts +29 -0
- package/dist/cli/exit-codes.js +40 -0
- package/dist/cli/main.d.ts +27 -0
- package/dist/cli/main.js +177 -0
- package/dist/cli/runtime.d.ts +90 -0
- package/dist/cli/runtime.js +8 -0
- package/dist/cli/startup-screen.d.ts +15 -0
- package/dist/cli/startup-screen.js +56 -0
- package/dist/cli/startup-steps.d.ts +35 -0
- package/dist/cli/startup-steps.js +202 -0
- package/dist/cli/subcommands/auth.d.ts +12 -0
- package/dist/cli/subcommands/auth.js +91 -0
- package/dist/cli/subcommands/config.d.ts +53 -0
- package/dist/cli/subcommands/config.js +277 -0
- package/dist/cli/subcommands/context.d.ts +21 -0
- package/dist/cli/subcommands/context.js +42 -0
- package/dist/cli/subcommands/doctor.d.ts +11 -0
- package/dist/cli/subcommands/doctor.js +246 -0
- package/dist/cli/subcommands/init.d.ts +6 -0
- package/dist/cli/subcommands/init.js +22 -0
- package/dist/cli/subcommands/model-meta.d.ts +16 -0
- package/dist/cli/subcommands/model-meta.js +54 -0
- package/dist/cli/subcommands/models-cache-probe.d.ts +34 -0
- package/dist/cli/subcommands/models-cache-probe.js +190 -0
- package/dist/cli/subcommands/models-discover.d.ts +46 -0
- package/dist/cli/subcommands/models-discover.js +232 -0
- package/dist/cli/subcommands/models.d.ts +40 -0
- package/dist/cli/subcommands/models.js +152 -0
- package/dist/cli/subcommands/providers-list.d.ts +8 -0
- package/dist/cli/subcommands/providers-list.js +97 -0
- package/dist/cli/subcommands/providers-plan.d.ts +79 -0
- package/dist/cli/subcommands/providers-plan.js +215 -0
- package/dist/cli/subcommands/providers.d.ts +27 -0
- package/dist/cli/subcommands/providers.js +433 -0
- package/dist/cli/subcommands/sessions.d.ts +11 -0
- package/dist/cli/subcommands/sessions.js +100 -0
- package/dist/codemode/capability.d.ts +37 -0
- package/dist/codemode/capability.js +55 -0
- package/dist/codemode/declarations.d.ts +44 -0
- package/dist/codemode/declarations.js +111 -0
- package/dist/codemode/host-side.d.ts +59 -0
- package/dist/codemode/host-side.js +253 -0
- package/dist/codemode/modes.d.ts +23 -0
- package/dist/codemode/modes.js +40 -0
- package/dist/codemode/protocol.d.ts +91 -0
- package/dist/codemode/protocol.js +114 -0
- package/dist/codemode/sandbox-entry.d.ts +25 -0
- package/dist/codemode/sandbox-entry.js +323 -0
- package/dist/codemode/store.d.ts +26 -0
- package/dist/codemode/store.js +53 -0
- package/dist/codemode/tool.d.ts +105 -0
- package/dist/codemode/tool.js +283 -0
- package/dist/compaction/branch-summary.d.ts +30 -0
- package/dist/compaction/branch-summary.js +79 -0
- package/dist/compaction/breaker.d.ts +32 -0
- package/dist/compaction/breaker.js +65 -0
- package/dist/compaction/cut-point.d.ts +23 -0
- package/dist/compaction/cut-point.js +70 -0
- package/dist/compaction/estimate.d.ts +30 -0
- package/dist/compaction/estimate.js +114 -0
- package/dist/compaction/prune-tier.d.ts +31 -0
- package/dist/compaction/prune-tier.js +83 -0
- package/dist/compaction/serialize.d.ts +20 -0
- package/dist/compaction/serialize.js +115 -0
- package/dist/compaction/summarize-tier.d.ts +111 -0
- package/dist/compaction/summarize-tier.js +305 -0
- package/dist/config/auth-file.d.ts +39 -0
- package/dist/config/auth-file.js +92 -0
- package/dist/config/context-files.d.ts +30 -0
- package/dist/config/context-files.js +77 -0
- package/dist/config/init.d.ts +30 -0
- package/dist/config/init.js +92 -0
- package/dist/config/json-schema.d.ts +14 -0
- package/dist/config/json-schema.js +182 -0
- package/dist/config/load.d.ts +45 -0
- package/dist/config/load.js +97 -0
- package/dist/config/merge.d.ts +73 -0
- package/dist/config/merge.js +248 -0
- package/dist/config/paths.d.ts +38 -0
- package/dist/config/paths.js +108 -0
- package/dist/config/profile.d.ts +27 -0
- package/dist/config/profile.js +65 -0
- package/dist/config/schema.d.ts +34 -0
- package/dist/config/schema.js +476 -0
- package/dist/config/trust.d.ts +42 -0
- package/dist/config/trust.js +122 -0
- package/dist/config/types.d.ts +190 -0
- package/dist/config/types.js +29 -0
- package/dist/config/write.d.ts +10 -0
- package/dist/config/write.js +25 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.js +30 -0
- package/dist/hooks/config.d.ts +49 -0
- package/dist/hooks/config.js +102 -0
- package/dist/hooks/dispatcher.d.ts +41 -0
- package/dist/hooks/dispatcher.js +104 -0
- package/dist/hooks/matcher.d.ts +22 -0
- package/dist/hooks/matcher.js +108 -0
- package/dist/hooks/protocol.d.ts +33 -0
- package/dist/hooks/protocol.js +240 -0
- package/dist/hooks/runner.d.ts +30 -0
- package/dist/hooks/runner.js +143 -0
- package/dist/hooks/types.d.ts +147 -0
- package/dist/hooks/types.js +20 -0
- package/dist/host/api-impl.d.ts +75 -0
- package/dist/host/api-impl.js +173 -0
- package/dist/host/loader.d.ts +31 -0
- package/dist/host/loader.js +147 -0
- package/dist/host/types.d.ts +169 -0
- package/dist/host/types.js +14 -0
- package/dist/host.d.ts +5 -0
- package/dist/host.js +4 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +28 -0
- package/dist/modes/commands-core.d.ts +44 -0
- package/dist/modes/commands-core.js +171 -0
- package/dist/modes/image-input.d.ts +27 -0
- package/dist/modes/image-input.js +78 -0
- package/dist/modes/interactive/approval-dialog.d.ts +48 -0
- package/dist/modes/interactive/approval-dialog.js +202 -0
- package/dist/modes/interactive/commands.d.ts +41 -0
- package/dist/modes/interactive/commands.js +201 -0
- package/dist/modes/interactive/completion.d.ts +53 -0
- package/dist/modes/interactive/completion.js +157 -0
- package/dist/modes/interactive/interactive-mode.d.ts +46 -0
- package/dist/modes/interactive/interactive-mode.js +491 -0
- package/dist/modes/interactive/key-dispatch.d.ts +29 -0
- package/dist/modes/interactive/key-dispatch.js +107 -0
- package/dist/modes/interactive/line/line-editor.d.ts +50 -0
- package/dist/modes/interactive/line/line-editor.js +232 -0
- package/dist/modes/interactive/line/line-mode.d.ts +20 -0
- package/dist/modes/interactive/line/line-mode.js +218 -0
- package/dist/modes/interactive/line/line-render.d.ts +35 -0
- package/dist/modes/interactive/line/line-render.js +171 -0
- package/dist/modes/interactive/line/paste-state.d.ts +26 -0
- package/dist/modes/interactive/line/paste-state.js +72 -0
- package/dist/modes/interactive/message-view.d.ts +77 -0
- package/dist/modes/interactive/message-view.js +269 -0
- package/dist/modes/interactive/pickers.d.ts +42 -0
- package/dist/modes/interactive/pickers.js +115 -0
- package/dist/modes/interactive/startup-ui.d.ts +33 -0
- package/dist/modes/interactive/startup-ui.js +268 -0
- package/dist/modes/interactive/status-bar.d.ts +48 -0
- package/dist/modes/interactive/status-bar.js +139 -0
- package/dist/modes/interactive/tool-view.d.ts +95 -0
- package/dist/modes/interactive/tool-view.js +344 -0
- package/dist/modes/print/json-event.d.ts +15 -0
- package/dist/modes/print/json-event.js +39 -0
- package/dist/modes/print/print-mode.d.ts +17 -0
- package/dist/modes/print/print-mode.js +107 -0
- package/dist/modes/rpc/commands.d.ts +40 -0
- package/dist/modes/rpc/commands.js +263 -0
- package/dist/modes/rpc/jsonl.d.ts +16 -0
- package/dist/modes/rpc/jsonl.js +55 -0
- package/dist/modes/rpc/rpc-mode.d.ts +22 -0
- package/dist/modes/rpc/rpc-mode.js +153 -0
- package/dist/modes/session-report.d.ts +55 -0
- package/dist/modes/session-report.js +278 -0
- package/dist/modes/shared.d.ts +13 -0
- package/dist/modes/shared.js +52 -0
- package/dist/modes/startup-ui-text.d.ts +16 -0
- package/dist/modes/startup-ui-text.js +103 -0
- package/dist/permissions/broker.d.ts +47 -0
- package/dist/permissions/broker.js +107 -0
- package/dist/permissions/dangerous.d.ts +47 -0
- package/dist/permissions/dangerous.js +370 -0
- package/dist/permissions/pipeline.d.ts +51 -0
- package/dist/permissions/pipeline.js +181 -0
- package/dist/permissions/preview.d.ts +43 -0
- package/dist/permissions/preview.js +463 -0
- package/dist/permissions/rules.d.ts +58 -0
- package/dist/permissions/rules.js +249 -0
- package/dist/permissions/types.d.ts +100 -0
- package/dist/permissions/types.js +18 -0
- package/dist/rpc.d.ts +173 -0
- package/dist/rpc.js +13 -0
- package/dist/sdk.d.ts +98 -0
- package/dist/sdk.js +195 -0
- package/dist/session/manager.d.ts +70 -0
- package/dist/session/manager.js +278 -0
- package/dist/session/migrate.d.ts +19 -0
- package/dist/session/migrate.js +53 -0
- package/dist/session/projection.d.ts +50 -0
- package/dist/session/projection.js +184 -0
- package/dist/session/store.d.ts +47 -0
- package/dist/session/store.js +210 -0
- package/dist/session/tree.d.ts +21 -0
- package/dist/session/tree.js +88 -0
- package/dist/session/types.d.ts +201 -0
- package/dist/session/types.js +18 -0
- package/dist/skills/discover.d.ts +60 -0
- package/dist/skills/discover.js +160 -0
- package/dist/skills/expand.d.ts +35 -0
- package/dist/skills/expand.js +46 -0
- package/dist/skills/frontmatter.d.ts +27 -0
- package/dist/skills/frontmatter.js +157 -0
- package/dist/skills/index-prompt.d.ts +13 -0
- package/dist/skills/index-prompt.js +42 -0
- package/dist/skills/templates.d.ts +52 -0
- package/dist/skills/templates.js +177 -0
- package/dist/tools/bash.d.ts +49 -0
- package/dist/tools/bash.js +222 -0
- package/dist/tools/edit-fuzzy.d.ts +36 -0
- package/dist/tools/edit-fuzzy.js +129 -0
- package/dist/tools/edit.d.ts +38 -0
- package/dist/tools/edit.js +241 -0
- package/dist/tools/file-mutex.d.ts +8 -0
- package/dist/tools/file-mutex.js +29 -0
- package/dist/tools/glob.d.ts +44 -0
- package/dist/tools/glob.js +217 -0
- package/dist/tools/grep.d.ts +39 -0
- package/dist/tools/grep.js +179 -0
- package/dist/tools/ignore.d.ts +40 -0
- package/dist/tools/ignore.js +174 -0
- package/dist/tools/image-file.d.ts +31 -0
- package/dist/tools/image-file.js +114 -0
- package/dist/tools/ls.d.ts +21 -0
- package/dist/tools/ls.js +96 -0
- package/dist/tools/output-accumulator.d.ts +48 -0
- package/dist/tools/output-accumulator.js +106 -0
- package/dist/tools/paths.d.ts +20 -0
- package/dist/tools/paths.js +58 -0
- package/dist/tools/presets.d.ts +71 -0
- package/dist/tools/presets.js +128 -0
- package/dist/tools/process-tree.d.ts +35 -0
- package/dist/tools/process-tree.js +125 -0
- package/dist/tools/read.d.ts +24 -0
- package/dist/tools/read.js +132 -0
- package/dist/tools/registry.d.ts +43 -0
- package/dist/tools/registry.js +106 -0
- package/dist/tools/shell.d.ts +38 -0
- package/dist/tools/shell.js +91 -0
- package/dist/tools/task.d.ts +29 -0
- package/dist/tools/task.js +107 -0
- package/dist/tools/todo.d.ts +28 -0
- package/dist/tools/todo.js +107 -0
- package/dist/tools/truncate.d.ts +44 -0
- package/dist/tools/truncate.js +175 -0
- package/dist/tools/types.d.ts +135 -0
- package/dist/tools/types.js +19 -0
- package/dist/tools/write.d.ts +20 -0
- package/dist/tools/write.js +84 -0
- package/dist/tui/ansi.d.ts +29 -0
- package/dist/tui/ansi.js +420 -0
- package/dist/tui/component.d.ts +38 -0
- package/dist/tui/component.js +13 -0
- package/dist/tui/components/box.d.ts +26 -0
- package/dist/tui/components/box.js +65 -0
- package/dist/tui/components/container.d.ts +14 -0
- package/dist/tui/components/container.js +39 -0
- package/dist/tui/components/editor-buffer.d.ts +67 -0
- package/dist/tui/components/editor-buffer.js +300 -0
- package/dist/tui/components/editor-paste.d.ts +29 -0
- package/dist/tui/components/editor-paste.js +64 -0
- package/dist/tui/components/editor.d.ts +114 -0
- package/dist/tui/components/editor.js +530 -0
- package/dist/tui/components/key-value.d.ts +29 -0
- package/dist/tui/components/key-value.js +48 -0
- package/dist/tui/components/loader.d.ts +35 -0
- package/dist/tui/components/loader.js +71 -0
- package/dist/tui/components/markdown.d.ts +90 -0
- package/dist/tui/components/markdown.js +462 -0
- package/dist/tui/components/meter.d.ts +29 -0
- package/dist/tui/components/meter.js +47 -0
- package/dist/tui/components/overlay.d.ts +24 -0
- package/dist/tui/components/overlay.js +52 -0
- package/dist/tui/components/select-list.d.ts +57 -0
- package/dist/tui/components/select-list.js +179 -0
- package/dist/tui/components/spacer.d.ts +11 -0
- package/dist/tui/components/spacer.js +16 -0
- package/dist/tui/components/text.d.ts +30 -0
- package/dist/tui/components/text.js +65 -0
- package/dist/tui/keybindings.d.ts +66 -0
- package/dist/tui/keybindings.js +120 -0
- package/dist/tui/keys.d.ts +38 -0
- package/dist/tui/keys.js +275 -0
- package/dist/tui/stdin-buffer.d.ts +41 -0
- package/dist/tui/stdin-buffer.js +206 -0
- package/dist/tui/terminal.d.ts +79 -0
- package/dist/tui/terminal.js +153 -0
- package/dist/tui/theme.d.ts +31 -0
- package/dist/tui/theme.js +199 -0
- package/dist/tui/tui.d.ts +103 -0
- package/dist/tui/tui.js +363 -0
- package/dist/tui/vt-screen.d.ts +46 -0
- package/dist/tui/vt-screen.js +251 -0
- package/dist/tui.d.ts +30 -0
- package/dist/tui.js +31 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +24 -0
- package/docs/codemode.md +102 -0
- package/docs/hooks.md +196 -0
- package/docs/host-api.md +161 -0
- package/docs/providers.md +531 -0
- package/docs/rpc.md +253 -0
- package/docs/session-format.md +119 -0
- package/docs/tui.md +170 -0
- package/package.json +101 -4
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 投影(设计 §8、§9):活动分支 → 进上下文的消息。[B2]
|
|
3
|
+
*
|
|
4
|
+
* - `buildContextEntries(branch)`:若路径上有 compaction,取最新一条:先放 compaction,再放
|
|
5
|
+
* `firstKeptEntryId` 到 compaction 之间的非 system 条目,再放 compaction 之后的条目。
|
|
6
|
+
* - `buildProjection(branch)`:对上一步的条目套用 `context_edit`(同一目标在活动分支上最新一条赢;
|
|
7
|
+
* `null` 剔除,字符串只换内容、保留角色与元数据),并把每个条目映射为 `AgentMessage`。
|
|
8
|
+
* 有 compaction 时,compaction 之前的全部 system 消息折成一条完整的 system 检查点放在最前
|
|
9
|
+
* (会话条目里不另存检查点,重放即可得到)。
|
|
10
|
+
* - `buildContext(branch)`:投影消息 + 路径上最近的模型 / 思考级别选择。
|
|
11
|
+
*/
|
|
12
|
+
function parseTime(iso) {
|
|
13
|
+
const value = Date.parse(iso);
|
|
14
|
+
return Number.isFinite(value) ? value : 0;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 条目 → 上下文消息;不进上下文的条目返回 undefined(model_change、label、custom、
|
|
18
|
+
* 第三波的 `usage` 条目等)。
|
|
19
|
+
*/
|
|
20
|
+
export function entryToMessage(entry) {
|
|
21
|
+
switch (entry.type) {
|
|
22
|
+
case "message":
|
|
23
|
+
return entry.message;
|
|
24
|
+
case "custom_message": {
|
|
25
|
+
const message = {
|
|
26
|
+
role: "custom",
|
|
27
|
+
customType: entry.customType,
|
|
28
|
+
content: entry.content,
|
|
29
|
+
display: entry.display,
|
|
30
|
+
timestamp: parseTime(entry.timestamp),
|
|
31
|
+
};
|
|
32
|
+
if (entry.details !== undefined)
|
|
33
|
+
message.details = entry.details;
|
|
34
|
+
return message;
|
|
35
|
+
}
|
|
36
|
+
case "branch_summary":
|
|
37
|
+
return {
|
|
38
|
+
role: "branchSummary",
|
|
39
|
+
summary: entry.summary,
|
|
40
|
+
fromId: entry.fromId,
|
|
41
|
+
timestamp: parseTime(entry.timestamp),
|
|
42
|
+
};
|
|
43
|
+
case "compaction":
|
|
44
|
+
return {
|
|
45
|
+
role: "compactionSummary",
|
|
46
|
+
summary: entry.summary,
|
|
47
|
+
tokensBefore: entry.tokensBefore,
|
|
48
|
+
timestamp: parseTime(entry.timestamp),
|
|
49
|
+
};
|
|
50
|
+
case "usage":
|
|
51
|
+
default:
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function isSystemEntry(entry) {
|
|
56
|
+
return entry.type === "message" && entry.message.role === "system";
|
|
57
|
+
}
|
|
58
|
+
function latestCompactionIndex(branch) {
|
|
59
|
+
for (let i = branch.length - 1; i >= 0; i--)
|
|
60
|
+
if (branch[i]?.type === "compaction")
|
|
61
|
+
return i;
|
|
62
|
+
return -1;
|
|
63
|
+
}
|
|
64
|
+
export function buildContextEntries(branch) {
|
|
65
|
+
const at = latestCompactionIndex(branch);
|
|
66
|
+
if (at < 0)
|
|
67
|
+
return [...branch];
|
|
68
|
+
const compaction = branch[at];
|
|
69
|
+
const keptStart = branch.findIndex((entry) => entry.id === compaction.firstKeptEntryId);
|
|
70
|
+
const kept = keptStart >= 0 && keptStart < at
|
|
71
|
+
? branch.slice(keptStart, at).filter((entry) => !isSystemEntry(entry))
|
|
72
|
+
: [];
|
|
73
|
+
return [compaction, ...kept, ...branch.slice(at + 1)];
|
|
74
|
+
}
|
|
75
|
+
/** 活动分支上每个目标最新的 context_edit。 */
|
|
76
|
+
export function collectContextEdits(branch) {
|
|
77
|
+
const edits = new Map();
|
|
78
|
+
for (const entry of branch)
|
|
79
|
+
if (entry.type === "context_edit")
|
|
80
|
+
edits.set(entry.targetId, entry);
|
|
81
|
+
return edits;
|
|
82
|
+
}
|
|
83
|
+
/** 用替换文本换掉消息内容,保留角色与元数据。 */
|
|
84
|
+
export function applyReplacement(message, text) {
|
|
85
|
+
switch (message.role) {
|
|
86
|
+
case "user":
|
|
87
|
+
case "toolResult":
|
|
88
|
+
case "custom":
|
|
89
|
+
return { ...message, content: text };
|
|
90
|
+
case "assistant": {
|
|
91
|
+
const replaced = { ...message, content: [{ type: "text", text }] };
|
|
92
|
+
return replaced;
|
|
93
|
+
}
|
|
94
|
+
case "compactionSummary":
|
|
95
|
+
case "branchSummary":
|
|
96
|
+
return { ...message, summary: text };
|
|
97
|
+
case "system":
|
|
98
|
+
return message;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** 依次应用转录里的 system 消息(首条全量,之后是补丁);没有 system 消息 → undefined。 */
|
|
102
|
+
export function replaySystem(messages) {
|
|
103
|
+
let found = false;
|
|
104
|
+
const sections = new Map();
|
|
105
|
+
const tools = new Map();
|
|
106
|
+
for (const message of messages) {
|
|
107
|
+
if (message.role !== "system")
|
|
108
|
+
continue;
|
|
109
|
+
found = true;
|
|
110
|
+
for (const [name, text] of Object.entries(message.sections)) {
|
|
111
|
+
if (text === null)
|
|
112
|
+
sections.delete(name);
|
|
113
|
+
else
|
|
114
|
+
sections.set(name, text);
|
|
115
|
+
}
|
|
116
|
+
for (const name of message.toolsRemoved ?? [])
|
|
117
|
+
tools.delete(name);
|
|
118
|
+
for (const tool of message.toolsAdded ?? []) {
|
|
119
|
+
tools.delete(tool.name);
|
|
120
|
+
tools.set(tool.name, tool);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!found)
|
|
124
|
+
return undefined;
|
|
125
|
+
return { sections: Object.fromEntries(sections), tools: [...tools.values()] };
|
|
126
|
+
}
|
|
127
|
+
/** 把重放结果写成一条完整的 system 消息。 */
|
|
128
|
+
export function systemCheckpoint(state, timestamp) {
|
|
129
|
+
const message = { role: "system", sections: { ...state.sections }, timestamp };
|
|
130
|
+
if (state.tools.length > 0)
|
|
131
|
+
message.toolsAdded = state.tools.map((tool) => ({ ...tool }));
|
|
132
|
+
return message;
|
|
133
|
+
}
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// 投影
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
export function buildProjection(branch) {
|
|
138
|
+
const edits = collectContextEdits(branch);
|
|
139
|
+
const at = latestCompactionIndex(branch);
|
|
140
|
+
const compaction = at >= 0 ? branch[at] : undefined;
|
|
141
|
+
const items = [];
|
|
142
|
+
if (compaction !== undefined) {
|
|
143
|
+
const before = [];
|
|
144
|
+
for (const entry of branch.slice(0, at)) {
|
|
145
|
+
if (entry.type === "message" && entry.message.role === "system")
|
|
146
|
+
before.push(entry.message);
|
|
147
|
+
}
|
|
148
|
+
const state = replaySystem(before);
|
|
149
|
+
if (state !== undefined) {
|
|
150
|
+
items.push({
|
|
151
|
+
entry: compaction,
|
|
152
|
+
message: systemCheckpoint(state, parseTime(compaction.timestamp)),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const entry of buildContextEntries(branch)) {
|
|
157
|
+
let message = entryToMessage(entry);
|
|
158
|
+
if (message === undefined)
|
|
159
|
+
continue;
|
|
160
|
+
const edit = edits.get(entry.id);
|
|
161
|
+
if (edit !== undefined) {
|
|
162
|
+
if (edit.replacement === null)
|
|
163
|
+
continue;
|
|
164
|
+
message = applyReplacement(message, edit.replacement);
|
|
165
|
+
}
|
|
166
|
+
items.push({ entry, message });
|
|
167
|
+
}
|
|
168
|
+
return { items, messages: items.map((item) => item.message), compaction };
|
|
169
|
+
}
|
|
170
|
+
export function buildContext(branch) {
|
|
171
|
+
let model;
|
|
172
|
+
let thinkingLevel;
|
|
173
|
+
for (const entry of branch) {
|
|
174
|
+
if (entry.type === "model_change")
|
|
175
|
+
model = {
|
|
176
|
+
provider: entry.provider,
|
|
177
|
+
id: entry.modelId,
|
|
178
|
+
...(entry.channel !== undefined ? { channel: entry.channel } : {}),
|
|
179
|
+
};
|
|
180
|
+
else if (entry.type === "thinking_level_change")
|
|
181
|
+
thinkingLevel = entry.thinkingLevel;
|
|
182
|
+
}
|
|
183
|
+
return { messages: buildProjection(branch).messages, model, thinkingLevel };
|
|
184
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话文件的磁盘层(设计 §8):目录编码、文件名、JSONL 读写、原子追加、单写者锁、trash。[B2]
|
|
3
|
+
*
|
|
4
|
+
* - 位置 `<sessionsRoot>/<编码 cwd>/<ISO>_<uuid>.jsonl`;编码 cwd = 去首分隔符、`/ \ :` 换 `-`。
|
|
5
|
+
* - 只追加:每条记录一行 JSON + LF,用 O_APPEND 写(单写者由锁保证)。
|
|
6
|
+
* - 打开时若末行是半行(进程在写入中途崩溃),截掉半行再继续追加;中间行损坏 → session_corrupt。
|
|
7
|
+
* - 锁:`<file>.lock`(O_EXCL 创建,内容为 pid);持锁进程已不在 → 视为陈旧锁并接管。
|
|
8
|
+
* - prune:移到 `<sessionsRoot>/.trash/`,文件名前缀删除时间;`purgeTrash` 清理超过 7 天的。
|
|
9
|
+
*/
|
|
10
|
+
import type { SessionLine } from "./types.js";
|
|
11
|
+
export declare const SESSION_FILE_SUFFIX = ".jsonl";
|
|
12
|
+
export declare const TRASH_DIR_NAME = ".trash";
|
|
13
|
+
export declare const TRASH_RETENTION_MS: number;
|
|
14
|
+
/** 去首分隔符,`/ \ :` 换 `-`。 */
|
|
15
|
+
export declare function encodeCwd(cwd: string): string;
|
|
16
|
+
export declare function sessionDirForCwd(sessionsRoot: string, cwd: string): string;
|
|
17
|
+
/** `2026-10-02T05-23-11-123Z_<id>.jsonl`(ISO 里的 `:` 与 `.` 换 `-`,Windows 可用)。 */
|
|
18
|
+
export declare function sessionFileName(date: Date, id: string): string;
|
|
19
|
+
/** 从文件名取会话 id(不符合命名时 undefined)。 */
|
|
20
|
+
export declare function sessionIdFromFileName(file: string): string | undefined;
|
|
21
|
+
export interface ReadResult {
|
|
22
|
+
lines: SessionLine[];
|
|
23
|
+
/** 末尾半行被截掉时为 true。 */
|
|
24
|
+
repairedTail: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 读并解析 JSONL。`repair: true` 时把末尾半行从文件里截掉(只截最后一行,且只在它无法解析时)。
|
|
28
|
+
*/
|
|
29
|
+
export declare function readSessionLines(file: string, options?: {
|
|
30
|
+
repair?: boolean;
|
|
31
|
+
}): ReadResult;
|
|
32
|
+
/** 追加若干行(一次 write,O_APPEND)。 */
|
|
33
|
+
export declare function appendLines(file: string, lines: readonly SessionLine[]): void;
|
|
34
|
+
/** 新建文件:先写临时文件再 rename,避免出现只有半个头的文件。 */
|
|
35
|
+
export declare function writeNewSessionFile(file: string, lines: readonly SessionLine[]): void;
|
|
36
|
+
export interface SessionLock {
|
|
37
|
+
readonly path: string;
|
|
38
|
+
release(): void;
|
|
39
|
+
}
|
|
40
|
+
/** 单写者锁。同一进程内对同一文件重复加锁也视为冲突。 */
|
|
41
|
+
export declare function acquireLock(file: string): SessionLock;
|
|
42
|
+
/** 目录下的会话文件(按文件名倒序 = 最新在前)。 */
|
|
43
|
+
export declare function listSessionFiles(dir: string): string[];
|
|
44
|
+
/** 移到 `<sessionsRoot>/.trash/<删除时间毫秒>__<编码目录>__<文件名>`,返回新路径。 */
|
|
45
|
+
export declare function trashSession(file: string, sessionsRoot: string, now?: number): string;
|
|
46
|
+
/** 删除 trash 中超过保留期的文件,返回删除的路径。 */
|
|
47
|
+
export declare function purgeTrash(sessionsRoot: string, now?: number, retentionMs?: number): string[];
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话文件的磁盘层(设计 §8):目录编码、文件名、JSONL 读写、原子追加、单写者锁、trash。[B2]
|
|
3
|
+
*
|
|
4
|
+
* - 位置 `<sessionsRoot>/<编码 cwd>/<ISO>_<uuid>.jsonl`;编码 cwd = 去首分隔符、`/ \ :` 换 `-`。
|
|
5
|
+
* - 只追加:每条记录一行 JSON + LF,用 O_APPEND 写(单写者由锁保证)。
|
|
6
|
+
* - 打开时若末行是半行(进程在写入中途崩溃),截掉半行再继续追加;中间行损坏 → session_corrupt。
|
|
7
|
+
* - 锁:`<file>.lock`(O_EXCL 创建,内容为 pid);持锁进程已不在 → 视为陈旧锁并接管。
|
|
8
|
+
* - prune:移到 `<sessionsRoot>/.trash/`,文件名前缀删除时间;`purgeTrash` 清理超过 7 天的。
|
|
9
|
+
*/
|
|
10
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, truncateSync, writeFileSync, writeSync, } from "node:fs";
|
|
11
|
+
import { basename, dirname, join } from "node:path";
|
|
12
|
+
import { AmaError } from "../errors.js";
|
|
13
|
+
export const SESSION_FILE_SUFFIX = ".jsonl";
|
|
14
|
+
export const TRASH_DIR_NAME = ".trash";
|
|
15
|
+
export const TRASH_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
16
|
+
/** 去首分隔符,`/ \ :` 换 `-`。 */
|
|
17
|
+
export function encodeCwd(cwd) {
|
|
18
|
+
return cwd.replace(/^[/\\]+/, "").replace(/[/\\:]/g, "-");
|
|
19
|
+
}
|
|
20
|
+
export function sessionDirForCwd(sessionsRoot, cwd) {
|
|
21
|
+
return join(sessionsRoot, encodeCwd(cwd));
|
|
22
|
+
}
|
|
23
|
+
/** `2026-10-02T05-23-11-123Z_<id>.jsonl`(ISO 里的 `:` 与 `.` 换 `-`,Windows 可用)。 */
|
|
24
|
+
export function sessionFileName(date, id) {
|
|
25
|
+
return `${date.toISOString().replace(/[:.]/g, "-")}_${id}${SESSION_FILE_SUFFIX}`;
|
|
26
|
+
}
|
|
27
|
+
/** 从文件名取会话 id(不符合命名时 undefined)。 */
|
|
28
|
+
export function sessionIdFromFileName(file) {
|
|
29
|
+
const name = basename(file);
|
|
30
|
+
if (!name.endsWith(SESSION_FILE_SUFFIX))
|
|
31
|
+
return undefined;
|
|
32
|
+
const stem = name.slice(0, -SESSION_FILE_SUFFIX.length);
|
|
33
|
+
const at = stem.indexOf("_");
|
|
34
|
+
return at < 0 ? undefined : stem.slice(at + 1);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 读并解析 JSONL。`repair: true` 时把末尾半行从文件里截掉(只截最后一行,且只在它无法解析时)。
|
|
38
|
+
*/
|
|
39
|
+
export function readSessionLines(file, options = {}) {
|
|
40
|
+
let text;
|
|
41
|
+
try {
|
|
42
|
+
text = readFileSync(file, "utf8");
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new AmaError("session_not_found", `cannot read session file ${file}`, { cause: error });
|
|
46
|
+
}
|
|
47
|
+
const rawLines = text.split("\n");
|
|
48
|
+
const endsWithNewline = text.endsWith("\n");
|
|
49
|
+
if (endsWithNewline)
|
|
50
|
+
rawLines.pop();
|
|
51
|
+
const lines = [];
|
|
52
|
+
let repairedTail = false;
|
|
53
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
54
|
+
const raw = (rawLines[i] ?? "").replace(/\r$/, "");
|
|
55
|
+
if (raw.trim() === "")
|
|
56
|
+
continue;
|
|
57
|
+
try {
|
|
58
|
+
lines.push(JSON.parse(raw));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const isLast = i === rawLines.length - 1;
|
|
62
|
+
if (isLast && !endsWithNewline) {
|
|
63
|
+
repairedTail = true;
|
|
64
|
+
if (options.repair === true) {
|
|
65
|
+
const keep = Buffer.byteLength(rawLines.slice(0, i).join("\n"), "utf8") + (i > 0 ? 1 : 0);
|
|
66
|
+
truncateSync(file, keep);
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
throw new AmaError("session_corrupt", `${file}:${i + 1}: invalid JSON line`, {
|
|
71
|
+
cause: error,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!repairedTail && !endsWithNewline && text.length > 0 && options.repair === true) {
|
|
76
|
+
// 最后一行完整但缺 LF:补上,保证后续追加另起一行。
|
|
77
|
+
appendRaw(file, "\n");
|
|
78
|
+
}
|
|
79
|
+
return { lines, repairedTail };
|
|
80
|
+
}
|
|
81
|
+
function appendRaw(file, text) {
|
|
82
|
+
const fd = openSync(file, "a");
|
|
83
|
+
try {
|
|
84
|
+
writeSync(fd, text);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
closeSync(fd);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** 追加若干行(一次 write,O_APPEND)。 */
|
|
91
|
+
export function appendLines(file, lines) {
|
|
92
|
+
if (lines.length === 0)
|
|
93
|
+
return;
|
|
94
|
+
appendRaw(file, lines.map((line) => JSON.stringify(line)).join("\n") + "\n");
|
|
95
|
+
}
|
|
96
|
+
/** 新建文件:先写临时文件再 rename,避免出现只有半个头的文件。 */
|
|
97
|
+
export function writeNewSessionFile(file, lines) {
|
|
98
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
99
|
+
if (existsSync(file)) {
|
|
100
|
+
throw new AmaError("session_exists", `session file already exists: ${file}`);
|
|
101
|
+
}
|
|
102
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
103
|
+
writeFileSync(tmp, lines.map((line) => JSON.stringify(line)).join("\n") + "\n", { mode: 0o600 });
|
|
104
|
+
renameSync(tmp, file);
|
|
105
|
+
}
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// 锁
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
const heldLocks = new Set();
|
|
110
|
+
let exitHandlerInstalled = false;
|
|
111
|
+
function installExitHandler() {
|
|
112
|
+
if (exitHandlerInstalled)
|
|
113
|
+
return;
|
|
114
|
+
exitHandlerInstalled = true;
|
|
115
|
+
process.once("exit", () => {
|
|
116
|
+
for (const lock of heldLocks)
|
|
117
|
+
rmSync(lock, { force: true });
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function pidAlive(pid) {
|
|
121
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
122
|
+
return false;
|
|
123
|
+
try {
|
|
124
|
+
process.kill(pid, 0);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return error.code === "EPERM";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** 单写者锁。同一进程内对同一文件重复加锁也视为冲突。 */
|
|
132
|
+
export function acquireLock(file) {
|
|
133
|
+
const path = `${file}.lock`;
|
|
134
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
135
|
+
try {
|
|
136
|
+
const fd = openSync(path, "wx", 0o600);
|
|
137
|
+
writeSync(fd, String(process.pid));
|
|
138
|
+
closeSync(fd);
|
|
139
|
+
heldLocks.add(path);
|
|
140
|
+
installExitHandler();
|
|
141
|
+
let released = false;
|
|
142
|
+
return {
|
|
143
|
+
path,
|
|
144
|
+
release() {
|
|
145
|
+
if (released)
|
|
146
|
+
return;
|
|
147
|
+
released = true;
|
|
148
|
+
heldLocks.delete(path);
|
|
149
|
+
rmSync(path, { force: true });
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error.code !== "EEXIST")
|
|
155
|
+
throw error;
|
|
156
|
+
let owner = Number.NaN;
|
|
157
|
+
try {
|
|
158
|
+
owner = Number.parseInt(readFileSync(path, "utf8").trim(), 10);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// 读不到当作陈旧
|
|
162
|
+
}
|
|
163
|
+
const stale = heldLocks.has(path) ? false : !pidAlive(owner) || owner === process.pid;
|
|
164
|
+
if (!stale || attempt > 0) {
|
|
165
|
+
throw new AmaError("session_locked", `session file is in use by pid ${owner}: ${file}`);
|
|
166
|
+
}
|
|
167
|
+
rmSync(path, { force: true });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw new AmaError("session_locked", `cannot lock session file ${file}`);
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// 列表与 trash
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
/** 目录下的会话文件(按文件名倒序 = 最新在前)。 */
|
|
176
|
+
export function listSessionFiles(dir) {
|
|
177
|
+
if (!existsSync(dir))
|
|
178
|
+
return [];
|
|
179
|
+
return readdirSync(dir)
|
|
180
|
+
.filter((name) => name.endsWith(SESSION_FILE_SUFFIX))
|
|
181
|
+
.sort()
|
|
182
|
+
.reverse()
|
|
183
|
+
.map((name) => join(dir, name));
|
|
184
|
+
}
|
|
185
|
+
/** 移到 `<sessionsRoot>/.trash/<删除时间毫秒>__<编码目录>__<文件名>`,返回新路径。 */
|
|
186
|
+
export function trashSession(file, sessionsRoot, now = Date.now()) {
|
|
187
|
+
const trash = join(sessionsRoot, TRASH_DIR_NAME);
|
|
188
|
+
mkdirSync(trash, { recursive: true, mode: 0o700 });
|
|
189
|
+
const target = join(trash, `${now}__${basename(dirname(file))}__${basename(file)}`);
|
|
190
|
+
renameSync(file, target);
|
|
191
|
+
rmSync(`${file}.lock`, { force: true });
|
|
192
|
+
return target;
|
|
193
|
+
}
|
|
194
|
+
/** 删除 trash 中超过保留期的文件,返回删除的路径。 */
|
|
195
|
+
export function purgeTrash(sessionsRoot, now = Date.now(), retentionMs = TRASH_RETENTION_MS) {
|
|
196
|
+
const trash = join(sessionsRoot, TRASH_DIR_NAME);
|
|
197
|
+
if (!existsSync(trash))
|
|
198
|
+
return [];
|
|
199
|
+
const removed = [];
|
|
200
|
+
for (const name of readdirSync(trash)) {
|
|
201
|
+
const full = join(trash, name);
|
|
202
|
+
const stamp = Number.parseInt(name.split("__")[0] ?? "", 10);
|
|
203
|
+
const deletedAt = Number.isFinite(stamp) ? stamp : statSync(full).mtimeMs;
|
|
204
|
+
if (now - deletedAt > retentionMs) {
|
|
205
|
+
rmSync(full, { force: true });
|
|
206
|
+
removed.push(full);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return removed;
|
|
210
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话条目树(设计 §8):按 id 索引、根 → 叶子路径、公共祖先、树形视图。[B2]
|
|
3
|
+
*
|
|
4
|
+
* 条目以 parentId 串成树;多根是允许的(`setLeaf(null)` 后再追加)。所有函数都是纯函数,
|
|
5
|
+
* 输入是文件顺序的条目数组。
|
|
6
|
+
*/
|
|
7
|
+
import type { SessionEntry, SessionTreeNode } from "./types.js";
|
|
8
|
+
export type EntryIndex = ReadonlyMap<string, SessionEntry>;
|
|
9
|
+
export declare function indexEntries(entries: readonly SessionEntry[]): Map<string, SessionEntry>;
|
|
10
|
+
/** 根 → leafId 的路径(含 leaf);leafId 为 null 或找不到时返回空数组。遇环即停。 */
|
|
11
|
+
export declare function pathToRoot(index: EntryIndex, leafId: string | null): SessionEntry[];
|
|
12
|
+
/** 两个叶子的最深公共祖先 id(无公共祖先 → null)。 */
|
|
13
|
+
export declare function commonAncestor(index: EntryIndex, a: string | null, b: string | null): string | null;
|
|
14
|
+
/** 从 `fromId`(不含)往上直到 `ancestorId`(不含)的条目,从旧到新。 */
|
|
15
|
+
export declare function entriesBetween(index: EntryIndex, ancestorId: string | null, fromId: string | null): SessionEntry[];
|
|
16
|
+
/** label 条目:同一目标最新一条赢;label 缺省 = 清除。 */
|
|
17
|
+
export declare function collectLabels(entries: readonly SessionEntry[]): Map<string, string>;
|
|
18
|
+
/** 文件顺序建树;孤儿(parentId 指向不存在的条目)当作根。 */
|
|
19
|
+
export declare function buildTree(entries: readonly SessionEntry[]): SessionTreeNode[];
|
|
20
|
+
/** 叶子条目(没有子条目的)id,文件顺序。 */
|
|
21
|
+
export declare function leafIds(entries: readonly SessionEntry[]): string[];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话条目树(设计 §8):按 id 索引、根 → 叶子路径、公共祖先、树形视图。[B2]
|
|
3
|
+
*
|
|
4
|
+
* 条目以 parentId 串成树;多根是允许的(`setLeaf(null)` 后再追加)。所有函数都是纯函数,
|
|
5
|
+
* 输入是文件顺序的条目数组。
|
|
6
|
+
*/
|
|
7
|
+
export function indexEntries(entries) {
|
|
8
|
+
const map = new Map();
|
|
9
|
+
for (const entry of entries)
|
|
10
|
+
map.set(entry.id, entry);
|
|
11
|
+
return map;
|
|
12
|
+
}
|
|
13
|
+
/** 根 → leafId 的路径(含 leaf);leafId 为 null 或找不到时返回空数组。遇环即停。 */
|
|
14
|
+
export function pathToRoot(index, leafId) {
|
|
15
|
+
const path = [];
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
let current = leafId === null ? undefined : index.get(leafId);
|
|
18
|
+
while (current !== undefined && !seen.has(current.id)) {
|
|
19
|
+
seen.add(current.id);
|
|
20
|
+
path.push(current);
|
|
21
|
+
current = current.parentId === null ? undefined : index.get(current.parentId);
|
|
22
|
+
}
|
|
23
|
+
return path.reverse();
|
|
24
|
+
}
|
|
25
|
+
/** 两个叶子的最深公共祖先 id(无公共祖先 → null)。 */
|
|
26
|
+
export function commonAncestor(index, a, b) {
|
|
27
|
+
const ancestorsOfA = new Set(pathToRoot(index, a).map((entry) => entry.id));
|
|
28
|
+
const pathB = pathToRoot(index, b);
|
|
29
|
+
for (let i = pathB.length - 1; i >= 0; i--) {
|
|
30
|
+
const entry = pathB[i];
|
|
31
|
+
if (entry !== undefined && ancestorsOfA.has(entry.id))
|
|
32
|
+
return entry.id;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/** 从 `fromId`(不含)往上直到 `ancestorId`(不含)的条目,从旧到新。 */
|
|
37
|
+
export function entriesBetween(index, ancestorId, fromId) {
|
|
38
|
+
const path = pathToRoot(index, fromId);
|
|
39
|
+
if (ancestorId === null)
|
|
40
|
+
return path;
|
|
41
|
+
const at = path.findIndex((entry) => entry.id === ancestorId);
|
|
42
|
+
return at < 0 ? path : path.slice(at + 1);
|
|
43
|
+
}
|
|
44
|
+
/** label 条目:同一目标最新一条赢;label 缺省 = 清除。 */
|
|
45
|
+
export function collectLabels(entries) {
|
|
46
|
+
const labels = new Map();
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (entry.type !== "label")
|
|
49
|
+
continue;
|
|
50
|
+
if (entry.label === undefined || entry.label === "")
|
|
51
|
+
labels.delete(entry.targetId);
|
|
52
|
+
else
|
|
53
|
+
labels.set(entry.targetId, entry.label);
|
|
54
|
+
}
|
|
55
|
+
return labels;
|
|
56
|
+
}
|
|
57
|
+
/** 文件顺序建树;孤儿(parentId 指向不存在的条目)当作根。 */
|
|
58
|
+
export function buildTree(entries) {
|
|
59
|
+
const labels = collectLabels(entries);
|
|
60
|
+
const nodes = new Map();
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const node = { entry, children: [] };
|
|
63
|
+
const label = labels.get(entry.id);
|
|
64
|
+
if (label !== undefined)
|
|
65
|
+
node.label = label;
|
|
66
|
+
nodes.set(entry.id, node);
|
|
67
|
+
}
|
|
68
|
+
const roots = [];
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const node = nodes.get(entry.id);
|
|
71
|
+
if (node === undefined)
|
|
72
|
+
continue;
|
|
73
|
+
const parent = entry.parentId === null ? undefined : nodes.get(entry.parentId);
|
|
74
|
+
if (parent === undefined || parent === node)
|
|
75
|
+
roots.push(node);
|
|
76
|
+
else
|
|
77
|
+
parent.children.push(node);
|
|
78
|
+
}
|
|
79
|
+
return roots;
|
|
80
|
+
}
|
|
81
|
+
/** 叶子条目(没有子条目的)id,文件顺序。 */
|
|
82
|
+
export function leafIds(entries) {
|
|
83
|
+
const hasChild = new Set();
|
|
84
|
+
for (const entry of entries)
|
|
85
|
+
if (entry.parentId !== null)
|
|
86
|
+
hasChild.add(entry.parentId);
|
|
87
|
+
return entries.filter((entry) => !hasChild.has(entry.id)).map((entry) => entry.id);
|
|
88
|
+
}
|