@ai-agent-forge/agent-forge 0.88.0 → 0.88.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/bundled-plugins/agent-loop-policy/agent-forge.json +19 -0
- package/bundled-plugins/agent-loop-policy/dist/entry.d.ts +28 -0
- package/bundled-plugins/agent-loop-policy/dist/entry.d.ts.map +1 -0
- package/bundled-plugins/agent-loop-policy/dist/entry.js +69 -0
- package/bundled-plugins/agent-loop-policy/dist/entry.js.map +1 -0
- package/bundled-plugins/agent-loop-policy/dist/loop-guard.d.ts +199 -0
- package/bundled-plugins/agent-loop-policy/dist/loop-guard.d.ts.map +1 -0
- package/bundled-plugins/agent-loop-policy/dist/loop-guard.js +208 -0
- package/bundled-plugins/agent-loop-policy/dist/loop-guard.js.map +1 -0
- package/bundled-plugins/agent-loop-policy/dist/run-loop-strategy.d.ts +17 -0
- package/bundled-plugins/agent-loop-policy/dist/run-loop-strategy.d.ts.map +1 -0
- package/bundled-plugins/agent-loop-policy/dist/run-loop-strategy.js +18 -0
- package/bundled-plugins/agent-loop-policy/dist/run-loop-strategy.js.map +1 -0
- package/bundled-plugins/agent-loop-policy/plugin.json +7 -0
- package/bundled-plugins/coding-tools/agent-forge.json +19 -0
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/bash.d.ts +5 -44
- package/bundled-plugins/coding-tools/dist/bash.d.ts.map +1 -0
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/bash.js +15 -119
- package/bundled-plugins/coding-tools/dist/bash.js.map +1 -0
- package/bundled-plugins/coding-tools/dist/capability.d.ts +25 -0
- package/bundled-plugins/coding-tools/dist/capability.d.ts.map +1 -0
- package/bundled-plugins/coding-tools/dist/capability.js +98 -0
- package/bundled-plugins/coding-tools/dist/capability.js.map +1 -0
- package/bundled-plugins/coding-tools/dist/config.d.ts +8 -0
- package/bundled-plugins/coding-tools/dist/config.d.ts.map +1 -0
- package/bundled-plugins/coding-tools/dist/config.js +8 -0
- package/bundled-plugins/coding-tools/dist/config.js.map +1 -0
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/edit.d.ts +3 -3
- package/bundled-plugins/coding-tools/dist/edit.d.ts.map +1 -0
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/edit.js +12 -10
- package/bundled-plugins/coding-tools/dist/edit.js.map +1 -0
- package/bundled-plugins/coding-tools/dist/entry.d.ts +14 -0
- package/bundled-plugins/coding-tools/dist/entry.d.ts.map +1 -0
- package/bundled-plugins/coding-tools/dist/entry.js +23 -0
- package/bundled-plugins/coding-tools/dist/entry.js.map +1 -0
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/find.d.ts +4 -4
- package/bundled-plugins/coding-tools/dist/find.d.ts.map +1 -0
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/find.js +10 -8
- package/bundled-plugins/coding-tools/dist/find.js.map +1 -0
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/grep.d.ts +3 -3
- package/bundled-plugins/coding-tools/dist/grep.d.ts.map +1 -0
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/grep.js +10 -8
- package/bundled-plugins/coding-tools/dist/grep.js.map +1 -0
- package/bundled-plugins/coding-tools/dist/index.d.ts +17 -0
- package/bundled-plugins/coding-tools/dist/index.d.ts.map +1 -0
- package/bundled-plugins/coding-tools/dist/index.js +17 -0
- package/bundled-plugins/coding-tools/dist/index.js.map +1 -0
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/ls.d.ts +3 -3
- package/bundled-plugins/coding-tools/dist/ls.d.ts.map +1 -0
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/ls.js +8 -6
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/ls.js.map +1 -1
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/powershell.d.ts +2 -1
- package/bundled-plugins/coding-tools/dist/powershell.d.ts.map +1 -0
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/powershell.js +6 -3
- package/bundled-plugins/coding-tools/dist/powershell.js.map +1 -0
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/read.d.ts +3 -3
- package/bundled-plugins/coding-tools/dist/read.d.ts.map +1 -0
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/read.js +15 -13
- package/bundled-plugins/coding-tools/dist/read.js.map +1 -0
- package/bundled-plugins/coding-tools/dist/types.d.ts +74 -0
- package/bundled-plugins/coding-tools/dist/types.d.ts.map +1 -0
- package/bundled-plugins/coding-tools/dist/types.js +28 -0
- package/bundled-plugins/coding-tools/dist/types.js.map +1 -0
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/write.d.ts +2 -2
- package/bundled-plugins/coding-tools/dist/write.d.ts.map +1 -0
- package/{dist/core/tools → bundled-plugins/coding-tools/dist}/write.js +11 -9
- package/bundled-plugins/coding-tools/dist/write.js.map +1 -0
- package/bundled-plugins/coding-tools/plugin.json +10 -0
- package/bundled-plugins/compaction-policy/agent-forge.json +19 -0
- package/bundled-plugins/compaction-policy/dist/branch-summary-policy.d.ts +6 -0
- package/bundled-plugins/compaction-policy/dist/branch-summary-policy.d.ts.map +1 -0
- package/bundled-plugins/compaction-policy/dist/branch-summary-policy.js +19 -0
- package/bundled-plugins/compaction-policy/dist/branch-summary-policy.js.map +1 -0
- package/bundled-plugins/compaction-policy/dist/compaction-policy.d.ts +6 -0
- package/bundled-plugins/compaction-policy/dist/compaction-policy.d.ts.map +1 -0
- package/bundled-plugins/compaction-policy/dist/compaction-policy.js +38 -0
- package/bundled-plugins/compaction-policy/dist/compaction-policy.js.map +1 -0
- package/bundled-plugins/compaction-policy/dist/entry.d.ts +13 -0
- package/bundled-plugins/compaction-policy/dist/entry.d.ts.map +1 -0
- package/bundled-plugins/compaction-policy/dist/entry.js +23 -0
- package/bundled-plugins/compaction-policy/dist/entry.js.map +1 -0
- package/bundled-plugins/compaction-policy/plugin.json +10 -0
- package/bundled-plugins/mcp-http/dist/index.d.ts +8 -0
- package/bundled-plugins/mcp-http/dist/index.d.ts.map +1 -0
- package/bundled-plugins/mcp-http/dist/index.js +8 -0
- package/bundled-plugins/mcp-http/dist/index.js.map +1 -0
- package/bundled-plugins/mcp-http/dist/mcp-http.d.ts.map +1 -0
- package/{dist/plugins → bundled-plugins/mcp-http/dist}/mcp-http.js +1 -1
- package/bundled-plugins/mcp-http/dist/mcp-http.js.map +1 -0
- package/bundled-plugins/overflow-policy/agent-forge.json +19 -0
- package/bundled-plugins/overflow-policy/dist/entry.d.ts +16 -0
- package/bundled-plugins/overflow-policy/dist/entry.d.ts.map +1 -0
- package/bundled-plugins/overflow-policy/dist/entry.js +35 -0
- package/bundled-plugins/overflow-policy/dist/entry.js.map +1 -0
- package/bundled-plugins/overflow-policy/plugin.json +10 -0
- package/bundled-plugins/provider-failover/agent-forge.json +19 -0
- package/bundled-plugins/provider-failover/dist/entry.d.ts +8 -0
- package/bundled-plugins/provider-failover/dist/entry.d.ts.map +1 -0
- package/bundled-plugins/provider-failover/dist/entry.js +8 -0
- package/bundled-plugins/provider-failover/dist/entry.js.map +1 -0
- package/bundled-plugins/provider-failover/dist/provider-failover.d.ts +122 -0
- package/bundled-plugins/provider-failover/dist/provider-failover.d.ts.map +1 -0
- package/{dist/core/extensions → bundled-plugins/provider-failover/dist}/provider-failover.js +143 -56
- package/bundled-plugins/provider-failover/dist/provider-failover.js.map +1 -0
- package/bundled-plugins/provider-failover/plugin.json +7 -0
- package/bundled-plugins/retry-policy/agent-forge.json +19 -0
- package/bundled-plugins/retry-policy/dist/entry.d.ts +16 -0
- package/bundled-plugins/retry-policy/dist/entry.d.ts.map +1 -0
- package/bundled-plugins/retry-policy/dist/entry.js +34 -0
- package/bundled-plugins/retry-policy/dist/entry.js.map +1 -0
- package/bundled-plugins/retry-policy/plugin.json +10 -0
- package/dist/bundle/.build-manifest.json +109 -155
- package/dist/bundle/chunks/{anthropic-messages-ALDZBUKI.js → anthropic-messages-PLAKXR73.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-A2WJWZFB.js → azure-openai-responses-OUWQ2AZD.js} +1 -1
- package/dist/bundle/chunks/chunk-44I47ACN.js +13 -0
- package/dist/bundle/chunks/chunk-6F5JWGKE.js +4 -0
- package/dist/bundle/chunks/{chunk-CIX3FSBK.js → chunk-A3JYRWB6.js} +1 -1
- package/dist/bundle/chunks/{chunk-SQFG22WN.js → chunk-AC2FIS4G.js} +1 -1
- package/dist/bundle/chunks/chunk-HTC5K5SM.js +866 -0
- package/dist/bundle/chunks/{chunk-YJCQVXW5.js → chunk-HTEVUY7F.js} +1 -1
- package/dist/bundle/chunks/{chunk-Y7HXTASS.js → chunk-HV64ASMT.js} +1 -1
- package/dist/bundle/chunks/chunk-LSP24WNH.js +665 -0
- package/dist/bundle/chunks/{chunk-7ZY7IXZD.js → chunk-TRDNDS6A.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-NIB4WONV.js → google-generative-ai-DLII6DAT.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-DCPYUEUO.js → google-vertex-G7BZL7GM.js} +1 -1
- package/dist/bundle/chunks/image-resize-worker.js +4 -4
- package/dist/bundle/chunks/{mistral-conversations-OC6LA5XL.js → mistral-conversations-JXUK24ID.js} +1 -1
- package/dist/bundle/chunks/{openai-codex-responses-IO2X4Y7L.js → openai-codex-responses-5GSGFIPG.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-XYQQHETU.js → openai-completions-EI2I6DTG.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-DLIRN3CI.js → openai-responses-AUJQ3NWT.js} +1 -1
- package/dist/bundle/chunks/{pi-messages-E6RRYBO7.js → pi-messages-PWU2VJMR.js} +1 -1
- package/dist/bundle/chunks/virtual-modules-MRXD3RWG.js +5 -0
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/client.js +1 -1
- package/dist/bundle/index.js +3 -3
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/capabilities/builtin.d.ts +1 -1
- package/dist/capabilities/builtin.d.ts.map +1 -1
- package/dist/capabilities/builtin.js +5 -0
- package/dist/capabilities/builtin.js.map +1 -1
- package/dist/cli/plugin-command.d.ts.map +1 -1
- package/dist/cli/plugin-command.js +25 -3
- package/dist/cli/plugin-command.js.map +1 -1
- package/dist/config.d.ts +2 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -4
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-bash.d.ts +1 -1
- package/dist/core/agent-session-bash.d.ts.map +1 -1
- package/dist/core/agent-session-bash.js +1 -1
- package/dist/core/agent-session-bash.js.map +1 -1
- package/dist/core/agent-session-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-compaction.js +14 -1
- package/dist/core/agent-session-compaction.js.map +1 -1
- package/dist/core/agent-session-internal.d.ts +3 -1
- package/dist/core/agent-session-internal.d.ts.map +1 -1
- package/dist/core/agent-session-internal.js.map +1 -1
- package/dist/core/agent-session-loop.d.ts +4 -3
- package/dist/core/agent-session-loop.d.ts.map +1 -1
- package/dist/core/agent-session-loop.js +9 -6
- package/dist/core/agent-session-loop.js.map +1 -1
- package/dist/core/agent-session-retry.d.ts +1 -1
- package/dist/core/agent-session-retry.d.ts.map +1 -1
- package/dist/core/agent-session-retry.js +32 -3
- package/dist/core/agent-session-retry.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +2 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +31 -9
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session-store.d.ts.map +1 -1
- package/dist/core/agent-session-store.js +6 -1
- package/dist/core/agent-session-store.js.map +1 -1
- package/dist/core/agent-session-tools.d.ts.map +1 -1
- package/dist/core/agent-session-tools.js +18 -1
- package/dist/core/agent-session-tools.js.map +1 -1
- package/dist/core/agent-session.d.ts +34 -11
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +114 -38
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/extensions/bootstrap-agent-loop.d.ts +23 -151
- package/dist/core/extensions/bootstrap-agent-loop.d.ts.map +1 -1
- package/dist/core/extensions/bootstrap-agent-loop.js +28 -290
- package/dist/core/extensions/bootstrap-agent-loop.js.map +1 -1
- package/dist/core/extensions/capability-runtime-types.d.ts +5 -3
- package/dist/core/extensions/capability-runtime-types.d.ts.map +1 -1
- package/dist/core/extensions/capability-runtime-types.js.map +1 -1
- package/dist/core/extensions/capability-runtime.d.ts.map +1 -1
- package/dist/core/extensions/capability-runtime.js +42 -6
- package/dist/core/extensions/capability-runtime.js.map +1 -1
- package/dist/core/extensions/index.d.ts +12 -5
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +12 -5
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/virtual-modules.d.ts.map +1 -1
- package/dist/core/extensions/virtual-modules.js +65 -0
- package/dist/core/extensions/virtual-modules.js.map +1 -1
- package/dist/core/facade/coding-agent-facade.d.ts +1 -1
- package/dist/core/facade/coding-agent-facade.d.ts.map +1 -1
- package/dist/core/facade/coding-agent-facade.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/cli.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/cli.js +2 -0
- package/dist/core/i18n/locales/zh-cn/cli.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-contract-core.d.ts +1 -1
- package/dist/core/i18n/locales/zh-cn/core-contract-core.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-contract-core.js +1 -1
- package/dist/core/i18n/locales/zh-cn/core-contract-core.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-contract-extensions.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-contract-extensions.js +2 -4
- package/dist/core/i18n/locales/zh-cn/core-contract-extensions.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-errors.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-errors.js +3 -1
- package/dist/core/i18n/locales/zh-cn/core-errors.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/index.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/index.js +2 -0
- package/dist/core/i18n/locales/zh-cn/index.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/interactive-mode.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/interactive-mode.js +5 -0
- package/dist/core/i18n/locales/zh-cn/interactive-mode.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/session-cleanup.d.ts +7 -0
- package/dist/core/i18n/locales/zh-cn/session-cleanup.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/session-cleanup.js +15 -0
- package/dist/core/i18n/locales/zh-cn/session-cleanup.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/slash-commands.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/slash-commands.js +1 -0
- package/dist/core/i18n/locales/zh-cn/slash-commands.js.map +1 -1
- package/dist/core/sdk.d.ts +10 -3
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +287 -78
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-cleanup.d.ts +49 -0
- package/dist/core/session-cleanup.d.ts.map +1 -0
- package/dist/core/session-cleanup.js +159 -0
- package/dist/core/session-cleanup.js.map +1 -0
- package/dist/core/session-index.d.ts +75 -0
- package/dist/core/session-index.d.ts.map +1 -0
- package/dist/core/session-index.js +455 -0
- package/dist/core/session-index.js.map +1 -0
- package/dist/core/session-manager.d.ts +9 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +202 -47
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +5 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +12 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash-operations.d.ts +41 -0
- package/dist/core/tools/bash-operations.d.ts.map +1 -0
- package/dist/core/tools/bash-operations.js +113 -0
- package/dist/core/tools/bash-operations.js.map +1 -0
- package/dist/core/tools/capability-mount.d.ts +40 -0
- package/dist/core/tools/capability-mount.d.ts.map +1 -0
- package/dist/core/tools/capability-mount.js +112 -0
- package/dist/core/tools/capability-mount.js.map +1 -0
- package/dist/core/tools/index.d.ts +12 -36
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -117
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/readme-path.d.ts +3 -0
- package/dist/core/tools/readme-path.d.ts.map +1 -0
- package/dist/core/tools/readme-path.js +15 -0
- package/dist/core/tools/readme-path.js.map +1 -0
- package/dist/index.d.ts +3 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -39
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +3 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +27 -5
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.js +5 -1
- package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +9 -0
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +17 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +52 -25
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-run-group.d.ts +44 -0
- package/dist/modes/interactive/components/tool-run-group.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-run-group.js +121 -0
- package/dist/modes/interactive/components/tool-run-group.js.map +1 -0
- package/dist/modes/interactive/interactive-commands.d.ts +2 -0
- package/dist/modes/interactive/interactive-commands.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-commands.js +7 -0
- package/dist/modes/interactive/interactive-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +18 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +85 -3
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/plugins/ecosystem/cli.d.ts.map +1 -1
- package/dist/plugins/ecosystem/cli.js +20 -5
- package/dist/plugins/ecosystem/cli.js.map +1 -1
- package/dist/plugins/ecosystem/node-npm-registry-source.d.ts +7 -2
- package/dist/plugins/ecosystem/node-npm-registry-source.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-npm-registry-source.js +64 -9
- package/dist/plugins/ecosystem/node-npm-registry-source.js.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-acquisition-host.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-acquisition-host.js +6 -0
- package/dist/plugins/ecosystem/node-plugin-acquisition-host.js.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-bootstrap.d.ts +7 -1
- package/dist/plugins/ecosystem/node-plugin-bootstrap.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-bootstrap.js +50 -3
- package/dist/plugins/ecosystem/node-plugin-bootstrap.js.map +1 -1
- package/dist/plugins/ecosystem/plugin-bootstrap.d.ts +28 -16
- package/dist/plugins/ecosystem/plugin-bootstrap.d.ts.map +1 -1
- package/dist/plugins/ecosystem/plugin-bootstrap.js +35 -33
- package/dist/plugins/ecosystem/plugin-bootstrap.js.map +1 -1
- package/dist/profile/plugin-profiles.d.ts +1 -1
- package/dist/profile/plugin-profiles.d.ts.map +1 -1
- package/dist/profile/plugin-profiles.js +18 -0
- package/dist/profile/plugin-profiles.js.map +1 -1
- package/dist/profiles/builtin-suites.d.ts +19 -5
- package/dist/profiles/builtin-suites.d.ts.map +1 -1
- package/dist/profiles/builtin-suites.js +26 -6
- package/dist/profiles/builtin-suites.js.map +1 -1
- package/docs/settings.md +354 -354
- package/docs/usage.md +292 -292
- package/examples/extensions/bash-spawn-hook/index.ts +7 -2
- package/examples/extensions/built-in-tool-renderer/index.ts +4 -2
- package/examples/extensions/minimal-mode/index.ts +2 -2
- package/examples/extensions/truncated-tool/index.ts +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/extensions/with-deps/plugin.json +1 -1
- package/package.json +3 -2
- package/dist/bundle/chunks/chunk-6ZKDNZHA.js +0 -145
- package/dist/bundle/chunks/chunk-HLJEVRWP.js +0 -13
- package/dist/bundle/chunks/chunk-YGHQME4V.js +0 -1456
- package/dist/bundle/chunks/virtual-modules-KZ5DRH7Q.js +0 -2
- package/dist/core/extensions/bootstrap-branch-summary-policy.d.ts +0 -7
- package/dist/core/extensions/bootstrap-branch-summary-policy.d.ts.map +0 -1
- package/dist/core/extensions/bootstrap-branch-summary-policy.js +0 -28
- package/dist/core/extensions/bootstrap-branch-summary-policy.js.map +0 -1
- package/dist/core/extensions/bootstrap-compaction-policy.d.ts +0 -7
- package/dist/core/extensions/bootstrap-compaction-policy.d.ts.map +0 -1
- package/dist/core/extensions/bootstrap-compaction-policy.js +0 -48
- package/dist/core/extensions/bootstrap-compaction-policy.js.map +0 -1
- package/dist/core/extensions/bootstrap-overflow-policy.d.ts +0 -7
- package/dist/core/extensions/bootstrap-overflow-policy.d.ts.map +0 -1
- package/dist/core/extensions/bootstrap-overflow-policy.js +0 -30
- package/dist/core/extensions/bootstrap-overflow-policy.js.map +0 -1
- package/dist/core/extensions/bootstrap-retry-policy.d.ts +0 -7
- package/dist/core/extensions/bootstrap-retry-policy.d.ts.map +0 -1
- package/dist/core/extensions/bootstrap-retry-policy.js +0 -29
- package/dist/core/extensions/bootstrap-retry-policy.js.map +0 -1
- package/dist/core/extensions/bootstrap-tools.d.ts +0 -20
- package/dist/core/extensions/bootstrap-tools.d.ts.map +0 -1
- package/dist/core/extensions/bootstrap-tools.js +0 -36
- package/dist/core/extensions/bootstrap-tools.js.map +0 -1
- package/dist/core/extensions/provider-failover.d.ts +0 -110
- package/dist/core/extensions/provider-failover.d.ts.map +0 -1
- package/dist/core/extensions/provider-failover.js.map +0 -1
- package/dist/core/tools/bash.d.ts.map +0 -1
- package/dist/core/tools/bash.js.map +0 -1
- package/dist/core/tools/edit.d.ts.map +0 -1
- package/dist/core/tools/edit.js.map +0 -1
- package/dist/core/tools/find.d.ts.map +0 -1
- package/dist/core/tools/find.js.map +0 -1
- package/dist/core/tools/grep.d.ts.map +0 -1
- package/dist/core/tools/grep.js.map +0 -1
- package/dist/core/tools/ls.d.ts.map +0 -1
- package/dist/core/tools/powershell.d.ts.map +0 -1
- package/dist/core/tools/powershell.js.map +0 -1
- package/dist/core/tools/read.d.ts.map +0 -1
- package/dist/core/tools/read.js.map +0 -1
- package/dist/core/tools/write.d.ts.map +0 -1
- package/dist/core/tools/write.js.map +0 -1
- package/dist/plugins/mcp-http.d.ts.map +0 -1
- package/dist/plugins/mcp-http.js.map +0 -1
- package/dist/profile/child-facade.d.ts +0 -44
- package/dist/profile/child-facade.d.ts.map +0 -1
- package/dist/profile/child-facade.js +0 -56
- package/dist/profile/child-facade.js.map +0 -1
- package/dist/profile/dependency-wiring.d.ts +0 -48
- package/dist/profile/dependency-wiring.d.ts.map +0 -1
- package/dist/profile/dependency-wiring.js +0 -56
- package/dist/profile/dependency-wiring.js.map +0 -1
- package/dist/profile/domain-recovery.d.ts +0 -60
- package/dist/profile/domain-recovery.d.ts.map +0 -1
- package/dist/profile/domain-recovery.js +0 -94
- package/dist/profile/domain-recovery.js.map +0 -1
- package/dist/profile/first-roles.d.ts +0 -30
- package/dist/profile/first-roles.d.ts.map +0 -1
- package/dist/profile/first-roles.js +0 -83
- package/dist/profile/first-roles.js.map +0 -1
- package/dist/profile/goal-plugin.d.ts +0 -38
- package/dist/profile/goal-plugin.d.ts.map +0 -1
- package/dist/profile/goal-plugin.js +0 -132
- package/dist/profile/goal-plugin.js.map +0 -1
- package/dist/profile/plan-plugin.d.ts +0 -44
- package/dist/profile/plan-plugin.d.ts.map +0 -1
- package/dist/profile/plan-plugin.js +0 -157
- package/dist/profile/plan-plugin.js.map +0 -1
- package/dist/profile/policy-manifest.d.ts +0 -53
- package/dist/profile/policy-manifest.d.ts.map +0 -1
- package/dist/profile/policy-manifest.js +0 -53
- package/dist/profile/policy-manifest.js.map +0 -1
- package/dist/profile/role-assembly.d.ts +0 -47
- package/dist/profile/role-assembly.d.ts.map +0 -1
- package/dist/profile/role-assembly.js +0 -31
- package/dist/profile/role-assembly.js.map +0 -1
- package/dist/profile/role-catalog.d.ts +0 -43
- package/dist/profile/role-catalog.d.ts.map +0 -1
- package/dist/profile/role-catalog.js +0 -132
- package/dist/profile/role-catalog.js.map +0 -1
- package/dist/profile/role-orchestrator.d.ts +0 -123
- package/dist/profile/role-orchestrator.d.ts.map +0 -1
- package/dist/profile/role-orchestrator.js +0 -195
- package/dist/profile/role-orchestrator.js.map +0 -1
- package/dist/profile/role-runner.d.ts +0 -99
- package/dist/profile/role-runner.d.ts.map +0 -1
- package/dist/profile/role-runner.js +0 -79
- package/dist/profile/role-runner.js.map +0 -1
- package/dist/profile/root-facade.d.ts +0 -36
- package/dist/profile/root-facade.d.ts.map +0 -1
- package/dist/profile/root-facade.js +0 -49
- package/dist/profile/root-facade.js.map +0 -1
- package/dist/profile/skill-discovery-wiring.d.ts +0 -58
- package/dist/profile/skill-discovery-wiring.d.ts.map +0 -1
- package/dist/profile/skill-discovery-wiring.js +0 -80
- package/dist/profile/skill-discovery-wiring.js.map +0 -1
- package/dist/profile/state-store.d.ts +0 -62
- package/dist/profile/state-store.d.ts.map +0 -1
- package/dist/profile/state-store.js +0 -144
- package/dist/profile/state-store.js.map +0 -1
- package/dist/profile/todo-plugin.d.ts +0 -37
- package/dist/profile/todo-plugin.d.ts.map +0 -1
- package/dist/profile/todo-plugin.js +0 -137
- package/dist/profile/todo-plugin.js.map +0 -1
- package/examples/extensions/mcp-stdio/index.d.ts +0 -64
- package/examples/extensions/mcp-stdio/index.d.ts.map +0 -1
- package/examples/extensions/mcp-stdio/index.js +0 -292
- package/examples/extensions/mcp-stdio/index.js.map +0 -1
- /package/{dist/plugins → bundled-plugins/mcp-http/dist}/mcp-http.d.ts +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface CleanupCandidate {
|
|
2
|
+
/** 会话文件绝对路径。 */
|
|
3
|
+
path: string;
|
|
4
|
+
sizeBytes: number;
|
|
5
|
+
mtimeMs: number;
|
|
6
|
+
}
|
|
7
|
+
export interface CleanupScanResult {
|
|
8
|
+
candidates: CleanupCandidate[];
|
|
9
|
+
totalBytes: number;
|
|
10
|
+
}
|
|
11
|
+
export interface CleanupApplyResult {
|
|
12
|
+
deleted: number;
|
|
13
|
+
freedBytes: number;
|
|
14
|
+
}
|
|
15
|
+
/** 解析 `/clean-sessions [days]` 参数:正整数天数,缺省 7;非法返回 null。 */
|
|
16
|
+
export declare function parseCleanSessionsDays(arg: string | undefined): number | null;
|
|
17
|
+
/**
|
|
18
|
+
* 扫描清理候选:遍历 sessionsRoot 下全部一级目录(含 `_agents`),对每个
|
|
19
|
+
* `*.jsonl`(排除 `*.trace.jsonl`)真实 stat,mtime 早于 cutoff 的收进候选。
|
|
20
|
+
* 不读索引;单个 readdir/stat 失败只损失该目录/文件,不中断扫描。
|
|
21
|
+
*/
|
|
22
|
+
export declare function scanSessionsForCleanup(input: {
|
|
23
|
+
sessionsRoot: string;
|
|
24
|
+
cutoffMs: number;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
}): Promise<CleanupScanResult>;
|
|
27
|
+
/**
|
|
28
|
+
* 执行删除:逐个 unlink 会话文件 + 配对 `*.trace.jsonl` 与 `*.jsonl.guard`
|
|
29
|
+
* (存在才删,单个失败不中断其余);每个会话删除成功后同步移除索引条目;
|
|
30
|
+
* 最后尝试删除被清空的项目目录(非空目录由 rmdir 自行失败并忽略)。
|
|
31
|
+
* 失败契约同 trace-retention:除中止外不抛错,按文件计数。
|
|
32
|
+
*/
|
|
33
|
+
export declare function applySessionCleanup(paths: string[], signal?: AbortSignal): Promise<CleanupApplyResult>;
|
|
34
|
+
/** runCleanSessions 的宿主面:交互模式用 showError/showStatus/showDialogConfirm 适配。 */
|
|
35
|
+
export interface CleanSessionsRunHost {
|
|
36
|
+
/** 展示信息行(预览 / 结果 / 取消提示)。 */
|
|
37
|
+
showMessage(text: string): void;
|
|
38
|
+
/** 展示错误(用法等)。 */
|
|
39
|
+
showError(text: string): void;
|
|
40
|
+
/** 宿主确认对话框;resolve true = 确认执行。 */
|
|
41
|
+
confirm(title: string, message: string): Promise<boolean>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* /clean-sessions 的完整两阶段流程(设计 §7):解析参数 → 扫描 → 零命中直接
|
|
45
|
+
* 提示 → 预览(命中数、总占用、截止时间)→ 宿主确认 → 执行 → 报告。宿主无关,
|
|
46
|
+
* 确定性可测;interactive-mode 只做输出/确认适配。
|
|
47
|
+
*/
|
|
48
|
+
export declare function runCleanSessions(host: CleanSessionsRunHost, arg: string | undefined, signal?: AbortSignal): Promise<void>;
|
|
49
|
+
//# sourceMappingURL=session-cleanup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-cleanup.d.ts","sourceRoot":"","sources":["../../src/core/session-cleanup.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,gBAAgB;IAChC,kCAAgB;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IACjC,UAAU,EAAE,gBAAgB,EAAE,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,iGAA2D;AAC3D,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAM7E;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,KAAK,EAAE;IACnD,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAgC7B;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAsC5G;AAED,uGAA6E;AAC7E,MAAM,WAAW,oBAAoB;IACpC,6DAA6B;IAC7B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,qCAAiB;IACjB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,6DAAmC;IACnC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1D;AAMD;;;;GAIG;AACH,wBAAsB,gBAAgB,CACrC,IAAI,EAAE,oBAAoB,EAC1B,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,IAAI,CAAC,CAuCf","sourcesContent":["import type { Dirent } from \"node:fs\";\nimport { readdir, rmdir, stat, unlink } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { getSessionsDir } from \"../config.ts\";\nimport { t } from \"./i18n/index.ts\";\nimport { removeSessionFromIndex } from \"./session-index.ts\";\nimport { getTraceFilePathForSession } from \"./session-manager.ts\";\n\n/**\n * 会话清理命令 /clean-sessions 的内核机制(设计 docs/design/会话索引.md §7)。\n *\n * 清理是磁盘回收语义:真实 stat 每个会话文件取文件系统 mtime 判定,不信任索引\n * 缓存(索引可能是过期投影)。范围是 sessions 根下全部一级项目目录含 `_agents/`;\n * 自定义 --session-dir 不在范围内。删除不可逆,两阶段执行:预览 → 宿主确认 →\n * 执行。单一触发入口:交互模式文本命令,无键绑定(设计 §8)。\n */\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\nconst DEFAULT_CLEAN_SESSIONS_DAYS = 7;\n/** 会话写守卫配对文件后缀(`<sessionFile>.guard`,同 session-storage.ts)。 */\nconst GUARD_FILE_SUFFIX = \".guard\";\n\nexport interface CleanupCandidate {\n\t/** 会话文件绝对路径。 */\n\tpath: string;\n\tsizeBytes: number;\n\tmtimeMs: number;\n}\n\nexport interface CleanupScanResult {\n\tcandidates: CleanupCandidate[];\n\ttotalBytes: number;\n}\n\nexport interface CleanupApplyResult {\n\tdeleted: number;\n\tfreedBytes: number;\n}\n\n/** 解析 `/clean-sessions [days]` 参数:正整数天数,缺省 7;非法返回 null。 */\nexport function parseCleanSessionsDays(arg: string | undefined): number | null {\n\tif (arg === undefined || arg.trim() === \"\") return DEFAULT_CLEAN_SESSIONS_DAYS;\n\tconst trimmed = arg.trim();\n\tif (!/^\\d+$/.test(trimmed)) return null;\n\tconst days = Number.parseInt(trimmed, 10);\n\treturn days > 0 ? days : null;\n}\n\n/**\n * 扫描清理候选:遍历 sessionsRoot 下全部一级目录(含 `_agents`),对每个\n * `*.jsonl`(排除 `*.trace.jsonl`)真实 stat,mtime 早于 cutoff 的收进候选。\n * 不读索引;单个 readdir/stat 失败只损失该目录/文件,不中断扫描。\n */\nexport async function scanSessionsForCleanup(input: {\n\tsessionsRoot: string;\n\tcutoffMs: number;\n\tsignal?: AbortSignal;\n}): Promise<CleanupScanResult> {\n\tconst { sessionsRoot, cutoffMs, signal } = input;\n\tconst candidates: CleanupCandidate[] = [];\n\tlet rootEntries: Dirent[];\n\ttry {\n\t\trootEntries = await readdir(sessionsRoot, { withFileTypes: true });\n\t} catch {\n\t\treturn { candidates, totalBytes: 0 };\n\t}\n\tfor (const entry of rootEntries) {\n\t\tif (!entry.isDirectory() && !entry.isSymbolicLink()) continue;\n\t\tlet fileNames: string[];\n\t\ttry {\n\t\t\tfileNames = await readdir(join(sessionsRoot, entry.name));\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const file of fileNames) {\n\t\t\tif (!file.endsWith(\".jsonl\") || file.endsWith(\".trace.jsonl\")) continue;\n\t\t\tsignal?.throwIfAborted();\n\t\t\tconst path = join(sessionsRoot, entry.name, file);\n\t\t\ttry {\n\t\t\t\tconst stats = await stat(path);\n\t\t\t\tif (stats.mtimeMs < cutoffMs) {\n\t\t\t\t\tcandidates.push({ path, sizeBytes: stats.size, mtimeMs: stats.mtimeMs });\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// stat 失败的文件不进候选(下轮收敛)。\n\t\t\t}\n\t\t}\n\t}\n\treturn { candidates, totalBytes: candidates.reduce((sum, candidate) => sum + candidate.sizeBytes, 0) };\n}\n\n/**\n * 执行删除:逐个 unlink 会话文件 + 配对 `*.trace.jsonl` 与 `*.jsonl.guard`\n * (存在才删,单个失败不中断其余);每个会话删除成功后同步移除索引条目;\n * 最后尝试删除被清空的项目目录(非空目录由 rmdir 自行失败并忽略)。\n * 失败契约同 trace-retention:除中止外不抛错,按文件计数。\n */\nexport async function applySessionCleanup(paths: string[], signal?: AbortSignal): Promise<CleanupApplyResult> {\n\tlet deleted = 0;\n\tlet freedBytes = 0;\n\tconst affectedDirs = new Set<string>();\n\tfor (const path of paths) {\n\t\tsignal?.throwIfAborted();\n\t\tlet sizeBytes = 0;\n\t\ttry {\n\t\t\tsizeBytes = (await stat(path)).size;\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\ttry {\n\t\t\tawait unlink(path);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tdeleted++;\n\t\tfreedBytes += sizeBytes;\n\t\tremoveSessionFromIndex(path);\n\t\taffectedDirs.add(dirname(path));\n\t\tfor (const sidecar of [getTraceFilePathForSession(path), `${path}${GUARD_FILE_SUFFIX}`]) {\n\t\t\ttry {\n\t\t\t\tawait unlink(sidecar);\n\t\t\t} catch {\n\t\t\t\t// 配对文件存在才删。\n\t\t\t}\n\t\t}\n\t}\n\tfor (const dir of affectedDirs) {\n\t\tsignal?.throwIfAborted();\n\t\ttry {\n\t\t\tawait rmdir(dir);\n\t\t} catch {\n\t\t\t// 目录非空(还有别的文件)则保留。\n\t\t}\n\t}\n\treturn { deleted, freedBytes };\n}\n\n/** runCleanSessions 的宿主面:交互模式用 showError/showStatus/showDialogConfirm 适配。 */\nexport interface CleanSessionsRunHost {\n\t/** 展示信息行(预览 / 结果 / 取消提示)。 */\n\tshowMessage(text: string): void;\n\t/** 展示错误(用法等)。 */\n\tshowError(text: string): void;\n\t/** 宿主确认对话框;resolve true = 确认执行。 */\n\tconfirm(title: string, message: string): Promise<boolean>;\n}\n\nfunction formatMegabytes(bytes: number): string {\n\treturn `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\n/**\n * /clean-sessions 的完整两阶段流程(设计 §7):解析参数 → 扫描 → 零命中直接\n * 提示 → 预览(命中数、总占用、截止时间)→ 宿主确认 → 执行 → 报告。宿主无关,\n * 确定性可测;interactive-mode 只做输出/确认适配。\n */\nexport async function runCleanSessions(\n\thost: CleanSessionsRunHost,\n\targ: string | undefined,\n\tsignal?: AbortSignal,\n): Promise<void> {\n\tconst days = parseCleanSessionsDays(arg);\n\tif (days === null) {\n\t\thost.showError(t(\"Usage: /clean-sessions [days] - days must be a positive integer (default 7)\"));\n\t\treturn;\n\t}\n\tconst cutoffMs = Date.now() - days * DAY_MS;\n\tconst scan = await scanSessionsForCleanup({ sessionsRoot: getSessionsDir(), cutoffMs, signal });\n\tif (scan.candidates.length === 0) {\n\t\thost.showMessage(t(\"No sessions untouched for {days} days - nothing to clean\", { days }));\n\t\treturn;\n\t}\n\t// 预览(两阶段第一步):命中数量、总占用、截止时间点。\n\thost.showMessage(\n\t\tt(\"{count} sessions ({size}) untouched since {cutoff} - confirm to delete\", {\n\t\t\tcount: scan.candidates.length,\n\t\t\tsize: formatMegabytes(scan.totalBytes),\n\t\t\tcutoff: new Date(cutoffMs).toLocaleString(),\n\t\t}),\n\t);\n\tconst confirmed = await host.confirm(\n\t\tt(\"Clean old sessions\"),\n\t\tt(\"Permanently delete {count} sessions ({size}) untouched for {days} days? This cannot be undone.\", {\n\t\t\tcount: scan.candidates.length,\n\t\t\tsize: formatMegabytes(scan.totalBytes),\n\t\t\tdays,\n\t\t}),\n\t);\n\tif (!confirmed) {\n\t\thost.showMessage(t(\"Clean-sessions cancelled\"));\n\t\treturn;\n\t}\n\tconst result = await applySessionCleanup(\n\t\tscan.candidates.map((candidate) => candidate.path),\n\t\tsignal,\n\t);\n\thost.showMessage(\n\t\tt(\"Deleted {count} sessions, freed {size}\", { count: result.deleted, size: formatMegabytes(result.freedBytes) }),\n\t);\n}\n"]}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { readdir, rmdir, stat, unlink } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { getSessionsDir } from "../config.js";
|
|
4
|
+
import { t } from "./i18n/index.js";
|
|
5
|
+
import { removeSessionFromIndex } from "./session-index.js";
|
|
6
|
+
import { getTraceFilePathForSession } from "./session-manager.js";
|
|
7
|
+
/**
|
|
8
|
+
* 会话清理命令 /clean-sessions 的内核机制(设计 docs/design/会话索引.md §7)。
|
|
9
|
+
*
|
|
10
|
+
* 清理是磁盘回收语义:真实 stat 每个会话文件取文件系统 mtime 判定,不信任索引
|
|
11
|
+
* 缓存(索引可能是过期投影)。范围是 sessions 根下全部一级项目目录含 `_agents/`;
|
|
12
|
+
* 自定义 --session-dir 不在范围内。删除不可逆,两阶段执行:预览 → 宿主确认 →
|
|
13
|
+
* 执行。单一触发入口:交互模式文本命令,无键绑定(设计 §8)。
|
|
14
|
+
*/
|
|
15
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
16
|
+
const DEFAULT_CLEAN_SESSIONS_DAYS = 7;
|
|
17
|
+
/** 会话写守卫配对文件后缀(`<sessionFile>.guard`,同 session-storage.ts)。 */
|
|
18
|
+
const GUARD_FILE_SUFFIX = ".guard";
|
|
19
|
+
/** 解析 `/clean-sessions [days]` 参数:正整数天数,缺省 7;非法返回 null。 */
|
|
20
|
+
export function parseCleanSessionsDays(arg) {
|
|
21
|
+
if (arg === undefined || arg.trim() === "")
|
|
22
|
+
return DEFAULT_CLEAN_SESSIONS_DAYS;
|
|
23
|
+
const trimmed = arg.trim();
|
|
24
|
+
if (!/^\d+$/.test(trimmed))
|
|
25
|
+
return null;
|
|
26
|
+
const days = Number.parseInt(trimmed, 10);
|
|
27
|
+
return days > 0 ? days : null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 扫描清理候选:遍历 sessionsRoot 下全部一级目录(含 `_agents`),对每个
|
|
31
|
+
* `*.jsonl`(排除 `*.trace.jsonl`)真实 stat,mtime 早于 cutoff 的收进候选。
|
|
32
|
+
* 不读索引;单个 readdir/stat 失败只损失该目录/文件,不中断扫描。
|
|
33
|
+
*/
|
|
34
|
+
export async function scanSessionsForCleanup(input) {
|
|
35
|
+
const { sessionsRoot, cutoffMs, signal } = input;
|
|
36
|
+
const candidates = [];
|
|
37
|
+
let rootEntries;
|
|
38
|
+
try {
|
|
39
|
+
rootEntries = await readdir(sessionsRoot, { withFileTypes: true });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return { candidates, totalBytes: 0 };
|
|
43
|
+
}
|
|
44
|
+
for (const entry of rootEntries) {
|
|
45
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
46
|
+
continue;
|
|
47
|
+
let fileNames;
|
|
48
|
+
try {
|
|
49
|
+
fileNames = await readdir(join(sessionsRoot, entry.name));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
for (const file of fileNames) {
|
|
55
|
+
if (!file.endsWith(".jsonl") || file.endsWith(".trace.jsonl"))
|
|
56
|
+
continue;
|
|
57
|
+
signal?.throwIfAborted();
|
|
58
|
+
const path = join(sessionsRoot, entry.name, file);
|
|
59
|
+
try {
|
|
60
|
+
const stats = await stat(path);
|
|
61
|
+
if (stats.mtimeMs < cutoffMs) {
|
|
62
|
+
candidates.push({ path, sizeBytes: stats.size, mtimeMs: stats.mtimeMs });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// stat 失败的文件不进候选(下轮收敛)。
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { candidates, totalBytes: candidates.reduce((sum, candidate) => sum + candidate.sizeBytes, 0) };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 执行删除:逐个 unlink 会话文件 + 配对 `*.trace.jsonl` 与 `*.jsonl.guard`
|
|
74
|
+
* (存在才删,单个失败不中断其余);每个会话删除成功后同步移除索引条目;
|
|
75
|
+
* 最后尝试删除被清空的项目目录(非空目录由 rmdir 自行失败并忽略)。
|
|
76
|
+
* 失败契约同 trace-retention:除中止外不抛错,按文件计数。
|
|
77
|
+
*/
|
|
78
|
+
export async function applySessionCleanup(paths, signal) {
|
|
79
|
+
let deleted = 0;
|
|
80
|
+
let freedBytes = 0;
|
|
81
|
+
const affectedDirs = new Set();
|
|
82
|
+
for (const path of paths) {
|
|
83
|
+
signal?.throwIfAborted();
|
|
84
|
+
let sizeBytes = 0;
|
|
85
|
+
try {
|
|
86
|
+
sizeBytes = (await stat(path)).size;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
await unlink(path);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
deleted++;
|
|
98
|
+
freedBytes += sizeBytes;
|
|
99
|
+
removeSessionFromIndex(path);
|
|
100
|
+
affectedDirs.add(dirname(path));
|
|
101
|
+
for (const sidecar of [getTraceFilePathForSession(path), `${path}${GUARD_FILE_SUFFIX}`]) {
|
|
102
|
+
try {
|
|
103
|
+
await unlink(sidecar);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// 配对文件存在才删。
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const dir of affectedDirs) {
|
|
111
|
+
signal?.throwIfAborted();
|
|
112
|
+
try {
|
|
113
|
+
await rmdir(dir);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// 目录非空(还有别的文件)则保留。
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { deleted, freedBytes };
|
|
120
|
+
}
|
|
121
|
+
function formatMegabytes(bytes) {
|
|
122
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* /clean-sessions 的完整两阶段流程(设计 §7):解析参数 → 扫描 → 零命中直接
|
|
126
|
+
* 提示 → 预览(命中数、总占用、截止时间)→ 宿主确认 → 执行 → 报告。宿主无关,
|
|
127
|
+
* 确定性可测;interactive-mode 只做输出/确认适配。
|
|
128
|
+
*/
|
|
129
|
+
export async function runCleanSessions(host, arg, signal) {
|
|
130
|
+
const days = parseCleanSessionsDays(arg);
|
|
131
|
+
if (days === null) {
|
|
132
|
+
host.showError(t("Usage: /clean-sessions [days] - days must be a positive integer (default 7)"));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const cutoffMs = Date.now() - days * DAY_MS;
|
|
136
|
+
const scan = await scanSessionsForCleanup({ sessionsRoot: getSessionsDir(), cutoffMs, signal });
|
|
137
|
+
if (scan.candidates.length === 0) {
|
|
138
|
+
host.showMessage(t("No sessions untouched for {days} days - nothing to clean", { days }));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// 预览(两阶段第一步):命中数量、总占用、截止时间点。
|
|
142
|
+
host.showMessage(t("{count} sessions ({size}) untouched since {cutoff} - confirm to delete", {
|
|
143
|
+
count: scan.candidates.length,
|
|
144
|
+
size: formatMegabytes(scan.totalBytes),
|
|
145
|
+
cutoff: new Date(cutoffMs).toLocaleString(),
|
|
146
|
+
}));
|
|
147
|
+
const confirmed = await host.confirm(t("Clean old sessions"), t("Permanently delete {count} sessions ({size}) untouched for {days} days? This cannot be undone.", {
|
|
148
|
+
count: scan.candidates.length,
|
|
149
|
+
size: formatMegabytes(scan.totalBytes),
|
|
150
|
+
days,
|
|
151
|
+
}));
|
|
152
|
+
if (!confirmed) {
|
|
153
|
+
host.showMessage(t("Clean-sessions cancelled"));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const result = await applySessionCleanup(scan.candidates.map((candidate) => candidate.path), signal);
|
|
157
|
+
host.showMessage(t("Deleted {count} sessions, freed {size}", { count: result.deleted, size: formatMegabytes(result.freedBytes) }));
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=session-cleanup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-cleanup.js","sourceRoot":"","sources":["../../src/core/session-cleanup.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Dirent } from \"node:fs\";\nimport { readdir, rmdir, stat, unlink } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { getSessionsDir } from \"../config.ts\";\nimport { t } from \"./i18n/index.ts\";\nimport { removeSessionFromIndex } from \"./session-index.ts\";\nimport { getTraceFilePathForSession } from \"./session-manager.ts\";\n\n/**\n * 会话清理命令 /clean-sessions 的内核机制(设计 docs/design/会话索引.md §7)。\n *\n * 清理是磁盘回收语义:真实 stat 每个会话文件取文件系统 mtime 判定,不信任索引\n * 缓存(索引可能是过期投影)。范围是 sessions 根下全部一级项目目录含 `_agents/`;\n * 自定义 --session-dir 不在范围内。删除不可逆,两阶段执行:预览 → 宿主确认 →\n * 执行。单一触发入口:交互模式文本命令,无键绑定(设计 §8)。\n */\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\nconst DEFAULT_CLEAN_SESSIONS_DAYS = 7;\n/** 会话写守卫配对文件后缀(`<sessionFile>.guard`,同 session-storage.ts)。 */\nconst GUARD_FILE_SUFFIX = \".guard\";\n\nexport interface CleanupCandidate {\n\t/** 会话文件绝对路径。 */\n\tpath: string;\n\tsizeBytes: number;\n\tmtimeMs: number;\n}\n\nexport interface CleanupScanResult {\n\tcandidates: CleanupCandidate[];\n\ttotalBytes: number;\n}\n\nexport interface CleanupApplyResult {\n\tdeleted: number;\n\tfreedBytes: number;\n}\n\n/** 解析 `/clean-sessions [days]` 参数:正整数天数,缺省 7;非法返回 null。 */\nexport function parseCleanSessionsDays(arg: string | undefined): number | null {\n\tif (arg === undefined || arg.trim() === \"\") return DEFAULT_CLEAN_SESSIONS_DAYS;\n\tconst trimmed = arg.trim();\n\tif (!/^\\d+$/.test(trimmed)) return null;\n\tconst days = Number.parseInt(trimmed, 10);\n\treturn days > 0 ? days : null;\n}\n\n/**\n * 扫描清理候选:遍历 sessionsRoot 下全部一级目录(含 `_agents`),对每个\n * `*.jsonl`(排除 `*.trace.jsonl`)真实 stat,mtime 早于 cutoff 的收进候选。\n * 不读索引;单个 readdir/stat 失败只损失该目录/文件,不中断扫描。\n */\nexport async function scanSessionsForCleanup(input: {\n\tsessionsRoot: string;\n\tcutoffMs: number;\n\tsignal?: AbortSignal;\n}): Promise<CleanupScanResult> {\n\tconst { sessionsRoot, cutoffMs, signal } = input;\n\tconst candidates: CleanupCandidate[] = [];\n\tlet rootEntries: Dirent[];\n\ttry {\n\t\trootEntries = await readdir(sessionsRoot, { withFileTypes: true });\n\t} catch {\n\t\treturn { candidates, totalBytes: 0 };\n\t}\n\tfor (const entry of rootEntries) {\n\t\tif (!entry.isDirectory() && !entry.isSymbolicLink()) continue;\n\t\tlet fileNames: string[];\n\t\ttry {\n\t\t\tfileNames = await readdir(join(sessionsRoot, entry.name));\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const file of fileNames) {\n\t\t\tif (!file.endsWith(\".jsonl\") || file.endsWith(\".trace.jsonl\")) continue;\n\t\t\tsignal?.throwIfAborted();\n\t\t\tconst path = join(sessionsRoot, entry.name, file);\n\t\t\ttry {\n\t\t\t\tconst stats = await stat(path);\n\t\t\t\tif (stats.mtimeMs < cutoffMs) {\n\t\t\t\t\tcandidates.push({ path, sizeBytes: stats.size, mtimeMs: stats.mtimeMs });\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// stat 失败的文件不进候选(下轮收敛)。\n\t\t\t}\n\t\t}\n\t}\n\treturn { candidates, totalBytes: candidates.reduce((sum, candidate) => sum + candidate.sizeBytes, 0) };\n}\n\n/**\n * 执行删除:逐个 unlink 会话文件 + 配对 `*.trace.jsonl` 与 `*.jsonl.guard`\n * (存在才删,单个失败不中断其余);每个会话删除成功后同步移除索引条目;\n * 最后尝试删除被清空的项目目录(非空目录由 rmdir 自行失败并忽略)。\n * 失败契约同 trace-retention:除中止外不抛错,按文件计数。\n */\nexport async function applySessionCleanup(paths: string[], signal?: AbortSignal): Promise<CleanupApplyResult> {\n\tlet deleted = 0;\n\tlet freedBytes = 0;\n\tconst affectedDirs = new Set<string>();\n\tfor (const path of paths) {\n\t\tsignal?.throwIfAborted();\n\t\tlet sizeBytes = 0;\n\t\ttry {\n\t\t\tsizeBytes = (await stat(path)).size;\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\ttry {\n\t\t\tawait unlink(path);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tdeleted++;\n\t\tfreedBytes += sizeBytes;\n\t\tremoveSessionFromIndex(path);\n\t\taffectedDirs.add(dirname(path));\n\t\tfor (const sidecar of [getTraceFilePathForSession(path), `${path}${GUARD_FILE_SUFFIX}`]) {\n\t\t\ttry {\n\t\t\t\tawait unlink(sidecar);\n\t\t\t} catch {\n\t\t\t\t// 配对文件存在才删。\n\t\t\t}\n\t\t}\n\t}\n\tfor (const dir of affectedDirs) {\n\t\tsignal?.throwIfAborted();\n\t\ttry {\n\t\t\tawait rmdir(dir);\n\t\t} catch {\n\t\t\t// 目录非空(还有别的文件)则保留。\n\t\t}\n\t}\n\treturn { deleted, freedBytes };\n}\n\n/** runCleanSessions 的宿主面:交互模式用 showError/showStatus/showDialogConfirm 适配。 */\nexport interface CleanSessionsRunHost {\n\t/** 展示信息行(预览 / 结果 / 取消提示)。 */\n\tshowMessage(text: string): void;\n\t/** 展示错误(用法等)。 */\n\tshowError(text: string): void;\n\t/** 宿主确认对话框;resolve true = 确认执行。 */\n\tconfirm(title: string, message: string): Promise<boolean>;\n}\n\nfunction formatMegabytes(bytes: number): string {\n\treturn `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\n/**\n * /clean-sessions 的完整两阶段流程(设计 §7):解析参数 → 扫描 → 零命中直接\n * 提示 → 预览(命中数、总占用、截止时间)→ 宿主确认 → 执行 → 报告。宿主无关,\n * 确定性可测;interactive-mode 只做输出/确认适配。\n */\nexport async function runCleanSessions(\n\thost: CleanSessionsRunHost,\n\targ: string | undefined,\n\tsignal?: AbortSignal,\n): Promise<void> {\n\tconst days = parseCleanSessionsDays(arg);\n\tif (days === null) {\n\t\thost.showError(t(\"Usage: /clean-sessions [days] - days must be a positive integer (default 7)\"));\n\t\treturn;\n\t}\n\tconst cutoffMs = Date.now() - days * DAY_MS;\n\tconst scan = await scanSessionsForCleanup({ sessionsRoot: getSessionsDir(), cutoffMs, signal });\n\tif (scan.candidates.length === 0) {\n\t\thost.showMessage(t(\"No sessions untouched for {days} days - nothing to clean\", { days }));\n\t\treturn;\n\t}\n\t// 预览(两阶段第一步):命中数量、总占用、截止时间点。\n\thost.showMessage(\n\t\tt(\"{count} sessions ({size}) untouched since {cutoff} - confirm to delete\", {\n\t\t\tcount: scan.candidates.length,\n\t\t\tsize: formatMegabytes(scan.totalBytes),\n\t\t\tcutoff: new Date(cutoffMs).toLocaleString(),\n\t\t}),\n\t);\n\tconst confirmed = await host.confirm(\n\t\tt(\"Clean old sessions\"),\n\t\tt(\"Permanently delete {count} sessions ({size}) untouched for {days} days? This cannot be undone.\", {\n\t\t\tcount: scan.candidates.length,\n\t\t\tsize: formatMegabytes(scan.totalBytes),\n\t\t\tdays,\n\t\t}),\n\t);\n\tif (!confirmed) {\n\t\thost.showMessage(t(\"Clean-sessions cancelled\"));\n\t\treturn;\n\t}\n\tconst result = await applySessionCleanup(\n\t\tscan.candidates.map((candidate) => candidate.path),\n\t\tsignal,\n\t);\n\thost.showMessage(\n\t\tt(\"Deleted {count} sessions, freed {size}\", { count: result.deleted, size: formatMegabytes(result.freedBytes) }),\n\t);\n}\n"]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { SessionInfo } from "./session-manager.ts";
|
|
2
|
+
export interface SessionIndexProjectEntry {
|
|
3
|
+
/** 项目目录名(getDefaultSessionDirPath(cwd) 的编码目录名,不从 cwd 重推导)。 */
|
|
4
|
+
dir: string;
|
|
5
|
+
cwd: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SessionIndexSessionEntry {
|
|
8
|
+
/** 会话文件 basename;键 = (dir, file)。 */
|
|
9
|
+
file: string;
|
|
10
|
+
dir: string;
|
|
11
|
+
id: string;
|
|
12
|
+
cwd: string;
|
|
13
|
+
/**
|
|
14
|
+
* 缓存有效期令牌:条目写入时该会话文件的 stat mtime。缺失或与磁盘不一致 =
|
|
15
|
+
* 缓存过期,对账时重读刷新(设计 §3/§5)。
|
|
16
|
+
*/
|
|
17
|
+
file_mtime_ms?: number;
|
|
18
|
+
name?: string;
|
|
19
|
+
parent_session_path?: string;
|
|
20
|
+
/** 创建时锁定的 profile(可选字段,向后可读;与 SessionHeader 对应)。 */
|
|
21
|
+
profile_id?: string;
|
|
22
|
+
profile_version?: string;
|
|
23
|
+
created_ms: number;
|
|
24
|
+
modified_ms: number;
|
|
25
|
+
message_count: number;
|
|
26
|
+
first_message: string;
|
|
27
|
+
}
|
|
28
|
+
export interface SessionIndexUpdateInput {
|
|
29
|
+
/** 绝对会话文件路径(默认布局下位于 sessions 根之内)。 */
|
|
30
|
+
sessionPath: string;
|
|
31
|
+
sessionId: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
parentSessionPath?: string;
|
|
35
|
+
profileId?: string;
|
|
36
|
+
profileVersion?: string;
|
|
37
|
+
createdMs: number;
|
|
38
|
+
modifiedMs: number;
|
|
39
|
+
messageCount: number;
|
|
40
|
+
firstMessage: string;
|
|
41
|
+
/** true = 同步原子落盘;false = 标脏,节流窗口内落盘。 */
|
|
42
|
+
immediate: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface ListSessionsViaIndexInput {
|
|
45
|
+
/** sessions 根目录(…/sessions)绝对路径。 */
|
|
46
|
+
sessionsRoot: string;
|
|
47
|
+
/** 完整 SessionInfo 构建器(session-manager 的 buildSessionInfo);返回 null = 非会话文件。 */
|
|
48
|
+
buildInfo: (path: string) => Promise<SessionInfo | null>;
|
|
49
|
+
/** 旧全量扫描委托:索引缺失/损坏/版本不符时用于重建索引(与旧 listAll 同价)。 */
|
|
50
|
+
scanAll: () => Promise<SessionInfo[]>;
|
|
51
|
+
/** 索引命中时的单次完整已排序快照回调(重建路径的进度由 scanAll 按旧语义发布)。 */
|
|
52
|
+
onHit?: (sessions: SessionInfo[]) => void;
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
}
|
|
55
|
+
/** 索引文件路径(与 plugin-state.json 同级;getAgentDir() 尊重 AGENT_FORGE_DIR)。 */
|
|
56
|
+
export declare function getSessionIndexPath(): string;
|
|
57
|
+
/** 立即落盘脏状态(测试与 exit 钩子共用);无脏状态时为 no-op。 */
|
|
58
|
+
export declare function flushSessionIndexNow(): void;
|
|
59
|
+
/**
|
|
60
|
+
* upsert 单会话条目并按 immediate 决定落盘时机。预热优化(设计 §4):条目缺失
|
|
61
|
+
* 或过期由读路径的 mtime 令牌对账刷新,不承担正确性。内部错误一律吞掉
|
|
62
|
+
* (fail-open):索引是投影,写入失败由下次读取对账自愈,绝不影响会话持久化。
|
|
63
|
+
*/
|
|
64
|
+
export declare function recordSessionUpdate(input: SessionIndexUpdateInput): void;
|
|
65
|
+
/**
|
|
66
|
+
* 按路径移除条目并立即原子落盘;索引不存在/损坏/路径不在 sessions 根时静默 no-op
|
|
67
|
+
* (不抛错、不重建,设计 §6)。
|
|
68
|
+
*/
|
|
69
|
+
export declare function removeSessionFromIndex(sessionPath: string): void;
|
|
70
|
+
/**
|
|
71
|
+
* listAll 默认布局分支的索引读路径。返回 undefined 表示索引层意外失败(IO 等),
|
|
72
|
+
* 调用方应回退到旧全量扫描(fail-open,设计 §5);中止按惯例抛 AbortError。
|
|
73
|
+
*/
|
|
74
|
+
export declare function listSessionsViaIndex(input: ListSessionsViaIndexInput): Promise<SessionInfo[] | undefined>;
|
|
75
|
+
//# sourceMappingURL=session-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-index.d.ts","sourceRoot":"","sources":["../../src/core/session-index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAiBxD,MAAM,WAAW,wBAAwB;IACxC,sGAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,wBAAwB;IACxC,mDAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,gGAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACtB;AASD,MAAM,WAAW,uBAAuB;IACvC,gFAAsC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,4EAAwC;IACxC,SAAS,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACzC,0DAAoC;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,gHAA8E;IAC9E,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACzD,kHAAkD;IAClD,OAAO,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IACtC,sHAAkD;IAClD,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;IAC1C,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,qGAAuE;AACvE,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AA4CD,2FAA2C;AAC3C,wBAAgB,oBAAoB,IAAI,IAAI,CAS3C;AAoKD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAyCxE;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAchE;AA+HD;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,yBAAyB,GAAG,OAAO,CAAC,WAAW,EAAE,GAAG,SAAS,CAAC,CA+B/G","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { type Dirent, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { readdir, stat } from \"node:fs/promises\";\nimport { basename, dirname, isAbsolute, join, relative } from \"node:path\";\nimport { getAgentDir, getSessionsDir } from \"../config.ts\";\nimport { AGENT_SESSIONS_DIR_NAME } from \"./agent-session-paths.ts\";\nimport type { SessionInfo } from \"./session-manager.ts\";\n\n/**\n * 会话元数据索引(session-index.json),设计契约见 docs/design/会话索引.md。\n *\n * 索引是文件系统的投影、永远不是事实源;条目内部分两层:关系(file/dir/id/cwd)\n * 由 readdir 对账保证,内容缓存由 file_mtime_ms 令牌保证(mtime 未变才可信,\n * 变了就重读刷新)。写钩子是预热优化、不承担正确性(设计 §4);读路径\n * (listSessionsViaIndex)负责对账自愈与全量重建。不加跨进程锁——丢失的更新\n * 会被令牌机制限定为无害,下次读取对账时按文件系统纠正(设计 §6)。\n */\n\nconst SESSION_INDEX_VERSION = 1;\nconst SESSION_INDEX_FILE_NAME = \"session-index.json\";\n/** 节流落盘窗口(设计 §4:每轮消息追加标脏,10s 落盘一次)。 */\nconst FLUSH_THROTTLE_MS = 10_000;\n\nexport interface SessionIndexProjectEntry {\n\t/** 项目目录名(getDefaultSessionDirPath(cwd) 的编码目录名,不从 cwd 重推导)。 */\n\tdir: string;\n\tcwd: string;\n}\n\nexport interface SessionIndexSessionEntry {\n\t/** 会话文件 basename;键 = (dir, file)。 */\n\tfile: string;\n\tdir: string;\n\tid: string;\n\tcwd: string;\n\t/**\n\t * 缓存有效期令牌:条目写入时该会话文件的 stat mtime。缺失或与磁盘不一致 =\n\t * 缓存过期,对账时重读刷新(设计 §3/§5)。\n\t */\n\tfile_mtime_ms?: number;\n\tname?: string;\n\tparent_session_path?: string;\n\t/** 创建时锁定的 profile(可选字段,向后可读;与 SessionHeader 对应)。 */\n\tprofile_id?: string;\n\tprofile_version?: string;\n\tcreated_ms: number;\n\tmodified_ms: number;\n\tmessage_count: number;\n\tfirst_message: string;\n}\n\ninterface SessionIndexFile {\n\tversion: number;\n\tupdated_at_ms: number;\n\tprojects: SessionIndexProjectEntry[];\n\tsessions: SessionIndexSessionEntry[];\n}\n\nexport interface SessionIndexUpdateInput {\n\t/** 绝对会话文件路径(默认布局下位于 sessions 根之内)。 */\n\tsessionPath: string;\n\tsessionId: string;\n\tcwd: string;\n\tname?: string;\n\tparentSessionPath?: string;\n\tprofileId?: string;\n\tprofileVersion?: string;\n\tcreatedMs: number;\n\tmodifiedMs: number;\n\tmessageCount: number;\n\tfirstMessage: string;\n\t/** true = 同步原子落盘;false = 标脏,节流窗口内落盘。 */\n\timmediate: boolean;\n}\n\nexport interface ListSessionsViaIndexInput {\n\t/** sessions 根目录(…/sessions)绝对路径。 */\n\tsessionsRoot: string;\n\t/** 完整 SessionInfo 构建器(session-manager 的 buildSessionInfo);返回 null = 非会话文件。 */\n\tbuildInfo: (path: string) => Promise<SessionInfo | null>;\n\t/** 旧全量扫描委托:索引缺失/损坏/版本不符时用于重建索引(与旧 listAll 同价)。 */\n\tscanAll: () => Promise<SessionInfo[]>;\n\t/** 索引命中时的单次完整已排序快照回调(重建路径的进度由 scanAll 按旧语义发布)。 */\n\tonHit?: (sessions: SessionInfo[]) => void;\n\tsignal?: AbortSignal;\n}\n\n/** 索引文件路径(与 plugin-state.json 同级;getAgentDir() 尊重 AGENT_FORGE_DIR)。 */\nexport function getSessionIndexPath(): string {\n\treturn join(getAgentDir(), SESSION_INDEX_FILE_NAME);\n}\n\n// ---------------------------------------------------------------------------\n// 模块级脏状态:pendingFlush 捕获写入目标与快照,进程 exit 时同步落盘。\n// 按路径比较,测试间 ENV_AGENT_DIR 切换不会串写。\n// ---------------------------------------------------------------------------\n\ninterface PendingFlush {\n\tindexPath: string;\n\tstate: SessionIndexFile;\n}\n\nlet pendingFlush: PendingFlush | undefined;\nlet flushTimer: ReturnType<typeof setTimeout> | undefined;\nlet exitHookRegistered = false;\n\nfunction clearPending(): void {\n\tpendingFlush = undefined;\n\tif (flushTimer !== undefined) {\n\t\tclearTimeout(flushTimer);\n\t\tflushTimer = undefined;\n\t}\n}\n\nfunction ensureExitFlushHook(): void {\n\tif (exitHookRegistered) return;\n\texitHookRegistered = true;\n\tprocess.once(\"exit\", () => {\n\t\tflushSessionIndexNow();\n\t});\n}\n\nfunction armDirty(indexPath: string, state: SessionIndexFile): void {\n\tpendingFlush = { indexPath, state };\n\tensureExitFlushHook();\n\tif (flushTimer === undefined) {\n\t\tflushTimer = setTimeout(() => {\n\t\t\tflushTimer = undefined;\n\t\t\tflushSessionIndexNow();\n\t\t}, FLUSH_THROTTLE_MS);\n\t\tflushTimer.unref();\n\t}\n}\n\n/** 立即落盘脏状态(测试与 exit 钩子共用);无脏状态时为 no-op。 */\nexport function flushSessionIndexNow(): void {\n\tconst pending = pendingFlush;\n\tif (!pending) return;\n\tclearPending();\n\ttry {\n\t\twriteStateSync(pending.indexPath, pending.state);\n\t} catch {\n\t\t// 失败留给对账自愈(设计 §6),不向会话写入路径传播。\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// 读写原语\n// ---------------------------------------------------------------------------\n\nfunction emptyState(): SessionIndexFile {\n\treturn { version: SESSION_INDEX_VERSION, updated_at_ms: 0, projects: [], sessions: [] };\n}\n\nfunction safeMs(value: number): number {\n\treturn Number.isFinite(value) ? value : 0;\n}\n\nfunction isSessionFileName(file: string): boolean {\n\treturn file.endsWith(\".jsonl\") && !file.endsWith(\".trace.jsonl\");\n}\n\nfunction isFiniteNumber(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value);\n}\n\n/**\n * 条目级最小形状校验:必填字段类型/finite 不符的条目直接丢弃(视同缺失,交给\n * 对账/重建自愈,覆盖设计 §2 目标 3 的字段级损坏);可选字段仅在为 string 时\n * 保留,否则按键缺席处理。\n */\nfunction coerceSessionEntry(value: unknown): SessionIndexSessionEntry | null {\n\tif (typeof value !== \"object\" || value === null) return null;\n\tconst raw = value as Record<string, unknown>;\n\tif (typeof raw.file !== \"string\" || typeof raw.dir !== \"string\" || typeof raw.id !== \"string\") return null;\n\tif (typeof raw.cwd !== \"string\") return null;\n\tif (!isFiniteNumber(raw.created_ms) || !isFiniteNumber(raw.modified_ms) || !isFiniteNumber(raw.message_count)) {\n\t\treturn null;\n\t}\n\tif (typeof raw.first_message !== \"string\") return null;\n\treturn {\n\t\tfile: raw.file,\n\t\tdir: raw.dir,\n\t\tid: raw.id,\n\t\tcwd: raw.cwd,\n\t\t...(isFiniteNumber(raw.file_mtime_ms) ? { file_mtime_ms: raw.file_mtime_ms } : {}),\n\t\t...(typeof raw.name === \"string\" ? { name: raw.name } : {}),\n\t\t...(typeof raw.parent_session_path === \"string\" ? { parent_session_path: raw.parent_session_path } : {}),\n\t\t...(typeof raw.profile_id === \"string\" ? { profile_id: raw.profile_id } : {}),\n\t\t...(typeof raw.profile_version === \"string\" ? { profile_version: raw.profile_version } : {}),\n\t\tcreated_ms: raw.created_ms,\n\t\tmodified_ms: raw.modified_ms,\n\t\tmessage_count: raw.message_count,\n\t\tfirst_message: raw.first_message,\n\t};\n}\n\nfunction coerceProjectEntry(value: unknown): SessionIndexProjectEntry | null {\n\tif (typeof value !== \"object\" || value === null) return null;\n\tconst raw = value as Record<string, unknown>;\n\t// 项目条目只有 dir/cwd 关系;last_active 由成员条目 modified_ms 服务时派生\n\t// (设计 §3),入库时的任何附加键一律忽略。\n\tif (typeof raw.dir !== \"string\" || typeof raw.cwd !== \"string\") return null;\n\treturn { dir: raw.dir, cwd: raw.cwd };\n}\n\n/** 读取并校验索引文件;不存在/JSON 损坏/版本不符返回 null(由调用方重建或忽略)。 */\nfunction readIndexFile(indexPath: string): SessionIndexFile | null {\n\tlet content: string;\n\ttry {\n\t\tcontent = readFileSync(indexPath, \"utf8\");\n\t} catch {\n\t\treturn null;\n\t}\n\ttry {\n\t\tconst parsed = JSON.parse(content) as { version?: unknown; projects?: unknown; sessions?: unknown };\n\t\tif (parsed.version !== SESSION_INDEX_VERSION) return null;\n\t\tif (!Array.isArray(parsed.projects) || !Array.isArray(parsed.sessions)) return null;\n\t\tconst projects = parsed.projects.map(coerceProjectEntry).filter((entry): entry is SessionIndexProjectEntry => {\n\t\t\treturn entry !== null;\n\t\t});\n\t\tconst sessions = parsed.sessions.map(coerceSessionEntry).filter((entry): entry is SessionIndexSessionEntry => {\n\t\t\treturn entry !== null;\n\t\t});\n\t\treturn { version: SESSION_INDEX_VERSION, updated_at_ms: 0, projects, sessions };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/** 原子写(temp + rename,同 ecosystem/state.ts 模式);同步 API 供 exit 钩子共用。 */\nfunction writeStateSync(indexPath: string, state: SessionIndexFile): void {\n\tconst parent = dirname(indexPath);\n\tmkdirSync(parent, { recursive: true });\n\tconst temporaryPath = join(parent, `.${basename(indexPath)}.${randomUUID()}.tmp`);\n\ttry {\n\t\tstate.updated_at_ms = Date.now();\n\t\twriteFileSync(temporaryPath, `${JSON.stringify(state)}\\n`, \"utf8\");\n\t\trenameSync(temporaryPath, indexPath);\n\t} finally {\n\t\ttry {\n\t\t\tunlinkSync(temporaryPath);\n\t\t} catch {\n\t\t\t// rename 成功后 tmp 已不存在;清理失败不掩盖写入结果。\n\t\t}\n\t}\n}\n\n/** 可变状态:优先续用未落盘的脏快照,避免节流窗口内的读-改-写互相覆盖。 */\nfunction loadMutableState(indexPath: string): SessionIndexFile {\n\tif (pendingFlush?.indexPath === indexPath) return pendingFlush.state;\n\treturn readIndexFile(indexPath) ?? emptyState();\n}\n\n/**\n * 解析会话路径在索引中的位置:dir = 相对 sessions 根的父目录名(不从 cwd 重推导,\n * 规避 Windows 盘符大小写陷阱,设计 §3),file = basename。\n * sessions 根之外、根一级散落文件、非 `*.jsonl` 与 `*.trace.jsonl` 一律返回 null。\n */\nfunction locationInRoot(sessionsRoot: string, sessionPath: string): { dir: string; file: string } | null {\n\tconst rel = relative(sessionsRoot, sessionPath);\n\tif (rel === \"\" || rel.startsWith(\"..\") || isAbsolute(rel)) return null;\n\tconst dir = dirname(rel);\n\tif (dir === \".\" || dir === \"..\") return null;\n\tconst file = basename(rel);\n\tif (!isSessionFileName(file)) return null;\n\treturn { dir, file };\n}\n\nfunction findSessionEntry(state: SessionIndexFile, dir: string, file: string): SessionIndexSessionEntry | undefined {\n\treturn state.sessions.find((entry) => entry.dir === dir && entry.file === file);\n}\n\nfunction touchProject(state: SessionIndexFile, dir: string, cwd: string): void {\n\tif (state.projects.some((project) => project.dir === dir)) return;\n\tstate.projects.push({ dir, cwd });\n}\n\nfunction addSessionFromInfo(\n\tstate: SessionIndexFile,\n\tdir: string,\n\tfile: string,\n\tinfo: SessionInfo,\n\tmtimeMs?: number,\n): void {\n\tstate.sessions = state.sessions.filter((entry) => !(entry.dir === dir && entry.file === file));\n\tstate.sessions.push({\n\t\tfile,\n\t\tdir,\n\t\tid: info.id,\n\t\tcwd: info.cwd,\n\t\t...(info.name === undefined ? {} : { name: info.name }),\n\t\t...(info.parentSessionPath === undefined ? {} : { parent_session_path: info.parentSessionPath }),\n\t\t...(info.profileId === undefined ? {} : { profile_id: info.profileId }),\n\t\t...(info.profileVersion === undefined ? {} : { profile_version: info.profileVersion }),\n\t\t...(mtimeMs === undefined ? {} : { file_mtime_ms: mtimeMs }),\n\t\tcreated_ms: safeMs(info.created.getTime()),\n\t\tmodified_ms: safeMs(info.modified.getTime()),\n\t\tmessage_count: info.messageCount,\n\t\tfirst_message: info.firstMessage,\n\t});\n\ttouchProject(state, dir, info.cwd);\n}\n\n// ---------------------------------------------------------------------------\n// 写路径(设计 §4)\n// ---------------------------------------------------------------------------\n\n/**\n * upsert 单会话条目并按 immediate 决定落盘时机。预热优化(设计 §4):条目缺失\n * 或过期由读路径的 mtime 令牌对账刷新,不承担正确性。内部错误一律吞掉\n * (fail-open):索引是投影,写入失败由下次读取对账自愈,绝不影响会话持久化。\n */\nexport function recordSessionUpdate(input: SessionIndexUpdateInput): void {\n\ttry {\n\t\tconst sessionsRoot = getSessionsDir();\n\t\tconst location = locationInRoot(sessionsRoot, input.sessionPath);\n\t\tif (!location) return;\n\t\t// 缓存有效期令牌 = 当前文件 mtime;stat 失败则条目不带令牌(视为过期,无害)。\n\t\tlet mtimeMs: number | undefined;\n\t\ttry {\n\t\t\tmtimeMs = statSync(input.sessionPath).mtimeMs;\n\t\t} catch {\n\t\t\tmtimeMs = undefined;\n\t\t}\n\t\tconst indexPath = getSessionIndexPath();\n\t\tconst state = loadMutableState(indexPath);\n\t\tstate.sessions = state.sessions.filter((entry) => !(entry.dir === location.dir && entry.file === location.file));\n\t\tconst modifiedMs = safeMs(input.modifiedMs);\n\t\tstate.sessions.push({\n\t\t\tfile: location.file,\n\t\t\tdir: location.dir,\n\t\t\tid: input.sessionId,\n\t\t\tcwd: input.cwd,\n\t\t\t...(input.name === undefined ? {} : { name: input.name }),\n\t\t\t...(input.parentSessionPath === undefined ? {} : { parent_session_path: input.parentSessionPath }),\n\t\t\t...(input.profileId === undefined ? {} : { profile_id: input.profileId }),\n\t\t\t...(input.profileVersion === undefined ? {} : { profile_version: input.profileVersion }),\n\t\t\t...(mtimeMs === undefined ? {} : { file_mtime_ms: mtimeMs }),\n\t\t\tcreated_ms: safeMs(input.createdMs),\n\t\t\tmodified_ms: modifiedMs,\n\t\t\tmessage_count: input.messageCount,\n\t\t\tfirst_message: input.firstMessage,\n\t\t});\n\t\ttouchProject(state, location.dir, input.cwd);\n\t\tif (input.immediate) {\n\t\t\tclearPending();\n\t\t\twriteStateSync(indexPath, state);\n\t\t} else {\n\t\t\tarmDirty(indexPath, state);\n\t\t}\n\t} catch {\n\t\t// 索引维护绝不影响会话写入(设计 §4/§6)。\n\t}\n}\n\n/**\n * 按路径移除条目并立即原子落盘;索引不存在/损坏/路径不在 sessions 根时静默 no-op\n * (不抛错、不重建,设计 §6)。\n */\nexport function removeSessionFromIndex(sessionPath: string): void {\n\ttry {\n\t\tconst location = locationInRoot(getSessionsDir(), sessionPath);\n\t\tif (!location) return;\n\t\tconst indexPath = getSessionIndexPath();\n\t\tconst state = loadMutableState(indexPath);\n\t\tconst before = state.sessions.length;\n\t\tstate.sessions = state.sessions.filter((entry) => !(entry.dir === location.dir && entry.file === location.file));\n\t\tif (state.sessions.length === before) return;\n\t\tclearPending();\n\t\twriteStateSync(indexPath, state);\n\t} catch {\n\t\t// 对账会在下次读取自愈(设计 §6)。\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// 读路径与对账(设计 §5)\n// ---------------------------------------------------------------------------\n\nfunction sessionInfoFromEntry(sessionsRoot: string, entry: SessionIndexSessionEntry): SessionInfo {\n\treturn {\n\t\tpath: join(sessionsRoot, entry.dir, entry.file),\n\t\tid: entry.id,\n\t\tcwd: entry.cwd,\n\t\t...(entry.name === undefined ? {} : { name: entry.name }),\n\t\t...(entry.parent_session_path === undefined ? {} : { parentSessionPath: entry.parent_session_path }),\n\t\t...(entry.profile_id === undefined ? {} : { profileId: entry.profile_id }),\n\t\t...(entry.profile_version === undefined ? {} : { profileVersion: entry.profile_version }),\n\t\tcreated: new Date(entry.created_ms),\n\t\tmodified: new Date(entry.modified_ms),\n\t\tmessageCount: entry.message_count,\n\t\tfirstMessage: entry.first_message,\n\t\tallMessagesText: \"\",\n\t};\n}\n\nfunction sortedSessions(sessionsRoot: string, state: SessionIndexFile): SessionInfo[] {\n\tconst ordered = [...state.sessions].sort((a, b) => b.modified_ms - a.modified_ms || b.file.localeCompare(a.file));\n\treturn ordered.map((entry) => sessionInfoFromEntry(sessionsRoot, entry));\n}\n\n/** 重建路径:由全量扫描结果构建索引状态,逐文件 stat 写入 mtime 令牌。 */\nasync function stateFromSessions(sessionsRoot: string, sessions: SessionInfo[]): Promise<SessionIndexFile> {\n\tconst state = emptyState();\n\tfor (const info of sessions) {\n\t\tconst location = locationInRoot(sessionsRoot, info.path);\n\t\tif (!location) continue;\n\t\tlet mtimeMs: number | undefined;\n\t\ttry {\n\t\t\tmtimeMs = (await stat(info.path)).mtimeMs;\n\t\t} catch {\n\t\t\tmtimeMs = undefined;\n\t\t}\n\t\taddSessionFromInfo(state, location.dir, location.file, info, mtimeMs);\n\t}\n\treturn state;\n}\n\n/**\n * 对账:目录名集合 diff + 目录内文件名集合 diff,逐文件 stat 与条目的\n * file_mtime_ms 令牌比对(设计 §5)——条目缺失 → buildInfo 补;mtime 与令牌\n * 一致 → 缓存可信直接沿用;不一致或令牌缺失 → buildInfo 重读刷新缓存与新令牌\n * (重读失败 = 文件已不再是会话 → 剔除陈旧条目);索引有、磁盘无 → 剔除;\n * 消失目录连同其 project 行剔除。返回是否有变更。\n */\nasync function reconcileIndex(\n\tsessionsRoot: string,\n\tstate: SessionIndexFile,\n\tbuildInfo: (path: string) => Promise<SessionInfo | null>,\n\tsignal?: AbortSignal,\n): Promise<boolean> {\n\t// 磁盘清单:目录名 → (文件名 → stat mtime)。stat 失败(readdir 与 stat 之间\n\t// 被删/不可读)视同本轮缺席,下轮对账收敛。\n\tconst diskDirs = new Map<string, Map<string, number>>();\n\tlet rootEntries: Dirent[];\n\ttry {\n\t\trootEntries = await readdir(sessionsRoot, { withFileTypes: true });\n\t} catch {\n\t\t// sessions 根不可读视同空盘:剔除全部条目(索引是投影,根恢复后重建)。\n\t\trootEntries = [];\n\t}\n\tfor (const entry of rootEntries) {\n\t\tif (entry.name === AGENT_SESSIONS_DIR_NAME) continue;\n\t\tif (!entry.isDirectory() && !entry.isSymbolicLink()) continue;\n\t\tlet fileNames: string[];\n\t\ttry {\n\t\t\tfileNames = await readdir(join(sessionsRoot, entry.name));\n\t\t} catch {\n\t\t\t// 目录不可读视同已清空,其索引条目被剔除(悬挂容忍,设计 §6)。\n\t\t\tfileNames = [];\n\t\t}\n\t\tconst files = new Map<string, number>();\n\t\tfor (const file of fileNames) {\n\t\t\tif (!isSessionFileName(file)) continue;\n\t\t\tsignal?.throwIfAborted();\n\t\t\ttry {\n\t\t\t\tfiles.set(file, (await stat(join(sessionsRoot, entry.name, file))).mtimeMs);\n\t\t\t} catch {\n\t\t\t\t// 本轮视同缺席。\n\t\t\t}\n\t\t}\n\t\tdiskDirs.set(entry.name, files);\n\t}\n\n\tlet changed = false;\n\n\tfor (const [dir, files] of diskDirs) {\n\t\tfor (const [file, mtimeMs] of files) {\n\t\t\tconst existing = findSessionEntry(state, dir, file);\n\t\t\tif (existing && existing.file_mtime_ms !== undefined && existing.file_mtime_ms === mtimeMs) continue;\n\t\t\tsignal?.throwIfAborted();\n\t\t\tconst info = await buildInfo(join(sessionsRoot, dir, file));\n\t\t\tif (!info) {\n\t\t\t\tif (existing) {\n\t\t\t\t\t// 文件已不再解析为会话(损坏/改写):剔除陈旧条目。\n\t\t\t\t\tstate.sessions = state.sessions.filter((candidate) => candidate !== existing);\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\taddSessionFromInfo(state, dir, file, info, mtimeMs);\n\t\t\tchanged = true;\n\t\t}\n\t}\n\n\tconst keptSessions = state.sessions.filter((entry) => diskDirs.get(entry.dir)?.has(entry.file));\n\tif (keptSessions.length !== state.sessions.length) {\n\t\tstate.sessions = keptSessions;\n\t\tchanged = true;\n\t}\n\n\tconst keptProjects = state.projects.filter((project) => diskDirs.has(project.dir));\n\tif (keptProjects.length !== state.projects.length) {\n\t\tstate.projects = keptProjects;\n\t\tchanged = true;\n\t}\n\n\treturn changed;\n}\n\n/**\n * listAll 默认布局分支的索引读路径。返回 undefined 表示索引层意外失败(IO 等),\n * 调用方应回退到旧全量扫描(fail-open,设计 §5);中止按惯例抛 AbortError。\n */\nexport async function listSessionsViaIndex(input: ListSessionsViaIndexInput): Promise<SessionInfo[] | undefined> {\n\tconst { sessionsRoot, buildInfo, scanAll, onHit, signal } = input;\n\ttry {\n\t\tsignal?.throwIfAborted();\n\t\tconst indexPath = getSessionIndexPath();\n\t\tlet state: SessionIndexFile;\n\t\tif (pendingFlush?.indexPath === indexPath) {\n\t\t\t// 节流窗口内已积累的更新是对账基准:对账落盘不得把窗口内更新回退覆盖。\n\t\t\tstate = pendingFlush.state;\n\t\t} else {\n\t\t\tconst fromDisk = readIndexFile(indexPath);\n\t\t\tif (fromDisk === null) {\n\t\t\t\t// 缺失/损坏/版本不符 → 旧扫描全量重建并落盘(与旧行为同价,设计 §5.1)。\n\t\t\t\tconst sessions = await scanAll();\n\t\t\t\twriteStateSync(indexPath, await stateFromSessions(sessionsRoot, sessions));\n\t\t\t\treturn sessions;\n\t\t\t}\n\t\t\tstate = fromDisk;\n\t\t}\n\t\tconst changed = await reconcileIndex(sessionsRoot, state, buildInfo, signal);\n\t\tif (changed) {\n\t\t\tif (pendingFlush?.indexPath === indexPath) clearPending();\n\t\t\twriteStateSync(indexPath, state);\n\t\t}\n\t\tconst sessions = sortedSessions(sessionsRoot, state);\n\t\tonHit?.(sessions);\n\t\treturn sessions;\n\t} catch {\n\t\tsignal?.throwIfAborted();\n\t\treturn undefined;\n\t}\n}\n"]}
|