@ai-agent-forge/agent-forge 0.86.7 → 0.87.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 +18 -0
- package/README.md +8 -9
- package/dist/artifacts/artifact-store.d.ts.map +1 -1
- package/dist/artifacts/artifact-store.js +9 -5
- package/dist/artifacts/artifact-store.js.map +1 -1
- package/dist/bundle/.build-manifest.json +410 -371
- package/dist/bundle/chunks/{chunk-A7GUOX2H.js → chunk-6ZKDNZHA.js} +5 -1
- package/dist/bundle/chunks/chunk-BFC2BDVJ.js +13 -0
- package/dist/bundle/chunks/chunk-MOHOLL3F.js +1456 -0
- package/dist/bundle/chunks/image-resize-worker.js +12 -1
- package/dist/bundle/chunks/{virtual-modules-3IZ4MYSQ.js → virtual-modules-KZ5DRH7Q.js} +1 -1
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/client.js +1 -1
- package/dist/bundle/index.js +2 -2
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +231 -185
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/auth-check.d.ts.map +1 -1
- package/dist/cli/auth-check.js +3 -2
- package/dist/cli/auth-check.js.map +1 -1
- package/dist/cli/auth-command.d.ts.map +1 -1
- package/dist/cli/auth-command.js +15 -9
- package/dist/cli/auth-command.js.map +1 -1
- package/dist/cli/credential-print.d.ts.map +1 -1
- package/dist/cli/credential-print.js +14 -7
- package/dist/cli/credential-print.js.map +1 -1
- package/dist/cli/experimental/command.d.ts.map +1 -1
- package/dist/cli/experimental/command.js +6 -5
- package/dist/cli/experimental/command.js.map +1 -1
- package/dist/cli/list-models.d.ts.map +1 -1
- package/dist/cli/list-models.js +9 -8
- package/dist/cli/list-models.js.map +1 -1
- package/dist/cli/plugin-manager-controller.d.ts.map +1 -1
- package/dist/cli/plugin-manager-controller.js +2 -1
- package/dist/cli/plugin-manager-controller.js.map +1 -1
- package/dist/cli/serve.d.ts.map +1 -1
- package/dist/cli/serve.js +8 -5
- package/dist/cli/serve.js.map +1 -1
- package/dist/client/remote-session.d.ts.map +1 -1
- package/dist/client/remote-session.js +16 -9
- package/dist/client/remote-session.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/core/agent-capability-contract.d.ts +0 -7
- package/dist/core/agent-capability-contract.d.ts.map +1 -1
- package/dist/core/agent-capability-contract.js +72 -68
- package/dist/core/agent-capability-contract.js.map +1 -1
- package/dist/core/agent-session-bash.d.ts.map +1 -1
- package/dist/core/agent-session-bash.js +5 -4
- 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 +28 -22
- package/dist/core/agent-session-compaction.js.map +1 -1
- package/dist/core/agent-session-context.d.ts.map +1 -1
- package/dist/core/agent-session-context.js +18 -13
- package/dist/core/agent-session-context.js.map +1 -1
- package/dist/core/agent-session-model.d.ts.map +1 -1
- package/dist/core/agent-session-model.js +2 -1
- package/dist/core/agent-session-model.js.map +1 -1
- package/dist/core/agent-session-retry.d.ts.map +1 -1
- package/dist/core/agent-session-retry.js +3 -2
- package/dist/core/agent-session-retry.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +7 -6
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +1 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session-tools.d.ts.map +1 -1
- package/dist/core/agent-session-tools.js +3 -2
- package/dist/core/agent-session-tools.js.map +1 -1
- package/dist/core/agent-session.d.ts +40 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +142 -34
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-guidance.d.ts.map +1 -1
- package/dist/core/auth-guidance.js +11 -5
- package/dist/core/auth-guidance.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +9 -6
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +2 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/embedded-sdk.d.ts.map +1 -1
- package/dist/core/embedded-sdk.js +10 -6
- package/dist/core/embedded-sdk.js.map +1 -1
- package/dist/core/export-html/index.d.ts.map +1 -1
- package/dist/core/export-html/index.js +9 -4
- package/dist/core/export-html/index.js.map +1 -1
- package/dist/core/extensions/agent-composition.d.ts.map +1 -1
- package/dist/core/extensions/agent-composition.js +26 -19
- package/dist/core/extensions/agent-composition.js.map +1 -1
- package/dist/core/extensions/bootstrap-agent-loop.d.ts.map +1 -1
- package/dist/core/extensions/bootstrap-agent-loop.js +3 -2
- package/dist/core/extensions/bootstrap-agent-loop.js.map +1 -1
- package/dist/core/extensions/bootstrap-lifecycle.d.ts.map +1 -1
- package/dist/core/extensions/bootstrap-lifecycle.js +20 -17
- package/dist/core/extensions/bootstrap-lifecycle.js.map +1 -1
- package/dist/core/extensions/bootstrap-overflow-policy.d.ts.map +1 -1
- package/dist/core/extensions/bootstrap-overflow-policy.js +2 -1
- package/dist/core/extensions/bootstrap-overflow-policy.js.map +1 -1
- package/dist/core/extensions/bootstrap-retry-policy.d.ts.map +1 -1
- package/dist/core/extensions/bootstrap-retry-policy.js +2 -1
- package/dist/core/extensions/bootstrap-retry-policy.js.map +1 -1
- package/dist/core/extensions/bootstrap-skills.d.ts.map +1 -1
- package/dist/core/extensions/bootstrap-skills.js +7 -6
- package/dist/core/extensions/bootstrap-skills.js.map +1 -1
- package/dist/core/extensions/bootstrap-tools.d.ts.map +1 -1
- package/dist/core/extensions/bootstrap-tools.js +2 -1
- package/dist/core/extensions/bootstrap-tools.js.map +1 -1
- package/dist/core/extensions/capability-plugin.d.ts.map +1 -1
- package/dist/core/extensions/capability-plugin.js +37 -13
- package/dist/core/extensions/capability-plugin.js.map +1 -1
- package/dist/core/extensions/capability-runtime-events.d.ts.map +1 -1
- package/dist/core/extensions/capability-runtime-events.js +17 -13
- package/dist/core/extensions/capability-runtime-events.js.map +1 -1
- package/dist/core/extensions/capability-runtime-guards.d.ts.map +1 -1
- package/dist/core/extensions/capability-runtime-guards.js +16 -15
- package/dist/core/extensions/capability-runtime-guards.js.map +1 -1
- package/dist/core/extensions/capability-runtime-manifest.d.ts.map +1 -1
- package/dist/core/extensions/capability-runtime-manifest.js +52 -30
- package/dist/core/extensions/capability-runtime-manifest.js.map +1 -1
- package/dist/core/extensions/capability-runtime-validation.d.ts.map +1 -1
- package/dist/core/extensions/capability-runtime-validation.js +66 -65
- package/dist/core/extensions/capability-runtime-validation.js.map +1 -1
- package/dist/core/extensions/capability-runtime.d.ts.map +1 -1
- package/dist/core/extensions/capability-runtime.js +147 -127
- package/dist/core/extensions/capability-runtime.js.map +1 -1
- package/dist/core/extensions/compaction-memory-failure-mode.d.ts.map +1 -1
- package/dist/core/extensions/compaction-memory-failure-mode.js +3 -2
- package/dist/core/extensions/compaction-memory-failure-mode.js.map +1 -1
- package/dist/core/extensions/context-strategy-binding.d.ts.map +1 -1
- package/dist/core/extensions/context-strategy-binding.js +7 -4
- package/dist/core/extensions/context-strategy-binding.js.map +1 -1
- package/dist/core/extensions/lifecycle-catalog.d.ts.map +1 -1
- package/dist/core/extensions/lifecycle-catalog.js +290 -279
- package/dist/core/extensions/lifecycle-catalog.js.map +1 -1
- package/dist/core/extensions/lifecycle-delivery-queue.d.ts.map +1 -1
- package/dist/core/extensions/lifecycle-delivery-queue.js +3 -2
- package/dist/core/extensions/lifecycle-delivery-queue.js.map +1 -1
- package/dist/core/facade/coding-agent-facade.d.ts +6 -0
- package/dist/core/facade/coding-agent-facade.d.ts.map +1 -1
- package/dist/core/facade/coding-agent-facade.js +2 -0
- package/dist/core/facade/coding-agent-facade.js.map +1 -1
- package/dist/core/http-dispatcher.d.ts.map +1 -1
- package/dist/core/http-dispatcher.js +2 -1
- package/dist/core/http-dispatcher.js.map +1 -1
- package/dist/core/i18n/index.d.ts +14 -0
- package/dist/core/i18n/index.d.ts.map +1 -0
- package/dist/core/i18n/index.js +67 -0
- package/dist/core/i18n/index.js.map +1 -0
- package/dist/core/i18n/locales/en/example.d.ts +7 -0
- package/dist/core/i18n/locales/en/example.d.ts.map +1 -0
- package/dist/core/i18n/locales/en/example.js +7 -0
- package/dist/core/i18n/locales/en/example.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/cli.d.ts +8 -0
- package/dist/core/i18n/locales/zh-cn/cli.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/cli.js +227 -0
- package/dist/core/i18n/locales/zh-cn/cli.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/common.d.ts +6 -0
- package/dist/core/i18n/locales/zh-cn/common.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/common.js +6 -0
- package/dist/core/i18n/locales/zh-cn/common.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-core.d.ts +13 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-core.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-core.js +173 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-core.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-extensions.d.ts +12 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-extensions.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-extensions.js +582 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-extensions.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-final.d.ts +9 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-final.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-final.js +55 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-final.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-plugins.d.ts +11 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-plugins.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-plugins.js +455 -0
- package/dist/core/i18n/locales/zh-cn/core-contract-plugins.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/core-errors.d.ts +9 -0
- package/dist/core/i18n/locales/zh-cn/core-errors.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/core-errors.js +191 -0
- package/dist/core/i18n/locales/zh-cn/core-errors.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/example.d.ts +7 -0
- package/dist/core/i18n/locales/zh-cn/example.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/example.js +10 -0
- package/dist/core/i18n/locales/zh-cn/example.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/index.d.ts +2 -0
- package/dist/core/i18n/locales/zh-cn/index.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/index.js +41 -0
- package/dist/core/i18n/locales/zh-cn/index.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/interactive-mode.d.ts +10 -0
- package/dist/core/i18n/locales/zh-cn/interactive-mode.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/interactive-mode.js +265 -0
- package/dist/core/i18n/locales/zh-cn/interactive-mode.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/keybindings.d.ts +11 -0
- package/dist/core/i18n/locales/zh-cn/keybindings.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/keybindings.js +67 -0
- package/dist/core/i18n/locales/zh-cn/keybindings.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/slash-commands.d.ts +7 -0
- package/dist/core/i18n/locales/zh-cn/slash-commands.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/slash-commands.js +39 -0
- package/dist/core/i18n/locales/zh-cn/slash-commands.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/tui-builtin.d.ts +10 -0
- package/dist/core/i18n/locales/zh-cn/tui-builtin.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/tui-builtin.js +15 -0
- package/dist/core/i18n/locales/zh-cn/tui-builtin.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/tui-components-misc.d.ts +13 -0
- package/dist/core/i18n/locales/zh-cn/tui-components-misc.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/tui-components-misc.js +102 -0
- package/dist/core/i18n/locales/zh-cn/tui-components-misc.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/tui-components-selectors.d.ts +12 -0
- package/dist/core/i18n/locales/zh-cn/tui-components-selectors.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/tui-components-selectors.js +288 -0
- package/dist/core/i18n/locales/zh-cn/tui-components-selectors.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/tui-interactive-misc.d.ts +9 -0
- package/dist/core/i18n/locales/zh-cn/tui-interactive-misc.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/tui-interactive-misc.js +79 -0
- package/dist/core/i18n/locales/zh-cn/tui-interactive-misc.js.map +1 -0
- package/dist/core/i18n/locales/zh-cn/tui-self-owned.d.ts +8 -0
- package/dist/core/i18n/locales/zh-cn/tui-self-owned.d.ts.map +1 -0
- package/dist/core/i18n/locales/zh-cn/tui-self-owned.js +72 -0
- package/dist/core/i18n/locales/zh-cn/tui-self-owned.js.map +1 -0
- package/dist/core/keybindings.d.ts +15 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +18 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/logger.d.ts.map +1 -1
- package/dist/core/logger.js +5 -1
- package/dist/core/logger.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +2 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +8 -4
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/package-install-host.d.ts.map +1 -1
- package/dist/core/package-install-host.js +4 -3
- package/dist/core/package-install-host.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +15 -10
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/package-resource-collection.d.ts.map +1 -1
- package/dist/core/package-resource-collection.js +2 -1
- package/dist/core/package-resource-collection.js.map +1 -1
- package/dist/core/package-source.d.ts.map +1 -1
- package/dist/core/package-source.js +3 -2
- package/dist/core/package-source.js.map +1 -1
- package/dist/core/provider-composer.d.ts.map +1 -1
- package/dist/core/provider-composer.js +22 -13
- package/dist/core/provider-composer.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +6 -2
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/resolve-config-value.d.ts.map +1 -1
- package/dist/core/resolve-config-value.js +15 -5
- package/dist/core/resolve-config-value.js.map +1 -1
- package/dist/core/sdk.d.ts +19 -5
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +74 -54
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-cwd.d.ts.map +1 -1
- package/dist/core/session-cwd.js +5 -2
- package/dist/core/session-cwd.js.map +1 -1
- package/dist/core/session-export.d.ts.map +1 -1
- package/dist/core/session-export.js +2 -1
- package/dist/core/session-export.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +39 -29
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-storage.d.ts.map +1 -1
- package/dist/core/session-storage.js +3 -3
- package/dist/core/session-storage.js.map +1 -1
- package/dist/core/settings-manager.d.ts +11 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +27 -5
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +53 -32
- 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 +25 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +11 -7
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +9 -4
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +4 -3
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +3 -2
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +2 -1
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +6 -2
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +2 -1
- package/dist/core/tools/write.js.map +1 -1
- package/dist/durable/durable-service.d.ts.map +1 -1
- package/dist/durable/durable-service.js +25 -18
- package/dist/durable/durable-service.js.map +1 -1
- package/dist/host/checkpoint-commit.d.ts.map +1 -1
- package/dist/host/checkpoint-commit.js +2 -1
- package/dist/host/checkpoint-commit.js.map +1 -1
- package/dist/host/manual-compaction.d.ts.map +1 -1
- package/dist/host/manual-compaction.js +2 -1
- package/dist/host/manual-compaction.js.map +1 -1
- package/dist/host/output-projection.d.ts +0 -13
- package/dist/host/output-projection.d.ts.map +1 -1
- package/dist/host/output-projection.js +16 -2
- package/dist/host/output-projection.js.map +1 -1
- package/dist/host/summary-retry.d.ts.map +1 -1
- package/dist/host/summary-retry.js +3 -2
- package/dist/host/summary-retry.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +94 -58
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/agents-view.d.ts.map +1 -1
- package/dist/modes/interactive/components/agents-view.js +18 -15
- package/dist/modes/interactive/components/agents-view.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +6 -5
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +12 -7
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.js +2 -1
- package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +6 -5
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +9 -5
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +18 -15
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/custom-entry.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-entry.js +2 -1
- package/dist/modes/interactive/components/custom-entry.js.map +1 -1
- package/dist/modes/interactive/components/dialog-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/dialog-editor.js +5 -4
- package/dist/modes/interactive/components/dialog-editor.js.map +1 -1
- package/dist/modes/interactive/components/dialog-input.d.ts.map +1 -1
- package/dist/modes/interactive/components/dialog-input.js +2 -1
- package/dist/modes/interactive/components/dialog-input.js.map +1 -1
- package/dist/modes/interactive/components/dialog-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/dialog-selector.js +4 -3
- package/dist/modes/interactive/components/dialog-selector.js.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.js +11 -10
- package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +7 -4
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +3 -2
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +12 -11
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/mermaid.d.ts.map +1 -1
- package/dist/modes/interactive/components/mermaid.js +6 -2
- package/dist/modes/interactive/components/mermaid.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +28 -16
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +10 -9
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/plugin-manager-entry.d.ts.map +1 -1
- package/dist/modes/interactive/components/plugin-manager-entry.js +22 -13
- package/dist/modes/interactive/components/plugin-manager-entry.js.map +1 -1
- package/dist/modes/interactive/components/plugin-selection-entry.d.ts.map +1 -1
- package/dist/modes/interactive/components/plugin-selection-entry.js +27 -12
- package/dist/modes/interactive/components/plugin-selection-entry.js.map +1 -1
- package/dist/modes/interactive/components/queue-manager-selector.d.ts +60 -0
- package/dist/modes/interactive/components/queue-manager-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/queue-manager-selector.js +166 -0
- package/dist/modes/interactive/components/queue-manager-selector.js.map +1 -0
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +16 -14
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +43 -31
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +113 -96
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-submenu.js +5 -4
- package/dist/modes/interactive/components/settings-submenu.js.map +1 -1
- package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/show-images-selector.js +3 -2
- package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.js +4 -3
- package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/dist/modes/interactive/components/status-indicator.d.ts.map +1 -1
- package/dist/modes/interactive/components/status-indicator.js +14 -5
- package/dist/modes/interactive/components/status-indicator.js.map +1 -1
- package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/theme-selector.js +2 -1
- package/dist/modes/interactive/components/theme-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +11 -4
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +8 -3
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tree-list.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-list.js +7 -6
- package/dist/modes/interactive/components/tree-list.js.map +1 -1
- package/dist/modes/interactive/components/tree-render.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-render.js +28 -23
- package/dist/modes/interactive/components/tree-render.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +9 -7
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.js +5 -4
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
- package/dist/modes/interactive/components/workflow-view.d.ts.map +1 -1
- package/dist/modes/interactive/components/workflow-view.js +36 -21
- package/dist/modes/interactive/components/workflow-view.js.map +1 -1
- package/dist/modes/interactive/external-editor.d.ts.map +1 -1
- package/dist/modes/interactive/external-editor.js +2 -1
- package/dist/modes/interactive/external-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-auth.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-auth.js +71 -40
- package/dist/modes/interactive/interactive-auth.js.map +1 -1
- package/dist/modes/interactive/interactive-commands.d.ts +1 -0
- package/dist/modes/interactive/interactive-commands.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-commands.js +3 -0
- package/dist/modes/interactive/interactive-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-dialog-ui.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-dialog-ui.js +5 -5
- package/dist/modes/interactive/interactive-dialog-ui.js.map +1 -1
- package/dist/modes/interactive/interactive-format.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-format.js +34 -18
- package/dist/modes/interactive/interactive-format.js.map +1 -1
- package/dist/modes/interactive/interactive-helpers.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-helpers.js +7 -2
- package/dist/modes/interactive/interactive-helpers.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +4 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +327 -243
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/interactive-selectors.d.ts +9 -0
- package/dist/modes/interactive/interactive-selectors.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-selectors.js +79 -25
- package/dist/modes/interactive/interactive-selectors.js.map +1 -1
- package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme-controller.js +5 -1
- package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +26 -23
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +3 -2
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +3 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +3 -2
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +28 -13
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/plugins/ecosystem/catalog.d.ts.map +1 -1
- package/dist/plugins/ecosystem/catalog.js +3 -2
- package/dist/plugins/ecosystem/catalog.js.map +1 -1
- package/dist/plugins/ecosystem/cli.d.ts.map +1 -1
- package/dist/plugins/ecosystem/cli.js +2 -1
- package/dist/plugins/ecosystem/cli.js.map +1 -1
- package/dist/plugins/ecosystem/default-plugin-market.d.ts.map +1 -1
- package/dist/plugins/ecosystem/default-plugin-market.js +3 -2
- package/dist/plugins/ecosystem/default-plugin-market.js.map +1 -1
- package/dist/plugins/ecosystem/distribution.d.ts.map +1 -1
- package/dist/plugins/ecosystem/distribution.js +2 -1
- package/dist/plugins/ecosystem/distribution.js.map +1 -1
- package/dist/plugins/ecosystem/installer.d.ts.map +1 -1
- package/dist/plugins/ecosystem/installer.js +3 -2
- package/dist/plugins/ecosystem/installer.js.map +1 -1
- package/dist/plugins/ecosystem/node-disk-registry-source.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-disk-registry-source.js +20 -17
- package/dist/plugins/ecosystem/node-disk-registry-source.js.map +1 -1
- package/dist/plugins/ecosystem/node-distribution-host.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-distribution-host.js +35 -34
- package/dist/plugins/ecosystem/node-distribution-host.js.map +1 -1
- package/dist/plugins/ecosystem/node-npm-registry-source.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-npm-registry-source.js +35 -34
- package/dist/plugins/ecosystem/node-npm-registry-source.js.map +1 -1
- package/dist/plugins/ecosystem/node-package-install-host.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-package-install-host.js +9 -8
- package/dist/plugins/ecosystem/node-package-install-host.js.map +1 -1
- package/dist/plugins/ecosystem/node-package-install-persistence.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-package-install-persistence.js +4 -3
- package/dist/plugins/ecosystem/node-package-install-persistence.js.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-bootstrap.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-bootstrap.js +21 -5
- package/dist/plugins/ecosystem/node-plugin-bootstrap.js.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-trust-root.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-trust-root.js +10 -9
- package/dist/plugins/ecosystem/node-plugin-trust-root.js.map +1 -1
- package/dist/plugins/ecosystem/node-registry-source.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-registry-source.js +31 -30
- package/dist/plugins/ecosystem/node-registry-source.js.map +1 -1
- package/dist/plugins/ecosystem/node-release-package-source.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-release-package-source.js +30 -29
- package/dist/plugins/ecosystem/node-release-package-source.js.map +1 -1
- package/dist/plugins/ecosystem/node-tar-writer.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-tar-writer.js +11 -10
- package/dist/plugins/ecosystem/node-tar-writer.js.map +1 -1
- package/dist/plugins/ecosystem/node-url-package-source.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-url-package-source.js +21 -20
- package/dist/plugins/ecosystem/node-url-package-source.js.map +1 -1
- package/dist/plugins/ecosystem/plugin-acquisition.d.ts.map +1 -1
- package/dist/plugins/ecosystem/plugin-acquisition.js +3 -2
- package/dist/plugins/ecosystem/plugin-acquisition.js.map +1 -1
- package/dist/plugins/ecosystem/registry-cli-adapter.d.ts.map +1 -1
- package/dist/plugins/ecosystem/registry-cli-adapter.js +2 -1
- package/dist/plugins/ecosystem/registry-cli-adapter.js.map +1 -1
- package/dist/plugins/ecosystem/registry.d.ts.map +1 -1
- package/dist/plugins/ecosystem/registry.js +35 -32
- package/dist/plugins/ecosystem/registry.js.map +1 -1
- package/dist/plugins/ecosystem/resolver.d.ts.map +1 -1
- package/dist/plugins/ecosystem/resolver.js +3 -2
- package/dist/plugins/ecosystem/resolver.js.map +1 -1
- package/dist/plugins/ecosystem/selection-store.d.ts.map +1 -1
- package/dist/plugins/ecosystem/selection-store.js +11 -10
- package/dist/plugins/ecosystem/selection-store.js.map +1 -1
- package/dist/plugins/ecosystem/selection.d.ts.map +1 -1
- package/dist/plugins/ecosystem/selection.js +8 -7
- package/dist/plugins/ecosystem/selection.js.map +1 -1
- package/dist/plugins/ecosystem/state.d.ts.map +1 -1
- package/dist/plugins/ecosystem/state.js +24 -23
- package/dist/plugins/ecosystem/state.js.map +1 -1
- package/dist/plugins/ecosystem/updates.d.ts.map +1 -1
- package/dist/plugins/ecosystem/updates.js +5 -4
- package/dist/plugins/ecosystem/updates.js.map +1 -1
- package/dist/plugins/mcp-http.d.ts +0 -17
- package/dist/plugins/mcp-http.d.ts.map +1 -1
- package/dist/plugins/mcp-http.js +30 -7
- package/dist/plugins/mcp-http.js.map +1 -1
- package/dist/plugins/plugin-supervisor.d.ts.map +1 -1
- package/dist/plugins/plugin-supervisor.js +19 -1
- package/dist/plugins/plugin-supervisor.js.map +1 -1
- package/dist/profile/first-roles.d.ts.map +1 -1
- package/dist/profile/first-roles.js +10 -1
- package/dist/profile/first-roles.js.map +1 -1
- package/dist/profile/goal-plugin.d.ts.map +1 -1
- package/dist/profile/goal-plugin.js +24 -5
- package/dist/profile/goal-plugin.js.map +1 -1
- package/dist/profile/identity.d.ts.map +1 -1
- package/dist/profile/identity.js +7 -6
- package/dist/profile/identity.js.map +1 -1
- package/dist/profile/lock.d.ts.map +1 -1
- package/dist/profile/lock.js +15 -1
- package/dist/profile/lock.js.map +1 -1
- package/dist/profile/plan-plugin.d.ts.map +1 -1
- package/dist/profile/plan-plugin.js +26 -13
- package/dist/profile/plan-plugin.js.map +1 -1
- package/dist/profile/plugin-profiles.d.ts.map +1 -1
- package/dist/profile/plugin-profiles.js +2 -1
- package/dist/profile/plugin-profiles.js.map +1 -1
- package/dist/profile/policy-manifest.d.ts.map +1 -1
- package/dist/profile/policy-manifest.js +5 -4
- package/dist/profile/policy-manifest.js.map +1 -1
- package/dist/profile/profile-registry.d.ts +0 -12
- package/dist/profile/profile-registry.d.ts.map +1 -1
- package/dist/profile/profile-registry.js +14 -1
- package/dist/profile/profile-registry.js.map +1 -1
- package/dist/profile/role-catalog.d.ts +0 -1
- package/dist/profile/role-catalog.d.ts.map +1 -1
- package/dist/profile/role-catalog.js +17 -15
- package/dist/profile/role-catalog.js.map +1 -1
- package/dist/profile/role-orchestrator.d.ts.map +1 -1
- package/dist/profile/role-orchestrator.js +8 -7
- package/dist/profile/role-orchestrator.js.map +1 -1
- package/dist/profile/role-runner.d.ts.map +1 -1
- package/dist/profile/role-runner.js +3 -2
- package/dist/profile/role-runner.js.map +1 -1
- package/dist/profile/root-facade.d.ts.map +1 -1
- package/dist/profile/root-facade.js +2 -1
- package/dist/profile/root-facade.js.map +1 -1
- package/dist/profile/skill-discovery-wiring.d.ts.map +1 -1
- package/dist/profile/skill-discovery-wiring.js +7 -2
- package/dist/profile/skill-discovery-wiring.js.map +1 -1
- package/dist/profile/state-store.d.ts.map +1 -1
- package/dist/profile/state-store.js +13 -9
- package/dist/profile/state-store.js.map +1 -1
- package/dist/profile/todo-plugin.d.ts.map +1 -1
- package/dist/profile/todo-plugin.js +9 -4
- package/dist/profile/todo-plugin.js.map +1 -1
- package/dist/profiles/builtin-suites.d.ts +13 -6
- package/dist/profiles/builtin-suites.d.ts.map +1 -1
- package/dist/profiles/builtin-suites.js +15 -6
- package/dist/profiles/builtin-suites.js.map +1 -1
- package/dist/profiles/suite-loader.d.ts +25 -1
- package/dist/profiles/suite-loader.d.ts.map +1 -1
- package/dist/profiles/suite-loader.js +34 -9
- package/dist/profiles/suite-loader.js.map +1 -1
- package/dist/remote/server-entry.d.ts.map +1 -1
- package/dist/remote/server-entry.js +3 -2
- package/dist/remote/server-entry.js.map +1 -1
- package/dist/remote/session-source.d.ts.map +1 -1
- package/dist/remote/session-source.js +8 -4
- package/dist/remote/session-source.js.map +1 -1
- package/dist/share/session-share.d.ts.map +1 -1
- package/dist/share/session-share.js +14 -2
- package/dist/share/session-share.js.map +1 -1
- package/dist/testing/plugin-testkit.d.ts.map +1 -1
- package/dist/testing/plugin-testkit.js +4 -3
- package/dist/testing/plugin-testkit.js.map +1 -1
- package/dist/testing/runtime-delivery-queue.d.ts.map +1 -1
- package/dist/testing/runtime-delivery-queue.js +2 -1
- package/dist/testing/runtime-delivery-queue.js.map +1 -1
- package/dist/testing/session-plugin-testkit.d.ts.map +1 -1
- package/dist/testing/session-plugin-testkit.js +10 -7
- package/dist/testing/session-plugin-testkit.js.map +1 -1
- package/dist/utils/ansi.d.ts.map +1 -1
- package/dist/utils/ansi.js +2 -1
- package/dist/utils/ansi.js.map +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +7 -6
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/image-resize-worker.d.ts.map +1 -1
- package/dist/utils/image-resize-worker.js +3 -2
- package/dist/utils/image-resize-worker.js.map +1 -1
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +9 -8
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +23 -13
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +5 -1
- package/dist/utils/version-check.js.map +1 -1
- package/docs/keybindings.md +3 -0
- package/docs/quickstart.md +1 -1
- package/docs/settings.md +1 -0
- package/docs/terminal-setup.md +2 -3
- package/docs/usage.md +6 -5
- 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 +1 -1
- package/dist/bundle/chunks/chunk-EALJ5DDX.js +0 -1452
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"durable-service.d.ts","sourceRoot":"","sources":["../../src/durable/durable-service.ts"],"names":[],"mappings":"AAuDA,YAAY,EACX,uBAAuB,EACvB,wBAAwB,EACxB,kBAAkB,EAClB,0BAA0B,EAC1B,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,GACvB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EACX,uBAAuB,EACvB,wBAAwB,EAExB,gBAAgB,EAChB,sBAAsB,EACtB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,MAAM,yBAAyB,GAAG,YAAY,CAAC;AAErD,qBAAa,qBAAsB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC;IAEzC,YAAY,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,MAAM,EAI3D;CACD;AAMD,4EAA4E;AAC5E,MAAM,MAAM,uBAAuB,GAChC;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAA;CAAE,GACrE;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,wBAAwB,CAAA;CAAE,GACzE;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAA;CAAE,CAAC;AAC3E,+FAA+F;AAC/F,MAAM,WAAW,wBAAwB;IACxC,2EAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,SAAS,uBAAuB,EAAE,CAAC;IACxD,6EAA6E;IAC7E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,gFAAgF;IAChF,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,mBAAmB;IACnC,iEAAiE;IACjE,OAAO,CAAC,UAAU,EAAE,SAAS,uBAAuB,EAAE,GAAG,IAAI,CAAC;IAC9D;;;;OAIG;IACH,IAAI,IAAI,wBAAwB,CAAC;IACjC,4EAA4E;IAC5E,KAAK,IAAI,IAAI,CAAC;IACd;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAC;CACrC;AAED,mDAAmD;AACnD,MAAM,MAAM,wBAAwB,GACjC;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD,qDAAqD;AACrD,MAAM,MAAM,0BAA0B,GACnC;IAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GAClE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/B;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAmB;IACnC,2GAA2G;IAC3G,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,wBAAwB,CAAC;IAClD,qHAAqH;IACrH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG;QAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;KAAE,CAAC;IAC3D,qHAAqH;IACrH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,0BAA0B,CAAC;IACtD,mGAAmG;IACnG,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1E;AAiJD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE;IACtD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,2EAA2E;IAC3E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B,GAAG,mBAAmB,CA2KtB;AAuDD,MAAM,WAAW,uBAAuB;IACvC,kFAAkF;IAClF,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,mBAAmB,CAAC;CACvC;AAuCD,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,uBAA4B,GAAG,gBAAgB,CAwP5F","sourcesContent":["/**\n * Durable service MVP (7.4b) — in-memory persistence for session, operation,\n * and artifact records with a single-writer lease, crash recovery, and a\n * readiness gate.\n *\n * Scope: this is the durable *contract* slice. Storage is an in-memory Map;\n * `exportState`/`importState` are the snapshot restart boundary, and a\n * {@link DurableStoreBackend} is the log restart boundary: when one is passed\n * to {@link createDurableService}, every committed record is appended to the\n * backend log (write-ahead) and the log is replayed at construction to\n * restore state. Without a backend the service stays purely in-memory — the\n * original MVP behavior. {@link createFileDurableStoreBackend} is the JSONL\n * append-only file backend (ledger.ts pattern). Rules frozen here:\n * - Writes require the writer lease: `persist*` with any writerId other than\n * the current holder fails with `not_writer` — before the first claim,\n * while another writer holds the lease, and after a crash (until\n * `recoverWriter` takes it over).\n * - A writer recorded in an imported snapshot belonged to the process that\n * produced the snapshot and cannot be alive in this one: import enters a\n * crash-pending state where the lease stays held by the dead writer, reads\n * still work, and only `recoverWriter` can take the lease over. Readiness\n * stays `false` until it does.\n * - File writes through {@link createFileDurableStoreBackend} are\n * cross-process mutually exclusive: every persist serializes through a\n * `.lock` sibling file (same protocol as the memory ledger — exclusive\n * create, poll, stale-lock stealing, token-guarded release; wall-clock\n * timeout and staleness configurable, defaults 5 s / 60 s). The lock\n * guarantees concurrent appends do not interleave.\n * - Store-level writer lease (D-067 phase 2 S2): backends may expose an\n * optional {@link DurableStoreLeaseV1} — a TTL-based, cross-process\n * mutually-exclusive lease over the store. The file backend implements it\n * with a `.lease` sibling file (write heartbeat on every persist); the\n * service's claim/recover/release then resolve against the store, so a\n * second process observes a live foreign writer (`held`) and a crashed\n * writer's lease becomes recoverable once its TTL lapses. Without a\n * backend lease the writer lease stays process-local memory (unchanged).\n */\nimport {\n\tcloseSync,\n\tfsyncSync,\n\tmkdirSync,\n\topenSync,\n\treadFileSync,\n\tstatSync,\n\tunlinkSync,\n\twriteFileSync,\n\twriteSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { sleepSync } from \"../utils/sync-sleep.ts\";\n\n// D-075 S4 third batch: the pure contract types moved to @agent-forge/plugin-sdk\n// (`durable.ts` is the plugin-facing authority); this module re-exports them so\n// the host's historical import paths and public export face keep resolving.\n// The implementation below (writer lease, file backend, store lease) stays here.\nexport type {\n\tDurableArtifactRecordV1,\n\tDurableOperationRecordV1,\n\tDurableReadinessV1,\n\tDurableReplayDiagnosticsV1,\n\tDurableServiceV1,\n\tDurableSessionRecordV1,\n\tDurableSnapshotV1,\n\tDurableWriterClaimV1,\n\tDurableWriterRecoveryV1,\n} from \"@agent-forge/plugin-sdk\";\n\nimport type {\n\tDurableArtifactRecordV1,\n\tDurableOperationRecordV1,\n\tDurableReplayDiagnosticsV1,\n\tDurableServiceV1,\n\tDurableSessionRecordV1,\n} from \"@agent-forge/plugin-sdk\";\n\nexport type DurableServiceErrorCodeV1 = \"not_writer\";\n\nexport class DurableServiceErrorV1 extends Error {\n\treadonly code: DurableServiceErrorCodeV1;\n\n\tconstructor(code: DurableServiceErrorCodeV1, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"DurableServiceErrorV1\";\n\t\tthis.code = code;\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Store backend (durable 落盘): JSONL append-only log contract\n// ---------------------------------------------------------------------------\n\n/** One committed record as carried in a {@link DurableStoreBackend} log. */\nexport type DurableStoreOperationV1 =\n\t| { readonly kind: \"session\"; readonly record: DurableSessionRecordV1 }\n\t| { readonly kind: \"operation\"; readonly record: DurableOperationRecordV1 }\n\t| { readonly kind: \"artifact\"; readonly record: DurableArtifactRecordV1 };\n/** Result of {@link DurableStoreBackend.load}: replayable operations plus skip diagnostics. */\nexport interface DurableStoreLoadResultV1 {\n\t/** Valid operations in log order — replay these into a fresh service. */\n\treadonly operations: readonly DurableStoreOperationV1[];\n\t/** Malformed lines (invalid JSON or invalid records) skipped during load. */\n\treadonly corruptedLines: number;\n\t/** True when the file existed but could not be read (permissions, IO error). */\n\treadonly unreadable: boolean;\n}\n\n/**\n * Durable log backend for {@link createDurableService}. The write path\n * appends every committed record; the read path `load()`s the log at\n * construction and replays it into the fresh in-memory maps (last write per\n * id wins, matching live persist semantics). The default\n * {@link FileDurableStoreBackend} never throws on load — corruption is\n * skipped and counted — and the append path runs under a cross-process\n * `.lock` file mutex and fsyncs, so hard crashes cannot lose the most recent\n * appends and concurrent processes cannot interleave lines (a torn trailing\n * line is still skipped by the loader's corruption tolerance). The lock\n * covers file writes only: the writer lease stays process-local memory.\n */\nexport interface DurableStoreBackend {\n\t/** Appends committed operations to the durable log, in order. */\n\tpersist(operations: readonly DurableStoreOperationV1[]): void;\n\t/**\n\t * Reads the whole log and returns the replayable operations plus skip\n\t * diagnostics. A missing log is an empty backend, not an error; malformed\n\t * lines are skipped and counted — load NEVER throws.\n\t */\n\tload(): DurableStoreLoadResultV1;\n\t/** Releases backend resources. Idempotent; a no-op for the file backend. */\n\tclose(): void;\n\t/**\n\t * Optional store-level writer lease (D-067 phase 2 S2). When present, the\n\t * service resolves its claim/recover/release against it, giving\n\t * cross-process lease mutual exclusion and crash recovery by TTL; when\n\t * absent, the writer lease stays process-local memory.\n\t */\n\treadonly lease?: DurableStoreLeaseV1;\n}\n\n/** Result of {@link DurableStoreLeaseV1.claim}. */\nexport type DurableStoreLeaseClaimV1 =\n\t| { readonly status: \"claimed\" }\n\t| { readonly status: \"held\"; readonly by: string };\n\n/** Result of {@link DurableStoreLeaseV1.recover}. */\nexport type DurableStoreLeaseRecoverV1 =\n\t| { readonly status: \"recovered\"; readonly previousWriter?: string }\n\t| { readonly status: \"held\" };\n\n/**\n * Store-level writer lease: a TTL-based mutual-exclusion token over the whole\n * store, visible across processes. `renew` is the heartbeat — the holder\n * extends its TTL on every persist — and doubles as the liveness check: a\n * writer whose lease expired (or was recovered by another process) fails its\n * next renew with `not_writer`, so stale writes fail closed. All methods may\n * throw `DurableServiceErrorV1` with code `not_writer`.\n */\nexport interface DurableStoreLeaseV1 {\n\t/** Takes the lease; fails while a live foreign holder exists. Re-claim by the live holder refreshes it. */\n\tclaim(writerId: string): DurableStoreLeaseClaimV1;\n\t/** Releases the lease when held by `writerId`; free/expired leases release idempotently; a foreign holder throws. */\n\trelease(writerId: string): { readonly status: \"released\" };\n\t/** Takes over the lease of a holder whose TTL lapsed (dead process); a live foreign holder is reported as `held`. */\n\trecover(writerId: string): DurableStoreLeaseRecoverV1;\n\t/** Heartbeat: extends the holder's TTL; throws `not_writer` when the lease was lost or expired. */\n\trenew(writerId: string): void;\n}\n\n/** The standard durable log path: `<agentDir>/durable/<sessionId>.jsonl`. */\nexport function durableLogPath(agentDir: string, sessionId: string): string {\n\treturn join(agentDir, \"durable\", `${sessionId}.jsonl`);\n}\n\nfunction assertNonEmptyString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string`);\n}\n\n/**\n * Writes every byte of `data` to the fd, looping over partial `writeSync`\n * results (a short write would silently break the fsync durability promise:\n * the caller fsyncs and closes only after the full payload is handed to the\n * OS). Same pattern as the memory ledger's write path, implemented here with\n * plain node:fs primitives so the file backend needs no ledger dependency.\n */\nconst writeAllSync = (fd: number, data: string): void => {\n\tconst buffer = Buffer.from(data, \"utf8\");\n\tlet written = 0;\n\twhile (written < buffer.length) {\n\t\twritten += writeSync(fd, buffer, written, buffer.length - written);\n\t}\n};\n\n// ---------------------------------------------------------------------------\n// Cross-process persist lock (ledger.ts lock protocol, no ledger import —\n// the two implementations must stay behaviorally identical)\n// ---------------------------------------------------------------------------\n\n/** Suffix of the persistence lock sibling file (`<logPath>.lock`). */\nconst LOCK_FILE_SUFFIX = \".lock\";\n/** Suffix of the store-level writer lease sibling file (`<logPath>.lease`). */\nconst LEASE_FILE_SUFFIX = \".lease\";\n/**\n * Wall-clock wait between lock acquisition attempts. Kept as a per-file\n * constant even though the memory ledger currently uses the same 25 ms value:\n * the two lock protocols stay independent.\n */\nconst LOCK_POLL_INTERVAL_MS = 25;\n/** Default ceiling on waiting for a competing writer's lock. */\nconst DEFAULT_LOCK_TIMEOUT_MS = 5000;\n/**\n * Default age at which a lock counts as abandoned (its writer crashed while\n * holding it). Deliberately far above any live hold time (milliseconds of a\n * bounded append) and above the wait timeout, so a waiter fails loudly at the\n * timeout instead of stealing a lock that might still be held.\n */\nconst DEFAULT_STALE_LOCK_MS = 60_000;\n/**\n * Default writer-lease TTL. The holder renews on every persist (heartbeat);\n * an idle-but-alive process that never persists loses the lease after this\n * window and must `recoverWriter` before writing again. Deliberately far\n * above any test-relevant hold so production never sees surprise expiries\n * within a single persist burst; tests pass small values explicitly.\n */\nconst DEFAULT_LEASE_TTL_MS = 30_000;\n\n/**\n * Takes the cross-process persist lock shared by every durable store file\n * write: exclusive create (`wx`) is the mutex; an existing lock is polled\n * until released, stolen once its mtime exceeds `staleLockMs` (crashed\n * writer), or turned into a thrown error at the wait deadline. The lock file\n * carries a per-acquisition token so a stale-steal victim releasing late can\n * never delete the thief's fresh lock. Same protocol as the memory ledger's\n * append lock (now the memory plugin package's ledger, plugins/memory), reimplemented here to avoid module\n * coupling; the wall-clock lock is a real-time mechanism, deliberately not\n * driven by any injected deterministic `now`.\n */\nfunction acquirePersistLock(lockPath: string, lockTimeoutMs: number, staleLockMs: number): string {\n\tconst deadline = Date.now() + lockTimeoutMs;\n\tfor (;;) {\n\t\tmkdirSync(dirname(lockPath), { recursive: true });\n\t\tconst token = `${process.pid}:${Date.now()}`;\n\t\ttry {\n\t\t\tconst fd = openSync(lockPath, \"wx\");\n\t\t\ttry {\n\t\t\t\twriteFileSync(fd, token);\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\treturn token;\n\t\t} catch (error) {\n\t\t\t// POSIX maps a lost create race to EEXIST; Windows/libuv maps the\n\t\t\t// same concurrent-create sharing violation to EPERM. Both mean the\n\t\t\t// lock file existed when we tried to create it — contend (poll /\n\t\t\t// stale-steal below). For EPERM the existence check re-stats: a\n\t\t\t// lock still there is contention, while ENOENT means the holder\n\t\t\t// released and deleted it between the failed open and this stat\n\t\t\t// (or Windows briefly left a delete-pending name that has now\n\t\t\t// gone) — the same vanished-lock window the EEXIST path tolerates,\n\t\t\t// so retry too. A stat error other than ENOENT means the EPERM was\n\t\t\t// a genuine permission failure and propagates, and the vanish path\n\t\t\t// still rethrows the original EPERM at the wait deadline: a\n\t\t\t// persistent EPERM with no lock file ever appearing is a genuine\n\t\t\t// permission failure, not a held lock.\n\t\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\t\tif (code === \"EPERM\") {\n\t\t\t\tlet vanished = false;\n\t\t\t\ttry {\n\t\t\t\t\tstatSync(lockPath);\n\t\t\t\t} catch (statError) {\n\t\t\t\t\tif ((statError as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n\t\t\t\t\tvanished = true;\n\t\t\t\t}\n\t\t\t\tif (vanished) {\n\t\t\t\t\tif (Date.now() >= deadline) throw error;\n\t\t\t\t\tsleepSync(LOCK_POLL_INTERVAL_MS);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t} else if (code !== \"EEXIST\") {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlet stale = false;\n\t\t\ttry {\n\t\t\t\tstale = Date.now() - statSync(lockPath).mtimeMs > staleLockMs;\n\t\t\t} catch {\n\t\t\t\t// The holder released the lock between the failed open and the\n\t\t\t\t// stat — retry the exclusive create immediately.\n\t\t\t}\n\t\t\tif (stale) {\n\t\t\t\t// A crashed writer left the lock behind: steal it. A competing\n\t\t\t\t// waiter may steal first — the next exclusive create resolves that.\n\t\t\t\ttry {\n\t\t\t\t\tunlinkSync(lockPath);\n\t\t\t\t} catch {\n\t\t\t\t\t// Already stolen — retry.\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (Date.now() >= deadline) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`durable store persist lock timeout after ${lockTimeoutMs} ms: ${lockPath} is held by another writer`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tsleepSync(LOCK_POLL_INTERVAL_MS);\n\t\t}\n\t}\n}\n\n/** Releases the persist lock, but only if our own token still owns it. */\nfunction releasePersistLock(lockPath: string, token: string): void {\n\ttry {\n\t\tif (readFileSync(lockPath, \"utf8\") === token) unlinkSync(lockPath);\n\t} catch {\n\t\t// The lock is already gone (or unreadable) — nothing to release.\n\t}\n}\n\n/**\n * Creates a JSONL append-only file backend at `path`\n * (ledger.ts pattern: one committed record per line, trailing newline so a\n * crash-truncated tail stays well-formed for the loader). Every persist\n * serializes against other processes through a `<path>.lock` sibling using\n * the memory ledger's lock protocol (exclusive create + 25 ms poll + mtime\n * stale-lock stealing + token-guarded release): `lockTimeoutMs` bounds the\n * wall-clock wait (default 5 s) and `staleLockMs` the abandoned-lock age\n * (default 60 s) — both exist so tests can exercise the timeout and steal\n * paths quickly. The lock makes concurrent appends non-interleaving.\n *\n * The backend also exposes the store-level writer lease (`<path>.lease`\n * sibling, D-067 phase 2 S2): a `{ writerId, expiresAt }` JSON file guarded\n * by the same lock protocol, with a TTL the holder renews on every persist\n * (`leaseTtlMs`, default 30 s — small values exist for tests). A crashed\n * writer's lease becomes recoverable once its TTL lapses; a live foreign\n * holder makes `claim`/`recover` report `held` and the holder's next `renew`\n * (i.e. its next persist) fail closed with `not_writer`.\n */\nexport function createFileDurableStoreBackend(options: {\n\treadonly path: string;\n\t/** Wall-clock ceiling on waiting for a competing writer's lock; default 5 s. */\n\treadonly lockTimeoutMs?: number;\n\t/** Age at which a lock counts as abandoned and is stolen; default 60 s. */\n\treadonly staleLockMs?: number;\n\t/** Writer-lease TTL; the holder renews on every persist. Default 30 s. */\n\treadonly leaseTtlMs?: number;\n}): DurableStoreBackend {\n\tassertNonEmptyString(options.path, \"Durable store path\");\n\tif (options.lockTimeoutMs !== undefined && (!Number.isFinite(options.lockTimeoutMs) || options.lockTimeoutMs <= 0)) {\n\t\tthrow new Error(\"Durable store lockTimeoutMs must be a positive number\");\n\t}\n\tif (options.staleLockMs !== undefined && (!Number.isFinite(options.staleLockMs) || options.staleLockMs <= 0)) {\n\t\tthrow new Error(\"Durable store staleLockMs must be a positive number\");\n\t}\n\tif (options.leaseTtlMs !== undefined && (!Number.isFinite(options.leaseTtlMs) || options.leaseTtlMs <= 0)) {\n\t\tthrow new Error(\"Durable store leaseTtlMs must be a positive number\");\n\t}\n\tconst path = options.path;\n\tconst lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;\n\tconst staleLockMs = options.staleLockMs ?? DEFAULT_STALE_LOCK_MS;\n\tconst leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;\n\tconst lockPath = `${path}${LOCK_FILE_SUFFIX}`;\n\tconst leasePath = `${path}${LEASE_FILE_SUFFIX}`;\n\n\t/** Reads the lease file; missing, unreadable, or malformed content = no lease. */\n\tconst readLease = (): { readonly writerId: string; readonly expiresAt: number } | undefined => {\n\t\ttry {\n\t\t\tconst value: unknown = JSON.parse(readFileSync(leasePath, \"utf8\"));\n\t\t\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined;\n\t\t\tconst writerId = (value as { writerId?: unknown }).writerId;\n\t\t\tconst expiresAt = (value as { expiresAt?: unknown }).expiresAt;\n\t\t\tif (typeof writerId !== \"string\" || writerId.length === 0) return undefined;\n\t\t\tif (typeof expiresAt !== \"number\" || !Number.isFinite(expiresAt)) return undefined;\n\t\t\treturn { writerId, expiresAt };\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t};\n\n\t/**\n\t * Overwrites the lease file (caller holds the persist lock). Deliberately\n\t * NOT fsynced: the lease is a liveness token, not data — the worst case of\n\t * a torn post-crash write is a free lease, which the recover path handles.\n\t */\n\tconst writeLease = (writerId: string, expiresAt: number): void => {\n\t\tconst fd = openSync(leasePath, \"w\");\n\t\ttry {\n\t\t\twriteAllSync(fd, JSON.stringify({ writerId, expiresAt }));\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t};\n\n\t/** True when `current` names a different writer whose TTL has not lapsed. */\n\tconst liveForeign = (\n\t\tcurrent: { readonly writerId: string; readonly expiresAt: number },\n\t\twriterId: string,\n\t): boolean => current.writerId !== writerId && Date.now() <= current.expiresAt;\n\n\tconst lease: DurableStoreLeaseV1 = {\n\t\tclaim(writerId) {\n\t\t\tconst token = acquirePersistLock(lockPath, lockTimeoutMs, staleLockMs);\n\t\t\ttry {\n\t\t\t\tconst current = readLease();\n\t\t\t\tif (current !== undefined && liveForeign(current, writerId))\n\t\t\t\t\treturn { status: \"held\", by: current.writerId };\n\t\t\t\twriteLease(writerId, Date.now() + leaseTtlMs);\n\t\t\t\treturn { status: \"claimed\" };\n\t\t\t} finally {\n\t\t\t\treleasePersistLock(lockPath, token);\n\t\t\t}\n\t\t},\n\t\trelease(writerId) {\n\t\t\tconst token = acquirePersistLock(lockPath, lockTimeoutMs, staleLockMs);\n\t\t\ttry {\n\t\t\t\tconst current = readLease();\n\t\t\t\tif (current === undefined || current.writerId === writerId) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tunlinkSync(leasePath);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Already gone (or vanished between the read and the unlink).\n\t\t\t\t\t}\n\t\t\t\t\treturn { status: \"released\" };\n\t\t\t\t}\n\t\t\t\tthrow new DurableServiceErrorV1(\n\t\t\t\t\t\"not_writer\",\n\t\t\t\t\t`Writer ${writerId} does not hold the store lease (held by ${current.writerId})`,\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\treleasePersistLock(lockPath, token);\n\t\t\t}\n\t\t},\n\t\trecover(writerId) {\n\t\t\tconst token = acquirePersistLock(lockPath, lockTimeoutMs, staleLockMs);\n\t\t\ttry {\n\t\t\t\tconst current = readLease();\n\t\t\t\tif (current !== undefined && liveForeign(current, writerId)) return { status: \"held\" };\n\t\t\t\tconst previousWriter =\n\t\t\t\t\tcurrent !== undefined && current.writerId !== writerId ? current.writerId : undefined;\n\t\t\t\twriteLease(writerId, Date.now() + leaseTtlMs);\n\t\t\t\treturn previousWriter === undefined ? { status: \"recovered\" } : { status: \"recovered\", previousWriter };\n\t\t\t} finally {\n\t\t\t\treleasePersistLock(lockPath, token);\n\t\t\t}\n\t\t},\n\t\trenew(writerId) {\n\t\t\tconst token = acquirePersistLock(lockPath, lockTimeoutMs, staleLockMs);\n\t\t\ttry {\n\t\t\t\tconst current = readLease();\n\t\t\t\tif (current === undefined || current.writerId !== writerId || Date.now() > current.expiresAt) {\n\t\t\t\t\tthrow new DurableServiceErrorV1(\n\t\t\t\t\t\t\"not_writer\",\n\t\t\t\t\t\t`Writer ${writerId} lost the store lease (expired or recovered by another writer)`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\twriteLease(writerId, Date.now() + leaseTtlMs);\n\t\t\t} finally {\n\t\t\t\treleasePersistLock(lockPath, token);\n\t\t\t}\n\t\t},\n\t};\n\n\treturn {\n\t\tpersist(operations) {\n\t\t\tif (operations.length === 0) return;\n\t\t\tconst lines = operations.map((operation) => `${JSON.stringify(storeOperationEnvelope(operation))}\\n`);\n\t\t\t// Competing writers — including another server process — serialize\n\t\t\t// through the `.lock` sibling before the fsynced append, so lines from\n\t\t\t// concurrent processes never interleave (ledger.ts lock protocol).\n\t\t\t// Lock acquisition creates the parent directory, as in the ledger.\n\t\t\tconst token = acquirePersistLock(lockPath, lockTimeoutMs, staleLockMs);\n\t\t\ttry {\n\t\t\t\t// Append is fsynced before close: once persist returns, a hard\n\t\t\t\t// crash cannot lose the appended lines (open → write → fsync →\n\t\t\t\t// close, the same durability pattern as the memory ledger). A\n\t\t\t\t// failure inside the lock releases it and propagates; no\n\t\t\t\t// in-memory state of the service is touched (write-ahead).\n\t\t\t\tconst fd = openSync(path, \"a\");\n\t\t\t\ttry {\n\t\t\t\t\twriteAllSync(fd, lines.join(\"\"));\n\t\t\t\t\tfsyncSync(fd);\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\treleasePersistLock(lockPath, token);\n\t\t\t}\n\t\t},\n\t\tload() {\n\t\t\tlet raw: string;\n\t\t\ttry {\n\t\t\t\traw = readFileSync(path, \"utf8\");\n\t\t\t} catch (error) {\n\t\t\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\t\t\t\treturn { operations: [], corruptedLines: 0, unreadable: false };\n\t\t\t\t}\n\t\t\t\treturn { operations: [], corruptedLines: 0, unreadable: true };\n\t\t\t}\n\t\t\tconst operations: DurableStoreOperationV1[] = [];\n\t\t\tlet corruptedLines = 0;\n\t\t\tfor (const line of raw.split(\"\\n\")) {\n\t\t\t\tif (line.trim() === \"\") continue;\n\t\t\t\tconst operation = parseStoreOperationLine(line);\n\t\t\t\tif (operation === undefined) {\n\t\t\t\t\tcorruptedLines += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\toperations.push(operation);\n\t\t\t}\n\t\t\treturn { operations, corruptedLines, unreadable: false };\n\t\t},\n\t\tclose() {\n\t\t\t// Files are opened per append; nothing persistent to release. The\n\t\t\t// hook exists for backends that hold resources (handles, sockets).\n\t\t},\n\t\tlease,\n\t};\n}\n\n/** Flat one-line envelope: the kind discriminator plus the record fields. */\nfunction storeOperationEnvelope(operation: DurableStoreOperationV1): Record<string, unknown> {\n\treturn { kind: operation.kind, ...operation.record };\n}\n\n/** Parses one log line; undefined when it is malformed (skipped and counted). */\nfunction parseStoreOperationLine(line: string): DurableStoreOperationV1 | undefined {\n\ttry {\n\t\tconst value: unknown = JSON.parse(line);\n\t\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined;\n\t\tconst record = value as Record<string, unknown>;\n\t\tswitch (record.kind) {\n\t\t\tcase \"session\": {\n\t\t\t\tconst base = importRecordBase(record, \"sessionId\", \"Durable store session record\");\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"session\",\n\t\t\t\t\trecord: { sessionId: base.id, data: base.data, persistedAt: base.persistedAt },\n\t\t\t\t};\n\t\t\t}\n\t\t\tcase \"operation\": {\n\t\t\t\tconst base = importRecordBase(record, \"operationId\", \"Durable store operation record\");\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"operation\",\n\t\t\t\t\trecord: {\n\t\t\t\t\t\toperationId: base.id,\n\t\t\t\t\t\tsessionId: importOwningSessionId(record, \"Durable store operation record\"),\n\t\t\t\t\t\tdata: base.data,\n\t\t\t\t\t\tpersistedAt: base.persistedAt,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tcase \"artifact\": {\n\t\t\t\tconst base = importRecordBase(record, \"artifactId\", \"Durable store artifact record\");\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"artifact\",\n\t\t\t\t\trecord: {\n\t\t\t\t\t\tartifactId: base.id,\n\t\t\t\t\t\tsessionId: importOwningSessionId(record, \"Durable store artifact record\"),\n\t\t\t\t\t\tdata: base.data,\n\t\t\t\t\t\tpersistedAt: base.persistedAt,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn undefined;\n\t\t}\n\t} catch {\n\t\t// Corruption tolerance: a malformed line is skipped and counted — a\n\t\t// damaged log must never fail the startup replay.\n\t\treturn undefined;\n\t}\n}\n\nexport interface DurableServiceOptionsV1 {\n\t/** Deterministic clock for `persistedAt`/`exportedAt`; defaults to `Date.now`. */\n\treadonly now?: () => number;\n\t/**\n\t * Optional durable log backend. Without it the service stays purely\n\t * in-memory (backward-compatible MVP behavior); with it every committed\n\t * record is appended to the backend log before the in-memory maps update\n\t * (write-ahead: an append failure leaves memory untouched) and the log is\n\t * replayed at construction to restore state.\n\t */\n\treadonly backend?: DurableStoreBackend;\n}\n\nfunction assertObject(value: unknown, label: string): asserts value is Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new Error(`${label} must be an object`);\n\t}\n}\n\ninterface ImportedRecordV1 {\n\treadonly id: string;\n\treadonly data: unknown;\n\treadonly persistedAt: number;\n}\n\nfunction importRecordBase(\n\tvalue: Record<string, unknown>,\n\tidField: \"sessionId\" | \"operationId\" | \"artifactId\",\n\tlabel: string,\n): ImportedRecordV1 {\n\tconst id = value[idField];\n\tif (typeof id !== \"string\" || id.length === 0) throw new Error(`${label} ${idField} must be a non-empty string`);\n\tconst persistedAt = value.persistedAt;\n\tif (typeof persistedAt !== \"number\" || !Number.isFinite(persistedAt)) {\n\t\tthrow new Error(`${label} persistedAt must be a finite number`);\n\t}\n\t// Legacy (pre-单机收敛) log lines and snapshots may carry a `tenantId` key\n\t// from the removed multi-tenant machinery; it is tolerated as an ignored\n\t// extra key (D-068 增补 1), so it is simply never read here.\n\treturn { id, data: value.data, persistedAt };\n}\n\nfunction importOwningSessionId(value: Record<string, unknown>, label: string): string {\n\tconst sessionId = value.sessionId;\n\tif (typeof sessionId !== \"string\" || sessionId.length === 0) {\n\t\tthrow new Error(`${label} sessionId must be a non-empty string`);\n\t}\n\treturn sessionId;\n}\n\nexport function createDurableService(options: DurableServiceOptionsV1 = {}): DurableServiceV1 {\n\tconst now = options.now ?? (() => Date.now());\n\tconst sessions = new Map<string, DurableSessionRecordV1>();\n\tconst operations = new Map<string, DurableOperationRecordV1>();\n\tconst artifacts = new Map<string, DurableArtifactRecordV1>();\n\n\t/** Current lease holder; `null` when unclaimed or crash-pending. */\n\tlet holder: string | null = null;\n\t/** Writer found dead in a restored snapshot; blocks claims and writes until recovered. */\n\tlet crashPending: string | null = null;\n\n\t// Startup replay (durable 落盘): with a backend, the log is the source of\n\t// truth for records — replay restores the maps with last-write-wins, the\n\t// same semantics as live persists. With a backend lease (D-067 phase 2\n\t// S2) the writer lease resolves against the store (cross-process mutual\n\t// exclusion, TTL crash recovery); without one it stays process-local.\n\tconst backend = options.backend;\n\tconst backendLease = backend?.lease;\n\tlet replay: DurableReplayDiagnosticsV1 = { corruptedLines: 0, unreadable: false };\n\tif (backend) {\n\t\tconst loaded = backend.load();\n\t\tfor (const operation of loaded.operations) {\n\t\t\tswitch (operation.kind) {\n\t\t\t\tcase \"session\":\n\t\t\t\t\tsessions.set(operation.record.sessionId, operation.record);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"operation\":\n\t\t\t\t\toperations.set(operation.record.operationId, operation.record);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"artifact\":\n\t\t\t\t\tartifacts.set(operation.record.artifactId, operation.record);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treplay = { corruptedLines: loaded.corruptedLines, unreadable: loaded.unreadable };\n\t}\n\n\t/** Write-ahead log append; an append failure leaves the maps untouched. */\n\tconst logOperation = (operation: DurableStoreOperationV1): void => {\n\t\tif (backend) backend.persist([operation]);\n\t};\n\n\tfunction requireWriter(writerId: string): void {\n\t\tif (holder !== writerId) {\n\t\t\tthrow new DurableServiceErrorV1(\"not_writer\", `Writer ${writerId} does not hold the writer lease`);\n\t\t}\n\t\t// Store-level heartbeat + liveness check (D-067 phase 2 S2): another\n\t\t// process recovering our expired lease fail-closes this write instead\n\t\t// of letting a stale writer interleave records.\n\t\tbackendLease?.renew(writerId);\n\t}\n\n\treturn {\n\t\tpersistSession(input) {\n\t\t\trequireWriter(input.writerId);\n\t\t\tconst record: DurableSessionRecordV1 = {\n\t\t\t\tsessionId: input.sessionId,\n\t\t\t\tdata: input.data,\n\t\t\t\tpersistedAt: now(),\n\t\t\t};\n\t\t\tlogOperation({ kind: \"session\", record });\n\t\t\tsessions.set(input.sessionId, record);\n\t\t\treturn record;\n\t\t},\n\t\tloadSession(input) {\n\t\t\treturn sessions.get(input.sessionId);\n\t\t},\n\t\tpersistOperation(input) {\n\t\t\trequireWriter(input.writerId);\n\t\t\tconst record: DurableOperationRecordV1 = {\n\t\t\t\toperationId: input.operationId,\n\t\t\t\tsessionId: input.sessionId,\n\t\t\t\tdata: input.data,\n\t\t\t\tpersistedAt: now(),\n\t\t\t};\n\t\t\tlogOperation({ kind: \"operation\", record });\n\t\t\toperations.set(input.operationId, record);\n\t\t\treturn record;\n\t\t},\n\t\tloadOperation(input) {\n\t\t\treturn operations.get(input.operationId);\n\t\t},\n\t\tpersistArtifact(input) {\n\t\t\trequireWriter(input.writerId);\n\t\t\tconst record: DurableArtifactRecordV1 = {\n\t\t\t\tartifactId: input.artifactId,\n\t\t\t\tsessionId: input.sessionId,\n\t\t\t\tdata: input.data,\n\t\t\t\tpersistedAt: now(),\n\t\t\t};\n\t\t\tlogOperation({ kind: \"artifact\", record });\n\t\t\tartifacts.set(input.artifactId, record);\n\t\t\treturn record;\n\t\t},\n\t\tloadArtifact(input) {\n\t\t\treturn artifacts.get(input.artifactId);\n\t\t},\n\t\tlistSessions() {\n\t\t\treturn (\n\t\t\t\t[...sessions.values()]\n\t\t\t\t\t// v8 ignore next -- sessionId 是 Map 键恒唯一,相等比较路径不可达\n\t\t\t\t\t.sort((a, b) => (a.sessionId < b.sessionId ? -1 : a.sessionId > b.sessionId ? 1 : 0))\n\t\t\t);\n\t\t},\n\t\tclaimWriter(writerId) {\n\t\t\tif (crashPending !== null) return { status: \"held\", by: crashPending };\n\t\t\tif (backendLease === undefined) {\n\t\t\t\tif (holder !== null && holder !== writerId) return { status: \"held\", by: holder };\n\t\t\t\tholder = writerId;\n\t\t\t\treturn { status: \"claimed\" };\n\t\t\t}\n\t\t\tconst result = backendLease.claim(writerId);\n\t\t\tif (result.status === \"claimed\") {\n\t\t\t\tholder = writerId;\n\t\t\t\treturn { status: \"claimed\" };\n\t\t\t}\n\t\t\treturn { status: \"held\", by: result.by };\n\t\t},\n\t\treleaseWriter(writerId) {\n\t\t\tif (crashPending !== null) {\n\t\t\t\tthrow new DurableServiceErrorV1(\n\t\t\t\t\t\"not_writer\",\n\t\t\t\t\t`Writer ${writerId} cannot release a crashed lease; recover it instead`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (backendLease === undefined) {\n\t\t\t\tif (holder !== null && holder !== writerId) {\n\t\t\t\t\tthrow new DurableServiceErrorV1(\n\t\t\t\t\t\t\"not_writer\",\n\t\t\t\t\t\t`Writer ${writerId} does not hold the writer lease (held by ${holder})`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tholder = null;\n\t\t\t\treturn { status: \"released\" };\n\t\t\t}\n\t\t\tconst result = backendLease.release(writerId);\n\t\t\tholder = null;\n\t\t\treturn result;\n\t\t},\n\t\trecoverWriter(writerId) {\n\t\t\tif (crashPending !== null) {\n\t\t\t\tconst previousWriter = crashPending;\n\t\t\t\tcrashPending = null;\n\t\t\t\tholder = writerId;\n\t\t\t\treturn { status: \"recovered\", previousWriter };\n\t\t\t}\n\t\t\tif (backendLease === undefined) {\n\t\t\t\tif (holder !== null && holder !== writerId) return { status: \"held\" };\n\t\t\t\tholder = writerId;\n\t\t\t\treturn { status: \"recovered\" };\n\t\t\t}\n\t\t\tconst result = backendLease.recover(writerId);\n\t\t\tif (result.status === \"recovered\") {\n\t\t\t\tholder = writerId;\n\t\t\t\treturn result.previousWriter === undefined\n\t\t\t\t\t? { status: \"recovered\" }\n\t\t\t\t\t: { status: \"recovered\", previousWriter: result.previousWriter };\n\t\t\t}\n\t\t\treturn { status: \"held\" };\n\t\t},\n\t\treadiness() {\n\t\t\tif (crashPending !== null) {\n\t\t\t\treturn { ready: false, reason: `writer crash is pending recovery: ${crashPending}` };\n\t\t\t}\n\t\t\tif (holder === null) {\n\t\t\t\treturn { ready: false, reason: \"no writer has claimed the lease\" };\n\t\t\t}\n\t\t\treturn { ready: true };\n\t\t},\n\t\texportState() {\n\t\t\treturn {\n\t\t\t\tversion: 1 as const,\n\t\t\t\texportedAt: now(),\n\t\t\t\tsessions: [...sessions.values()],\n\t\t\t\toperations: [...operations.values()],\n\t\t\t\tartifacts: [...artifacts.values()],\n\t\t\t\twriter: { holder, crashPending },\n\t\t\t};\n\t\t},\n\t\timportState(snapshot) {\n\t\t\tassertObject(snapshot, \"Durable snapshot\");\n\t\t\tif (snapshot.version !== 1) {\n\t\t\t\tthrow new Error(`Unsupported durable snapshot version: ${String(snapshot.version)}`);\n\t\t\t}\n\t\t\tfor (const field of [\"sessions\", \"operations\", \"artifacts\"] as const) {\n\t\t\t\tif (!Array.isArray(snapshot[field])) throw new Error(`Durable snapshot ${field} must be an array`);\n\t\t\t}\n\t\t\tconst writer = snapshot.writer;\n\t\t\tassertObject(writer, \"Durable snapshot writer\");\n\t\t\tconst snapshotHolder = writer.holder;\n\t\t\tconst snapshotCrashPending = writer.crashPending;\n\t\t\tif (snapshotHolder !== null && typeof snapshotHolder !== \"string\") {\n\t\t\t\tthrow new Error(\"Durable snapshot writer holder must be a string or null\");\n\t\t\t}\n\t\t\tif (snapshotCrashPending !== null && typeof snapshotCrashPending !== \"string\") {\n\t\t\t\tthrow new Error(\"Durable snapshot writer crashPending must be a string or null\");\n\t\t\t}\n\n\t\t\tconst importedSessions = new Map<string, DurableSessionRecordV1>();\n\t\t\tfor (const raw of snapshot.sessions as unknown[]) {\n\t\t\t\tassertObject(raw, \"Durable snapshot session\");\n\t\t\t\tconst record = importRecordBase(raw, \"sessionId\", \"Durable snapshot session\");\n\t\t\t\timportedSessions.set(record.id, {\n\t\t\t\t\tsessionId: record.id,\n\t\t\t\t\tdata: record.data,\n\t\t\t\t\tpersistedAt: record.persistedAt,\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst importedOperations = new Map<string, DurableOperationRecordV1>();\n\t\t\tfor (const raw of snapshot.operations as unknown[]) {\n\t\t\t\tassertObject(raw, \"Durable snapshot operation\");\n\t\t\t\tconst record = importRecordBase(raw, \"operationId\", \"Durable snapshot operation\");\n\t\t\t\timportedOperations.set(record.id, {\n\t\t\t\t\toperationId: record.id,\n\t\t\t\t\tsessionId: importOwningSessionId(raw, \"Durable snapshot operation\"),\n\t\t\t\t\tdata: record.data,\n\t\t\t\t\tpersistedAt: record.persistedAt,\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst importedArtifacts = new Map<string, DurableArtifactRecordV1>();\n\t\t\tfor (const raw of snapshot.artifacts as unknown[]) {\n\t\t\t\tassertObject(raw, \"Durable snapshot artifact\");\n\t\t\t\tconst record = importRecordBase(raw, \"artifactId\", \"Durable snapshot artifact\");\n\t\t\t\timportedArtifacts.set(record.id, {\n\t\t\t\t\tartifactId: record.id,\n\t\t\t\t\tsessionId: importOwningSessionId(raw, \"Durable snapshot artifact\"),\n\t\t\t\t\tdata: record.data,\n\t\t\t\t\tpersistedAt: record.persistedAt,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tsessions.clear();\n\t\t\tfor (const [id, record] of importedSessions) sessions.set(id, record);\n\t\t\toperations.clear();\n\t\t\tfor (const [id, record] of importedOperations) operations.set(id, record);\n\t\t\tartifacts.clear();\n\t\t\tfor (const [id, record] of importedArtifacts) artifacts.set(id, record);\n\n\t\t\t// A writer recorded in the snapshot belonged to the process that\n\t\t\t// produced it; it cannot be alive in this one, so its lease needs\n\t\t\t// recovery before anyone may write again.\n\t\t\tholder = null;\n\t\t\tcrashPending = snapshotHolder ?? snapshotCrashPending;\n\t\t},\n\t\treplayDiagnostics() {\n\t\t\treturn replay;\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"durable-service.d.ts","sourceRoot":"","sources":["../../src/durable/durable-service.ts"],"names":[],"mappings":"AAuDA,YAAY,EACX,uBAAuB,EACvB,wBAAwB,EACxB,kBAAkB,EAClB,0BAA0B,EAC1B,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,GACvB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EACX,uBAAuB,EACvB,wBAAwB,EAExB,gBAAgB,EAChB,sBAAsB,EACtB,MAAM,yBAAyB,CAAC;AAGjC,MAAM,MAAM,yBAAyB,GAAG,YAAY,CAAC;AAErD,qBAAa,qBAAsB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC;IAEzC,YAAY,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,MAAM,EAI3D;CACD;AAMD,4EAA4E;AAC5E,MAAM,MAAM,uBAAuB,GAChC;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAA;CAAE,GACrE;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,wBAAwB,CAAA;CAAE,GACzE;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAA;CAAE,CAAC;AAC3E,+FAA+F;AAC/F,MAAM,WAAW,wBAAwB;IACxC,2EAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,SAAS,uBAAuB,EAAE,CAAC;IACxD,6EAA6E;IAC7E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,gFAAgF;IAChF,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,mBAAmB;IACnC,iEAAiE;IACjE,OAAO,CAAC,UAAU,EAAE,SAAS,uBAAuB,EAAE,GAAG,IAAI,CAAC;IAC9D;;;;OAIG;IACH,IAAI,IAAI,wBAAwB,CAAC;IACjC,4EAA4E;IAC5E,KAAK,IAAI,IAAI,CAAC;IACd;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAC;CACrC;AAED,mDAAmD;AACnD,MAAM,MAAM,wBAAwB,GACjC;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD,qDAAqD;AACrD,MAAM,MAAM,0BAA0B,GACnC;IAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GAClE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/B;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAmB;IACnC,2GAA2G;IAC3G,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,wBAAwB,CAAC;IAClD,qHAAqH;IACrH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG;QAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;KAAE,CAAC;IAC3D,qHAAqH;IACrH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,0BAA0B,CAAC;IACtD,mGAAmG;IACnG,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1E;AAqJD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE;IACtD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,2EAA2E;IAC3E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B,GAAG,mBAAmB,CA8KtB;AAuDD,MAAM,WAAW,uBAAuB;IACvC,kFAAkF;IAClF,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,mBAAmB,CAAC;CACvC;AAwCD,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,uBAA4B,GAAG,gBAAgB,CA8P5F","sourcesContent":["/**\n * Durable service MVP (7.4b) — in-memory persistence for session, operation,\n * and artifact records with a single-writer lease, crash recovery, and a\n * readiness gate.\n *\n * Scope: this is the durable *contract* slice. Storage is an in-memory Map;\n * `exportState`/`importState` are the snapshot restart boundary, and a\n * {@link DurableStoreBackend} is the log restart boundary: when one is passed\n * to {@link createDurableService}, every committed record is appended to the\n * backend log (write-ahead) and the log is replayed at construction to\n * restore state. Without a backend the service stays purely in-memory — the\n * original MVP behavior. {@link createFileDurableStoreBackend} is the JSONL\n * append-only file backend (ledger.ts pattern). Rules frozen here:\n * - Writes require the writer lease: `persist*` with any writerId other than\n * the current holder fails with `not_writer` — before the first claim,\n * while another writer holds the lease, and after a crash (until\n * `recoverWriter` takes it over).\n * - A writer recorded in an imported snapshot belonged to the process that\n * produced the snapshot and cannot be alive in this one: import enters a\n * crash-pending state where the lease stays held by the dead writer, reads\n * still work, and only `recoverWriter` can take the lease over. Readiness\n * stays `false` until it does.\n * - File writes through {@link createFileDurableStoreBackend} are\n * cross-process mutually exclusive: every persist serializes through a\n * `.lock` sibling file (same protocol as the memory ledger — exclusive\n * create, poll, stale-lock stealing, token-guarded release; wall-clock\n * timeout and staleness configurable, defaults 5 s / 60 s). The lock\n * guarantees concurrent appends do not interleave.\n * - Store-level writer lease (D-067 phase 2 S2): backends may expose an\n * optional {@link DurableStoreLeaseV1} — a TTL-based, cross-process\n * mutually-exclusive lease over the store. The file backend implements it\n * with a `.lease` sibling file (write heartbeat on every persist); the\n * service's claim/recover/release then resolve against the store, so a\n * second process observes a live foreign writer (`held`) and a crashed\n * writer's lease becomes recoverable once its TTL lapses. Without a\n * backend lease the writer lease stays process-local memory (unchanged).\n */\nimport {\n\tcloseSync,\n\tfsyncSync,\n\tmkdirSync,\n\topenSync,\n\treadFileSync,\n\tstatSync,\n\tunlinkSync,\n\twriteFileSync,\n\twriteSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { sleepSync } from \"../utils/sync-sleep.ts\";\n\n// D-075 S4 third batch: the pure contract types moved to @agent-forge/plugin-sdk\n// (`durable.ts` is the plugin-facing authority); this module re-exports them so\n// the host's historical import paths and public export face keep resolving.\n// The implementation below (writer lease, file backend, store lease) stays here.\nexport type {\n\tDurableArtifactRecordV1,\n\tDurableOperationRecordV1,\n\tDurableReadinessV1,\n\tDurableReplayDiagnosticsV1,\n\tDurableServiceV1,\n\tDurableSessionRecordV1,\n\tDurableSnapshotV1,\n\tDurableWriterClaimV1,\n\tDurableWriterRecoveryV1,\n} from \"@agent-forge/plugin-sdk\";\n\nimport type {\n\tDurableArtifactRecordV1,\n\tDurableOperationRecordV1,\n\tDurableReplayDiagnosticsV1,\n\tDurableServiceV1,\n\tDurableSessionRecordV1,\n} from \"@agent-forge/plugin-sdk\";\nimport { t } from \"../core/i18n/index.ts\";\n\nexport type DurableServiceErrorCodeV1 = \"not_writer\";\n\nexport class DurableServiceErrorV1 extends Error {\n\treadonly code: DurableServiceErrorCodeV1;\n\n\tconstructor(code: DurableServiceErrorCodeV1, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"DurableServiceErrorV1\";\n\t\tthis.code = code;\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Store backend (durable 落盘): JSONL append-only log contract\n// ---------------------------------------------------------------------------\n\n/** One committed record as carried in a {@link DurableStoreBackend} log. */\nexport type DurableStoreOperationV1 =\n\t| { readonly kind: \"session\"; readonly record: DurableSessionRecordV1 }\n\t| { readonly kind: \"operation\"; readonly record: DurableOperationRecordV1 }\n\t| { readonly kind: \"artifact\"; readonly record: DurableArtifactRecordV1 };\n/** Result of {@link DurableStoreBackend.load}: replayable operations plus skip diagnostics. */\nexport interface DurableStoreLoadResultV1 {\n\t/** Valid operations in log order — replay these into a fresh service. */\n\treadonly operations: readonly DurableStoreOperationV1[];\n\t/** Malformed lines (invalid JSON or invalid records) skipped during load. */\n\treadonly corruptedLines: number;\n\t/** True when the file existed but could not be read (permissions, IO error). */\n\treadonly unreadable: boolean;\n}\n\n/**\n * Durable log backend for {@link createDurableService}. The write path\n * appends every committed record; the read path `load()`s the log at\n * construction and replays it into the fresh in-memory maps (last write per\n * id wins, matching live persist semantics). The default\n * {@link FileDurableStoreBackend} never throws on load — corruption is\n * skipped and counted — and the append path runs under a cross-process\n * `.lock` file mutex and fsyncs, so hard crashes cannot lose the most recent\n * appends and concurrent processes cannot interleave lines (a torn trailing\n * line is still skipped by the loader's corruption tolerance). The lock\n * covers file writes only: the writer lease stays process-local memory.\n */\nexport interface DurableStoreBackend {\n\t/** Appends committed operations to the durable log, in order. */\n\tpersist(operations: readonly DurableStoreOperationV1[]): void;\n\t/**\n\t * Reads the whole log and returns the replayable operations plus skip\n\t * diagnostics. A missing log is an empty backend, not an error; malformed\n\t * lines are skipped and counted — load NEVER throws.\n\t */\n\tload(): DurableStoreLoadResultV1;\n\t/** Releases backend resources. Idempotent; a no-op for the file backend. */\n\tclose(): void;\n\t/**\n\t * Optional store-level writer lease (D-067 phase 2 S2). When present, the\n\t * service resolves its claim/recover/release against it, giving\n\t * cross-process lease mutual exclusion and crash recovery by TTL; when\n\t * absent, the writer lease stays process-local memory.\n\t */\n\treadonly lease?: DurableStoreLeaseV1;\n}\n\n/** Result of {@link DurableStoreLeaseV1.claim}. */\nexport type DurableStoreLeaseClaimV1 =\n\t| { readonly status: \"claimed\" }\n\t| { readonly status: \"held\"; readonly by: string };\n\n/** Result of {@link DurableStoreLeaseV1.recover}. */\nexport type DurableStoreLeaseRecoverV1 =\n\t| { readonly status: \"recovered\"; readonly previousWriter?: string }\n\t| { readonly status: \"held\" };\n\n/**\n * Store-level writer lease: a TTL-based mutual-exclusion token over the whole\n * store, visible across processes. `renew` is the heartbeat — the holder\n * extends its TTL on every persist — and doubles as the liveness check: a\n * writer whose lease expired (or was recovered by another process) fails its\n * next renew with `not_writer`, so stale writes fail closed. All methods may\n * throw `DurableServiceErrorV1` with code `not_writer`.\n */\nexport interface DurableStoreLeaseV1 {\n\t/** Takes the lease; fails while a live foreign holder exists. Re-claim by the live holder refreshes it. */\n\tclaim(writerId: string): DurableStoreLeaseClaimV1;\n\t/** Releases the lease when held by `writerId`; free/expired leases release idempotently; a foreign holder throws. */\n\trelease(writerId: string): { readonly status: \"released\" };\n\t/** Takes over the lease of a holder whose TTL lapsed (dead process); a live foreign holder is reported as `held`. */\n\trecover(writerId: string): DurableStoreLeaseRecoverV1;\n\t/** Heartbeat: extends the holder's TTL; throws `not_writer` when the lease was lost or expired. */\n\trenew(writerId: string): void;\n}\n\n/** The standard durable log path: `<agentDir>/durable/<sessionId>.jsonl`. */\nexport function durableLogPath(agentDir: string, sessionId: string): string {\n\treturn join(agentDir, \"durable\", `${sessionId}.jsonl`);\n}\n\nfunction assertNonEmptyString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.trim().length === 0)\n\t\tthrow new Error(t(\"{label} must be a non-empty string\", { label }));\n}\n\n/**\n * Writes every byte of `data` to the fd, looping over partial `writeSync`\n * results (a short write would silently break the fsync durability promise:\n * the caller fsyncs and closes only after the full payload is handed to the\n * OS). Same pattern as the memory ledger's write path, implemented here with\n * plain node:fs primitives so the file backend needs no ledger dependency.\n */\nconst writeAllSync = (fd: number, data: string): void => {\n\tconst buffer = Buffer.from(data, \"utf8\");\n\tlet written = 0;\n\twhile (written < buffer.length) {\n\t\twritten += writeSync(fd, buffer, written, buffer.length - written);\n\t}\n};\n\n// ---------------------------------------------------------------------------\n// Cross-process persist lock (ledger.ts lock protocol, no ledger import —\n// the two implementations must stay behaviorally identical)\n// ---------------------------------------------------------------------------\n\n/** Suffix of the persistence lock sibling file (`<logPath>.lock`). */\nconst LOCK_FILE_SUFFIX = \".lock\";\n/** Suffix of the store-level writer lease sibling file (`<logPath>.lease`). */\nconst LEASE_FILE_SUFFIX = \".lease\";\n/**\n * Wall-clock wait between lock acquisition attempts. Kept as a per-file\n * constant even though the memory ledger currently uses the same 25 ms value:\n * the two lock protocols stay independent.\n */\nconst LOCK_POLL_INTERVAL_MS = 25;\n/** Default ceiling on waiting for a competing writer's lock. */\nconst DEFAULT_LOCK_TIMEOUT_MS = 5000;\n/**\n * Default age at which a lock counts as abandoned (its writer crashed while\n * holding it). Deliberately far above any live hold time (milliseconds of a\n * bounded append) and above the wait timeout, so a waiter fails loudly at the\n * timeout instead of stealing a lock that might still be held.\n */\nconst DEFAULT_STALE_LOCK_MS = 60_000;\n/**\n * Default writer-lease TTL. The holder renews on every persist (heartbeat);\n * an idle-but-alive process that never persists loses the lease after this\n * window and must `recoverWriter` before writing again. Deliberately far\n * above any test-relevant hold so production never sees surprise expiries\n * within a single persist burst; tests pass small values explicitly.\n */\nconst DEFAULT_LEASE_TTL_MS = 30_000;\n\n/**\n * Takes the cross-process persist lock shared by every durable store file\n * write: exclusive create (`wx`) is the mutex; an existing lock is polled\n * until released, stolen once its mtime exceeds `staleLockMs` (crashed\n * writer), or turned into a thrown error at the wait deadline. The lock file\n * carries a per-acquisition token so a stale-steal victim releasing late can\n * never delete the thief's fresh lock. Same protocol as the memory ledger's\n * append lock (now the memory plugin package's ledger, plugins/memory), reimplemented here to avoid module\n * coupling; the wall-clock lock is a real-time mechanism, deliberately not\n * driven by any injected deterministic `now`.\n */\nfunction acquirePersistLock(lockPath: string, lockTimeoutMs: number, staleLockMs: number): string {\n\tconst deadline = Date.now() + lockTimeoutMs;\n\tfor (;;) {\n\t\tmkdirSync(dirname(lockPath), { recursive: true });\n\t\tconst token = `${process.pid}:${Date.now()}`;\n\t\ttry {\n\t\t\tconst fd = openSync(lockPath, \"wx\");\n\t\t\ttry {\n\t\t\t\twriteFileSync(fd, token);\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\treturn token;\n\t\t} catch (error) {\n\t\t\t// POSIX maps a lost create race to EEXIST; Windows/libuv maps the\n\t\t\t// same concurrent-create sharing violation to EPERM. Both mean the\n\t\t\t// lock file existed when we tried to create it — contend (poll /\n\t\t\t// stale-steal below). For EPERM the existence check re-stats: a\n\t\t\t// lock still there is contention, while ENOENT means the holder\n\t\t\t// released and deleted it between the failed open and this stat\n\t\t\t// (or Windows briefly left a delete-pending name that has now\n\t\t\t// gone) — the same vanished-lock window the EEXIST path tolerates,\n\t\t\t// so retry too. A stat error other than ENOENT means the EPERM was\n\t\t\t// a genuine permission failure and propagates, and the vanish path\n\t\t\t// still rethrows the original EPERM at the wait deadline: a\n\t\t\t// persistent EPERM with no lock file ever appearing is a genuine\n\t\t\t// permission failure, not a held lock.\n\t\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\t\tif (code === \"EPERM\") {\n\t\t\t\tlet vanished = false;\n\t\t\t\ttry {\n\t\t\t\t\tstatSync(lockPath);\n\t\t\t\t} catch (statError) {\n\t\t\t\t\tif ((statError as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n\t\t\t\t\tvanished = true;\n\t\t\t\t}\n\t\t\t\tif (vanished) {\n\t\t\t\t\tif (Date.now() >= deadline) throw error;\n\t\t\t\t\tsleepSync(LOCK_POLL_INTERVAL_MS);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t} else if (code !== \"EEXIST\") {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlet stale = false;\n\t\t\ttry {\n\t\t\t\tstale = Date.now() - statSync(lockPath).mtimeMs > staleLockMs;\n\t\t\t} catch {\n\t\t\t\t// The holder released the lock between the failed open and the\n\t\t\t\t// stat — retry the exclusive create immediately.\n\t\t\t}\n\t\t\tif (stale) {\n\t\t\t\t// A crashed writer left the lock behind: steal it. A competing\n\t\t\t\t// waiter may steal first — the next exclusive create resolves that.\n\t\t\t\ttry {\n\t\t\t\t\tunlinkSync(lockPath);\n\t\t\t\t} catch {\n\t\t\t\t\t// Already stolen — retry.\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (Date.now() >= deadline) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\tt(\"durable store persist lock timeout after {timeoutMs} ms: {lockPath} is held by another writer\", {\n\t\t\t\t\t\ttimeoutMs: lockTimeoutMs,\n\t\t\t\t\t\tlockPath,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t\tsleepSync(LOCK_POLL_INTERVAL_MS);\n\t\t}\n\t}\n}\n\n/** Releases the persist lock, but only if our own token still owns it. */\nfunction releasePersistLock(lockPath: string, token: string): void {\n\ttry {\n\t\tif (readFileSync(lockPath, \"utf8\") === token) unlinkSync(lockPath);\n\t} catch {\n\t\t// The lock is already gone (or unreadable) — nothing to release.\n\t}\n}\n\n/**\n * Creates a JSONL append-only file backend at `path`\n * (ledger.ts pattern: one committed record per line, trailing newline so a\n * crash-truncated tail stays well-formed for the loader). Every persist\n * serializes against other processes through a `<path>.lock` sibling using\n * the memory ledger's lock protocol (exclusive create + 25 ms poll + mtime\n * stale-lock stealing + token-guarded release): `lockTimeoutMs` bounds the\n * wall-clock wait (default 5 s) and `staleLockMs` the abandoned-lock age\n * (default 60 s) — both exist so tests can exercise the timeout and steal\n * paths quickly. The lock makes concurrent appends non-interleaving.\n *\n * The backend also exposes the store-level writer lease (`<path>.lease`\n * sibling, D-067 phase 2 S2): a `{ writerId, expiresAt }` JSON file guarded\n * by the same lock protocol, with a TTL the holder renews on every persist\n * (`leaseTtlMs`, default 30 s — small values exist for tests). A crashed\n * writer's lease becomes recoverable once its TTL lapses; a live foreign\n * holder makes `claim`/`recover` report `held` and the holder's next `renew`\n * (i.e. its next persist) fail closed with `not_writer`.\n */\nexport function createFileDurableStoreBackend(options: {\n\treadonly path: string;\n\t/** Wall-clock ceiling on waiting for a competing writer's lock; default 5 s. */\n\treadonly lockTimeoutMs?: number;\n\t/** Age at which a lock counts as abandoned and is stolen; default 60 s. */\n\treadonly staleLockMs?: number;\n\t/** Writer-lease TTL; the holder renews on every persist. Default 30 s. */\n\treadonly leaseTtlMs?: number;\n}): DurableStoreBackend {\n\tassertNonEmptyString(options.path, \"Durable store path\");\n\tif (options.lockTimeoutMs !== undefined && (!Number.isFinite(options.lockTimeoutMs) || options.lockTimeoutMs <= 0)) {\n\t\tthrow new Error(t(\"Durable store lockTimeoutMs must be a positive number\"));\n\t}\n\tif (options.staleLockMs !== undefined && (!Number.isFinite(options.staleLockMs) || options.staleLockMs <= 0)) {\n\t\tthrow new Error(t(\"Durable store staleLockMs must be a positive number\"));\n\t}\n\tif (options.leaseTtlMs !== undefined && (!Number.isFinite(options.leaseTtlMs) || options.leaseTtlMs <= 0)) {\n\t\tthrow new Error(t(\"Durable store leaseTtlMs must be a positive number\"));\n\t}\n\tconst path = options.path;\n\tconst lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;\n\tconst staleLockMs = options.staleLockMs ?? DEFAULT_STALE_LOCK_MS;\n\tconst leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;\n\tconst lockPath = `${path}${LOCK_FILE_SUFFIX}`;\n\tconst leasePath = `${path}${LEASE_FILE_SUFFIX}`;\n\n\t/** Reads the lease file; missing, unreadable, or malformed content = no lease. */\n\tconst readLease = (): { readonly writerId: string; readonly expiresAt: number } | undefined => {\n\t\ttry {\n\t\t\tconst value: unknown = JSON.parse(readFileSync(leasePath, \"utf8\"));\n\t\t\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined;\n\t\t\tconst writerId = (value as { writerId?: unknown }).writerId;\n\t\t\tconst expiresAt = (value as { expiresAt?: unknown }).expiresAt;\n\t\t\tif (typeof writerId !== \"string\" || writerId.length === 0) return undefined;\n\t\t\tif (typeof expiresAt !== \"number\" || !Number.isFinite(expiresAt)) return undefined;\n\t\t\treturn { writerId, expiresAt };\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t};\n\n\t/**\n\t * Overwrites the lease file (caller holds the persist lock). Deliberately\n\t * NOT fsynced: the lease is a liveness token, not data — the worst case of\n\t * a torn post-crash write is a free lease, which the recover path handles.\n\t */\n\tconst writeLease = (writerId: string, expiresAt: number): void => {\n\t\tconst fd = openSync(leasePath, \"w\");\n\t\ttry {\n\t\t\twriteAllSync(fd, JSON.stringify({ writerId, expiresAt }));\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t};\n\n\t/** True when `current` names a different writer whose TTL has not lapsed. */\n\tconst liveForeign = (\n\t\tcurrent: { readonly writerId: string; readonly expiresAt: number },\n\t\twriterId: string,\n\t): boolean => current.writerId !== writerId && Date.now() <= current.expiresAt;\n\n\tconst lease: DurableStoreLeaseV1 = {\n\t\tclaim(writerId) {\n\t\t\tconst token = acquirePersistLock(lockPath, lockTimeoutMs, staleLockMs);\n\t\t\ttry {\n\t\t\t\tconst current = readLease();\n\t\t\t\tif (current !== undefined && liveForeign(current, writerId))\n\t\t\t\t\treturn { status: \"held\", by: current.writerId };\n\t\t\t\twriteLease(writerId, Date.now() + leaseTtlMs);\n\t\t\t\treturn { status: \"claimed\" };\n\t\t\t} finally {\n\t\t\t\treleasePersistLock(lockPath, token);\n\t\t\t}\n\t\t},\n\t\trelease(writerId) {\n\t\t\tconst token = acquirePersistLock(lockPath, lockTimeoutMs, staleLockMs);\n\t\t\ttry {\n\t\t\t\tconst current = readLease();\n\t\t\t\tif (current === undefined || current.writerId === writerId) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tunlinkSync(leasePath);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Already gone (or vanished between the read and the unlink).\n\t\t\t\t\t}\n\t\t\t\t\treturn { status: \"released\" };\n\t\t\t\t}\n\t\t\t\tthrow new DurableServiceErrorV1(\n\t\t\t\t\t\"not_writer\",\n\t\t\t\t\tt(\"Writer {writerId} does not hold the store lease (held by {holder})\", {\n\t\t\t\t\t\twriterId,\n\t\t\t\t\t\tholder: current.writerId,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\treleasePersistLock(lockPath, token);\n\t\t\t}\n\t\t},\n\t\trecover(writerId) {\n\t\t\tconst token = acquirePersistLock(lockPath, lockTimeoutMs, staleLockMs);\n\t\t\ttry {\n\t\t\t\tconst current = readLease();\n\t\t\t\tif (current !== undefined && liveForeign(current, writerId)) return { status: \"held\" };\n\t\t\t\tconst previousWriter =\n\t\t\t\t\tcurrent !== undefined && current.writerId !== writerId ? current.writerId : undefined;\n\t\t\t\twriteLease(writerId, Date.now() + leaseTtlMs);\n\t\t\t\treturn previousWriter === undefined ? { status: \"recovered\" } : { status: \"recovered\", previousWriter };\n\t\t\t} finally {\n\t\t\t\treleasePersistLock(lockPath, token);\n\t\t\t}\n\t\t},\n\t\trenew(writerId) {\n\t\t\tconst token = acquirePersistLock(lockPath, lockTimeoutMs, staleLockMs);\n\t\t\ttry {\n\t\t\t\tconst current = readLease();\n\t\t\t\tif (current === undefined || current.writerId !== writerId || Date.now() > current.expiresAt) {\n\t\t\t\t\tthrow new DurableServiceErrorV1(\n\t\t\t\t\t\t\"not_writer\",\n\t\t\t\t\t\tt(\"Writer {writerId} lost the store lease (expired or recovered by another writer)\", { writerId }),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\twriteLease(writerId, Date.now() + leaseTtlMs);\n\t\t\t} finally {\n\t\t\t\treleasePersistLock(lockPath, token);\n\t\t\t}\n\t\t},\n\t};\n\n\treturn {\n\t\tpersist(operations) {\n\t\t\tif (operations.length === 0) return;\n\t\t\tconst lines = operations.map((operation) => `${JSON.stringify(storeOperationEnvelope(operation))}\\n`);\n\t\t\t// Competing writers — including another server process — serialize\n\t\t\t// through the `.lock` sibling before the fsynced append, so lines from\n\t\t\t// concurrent processes never interleave (ledger.ts lock protocol).\n\t\t\t// Lock acquisition creates the parent directory, as in the ledger.\n\t\t\tconst token = acquirePersistLock(lockPath, lockTimeoutMs, staleLockMs);\n\t\t\ttry {\n\t\t\t\t// Append is fsynced before close: once persist returns, a hard\n\t\t\t\t// crash cannot lose the appended lines (open → write → fsync →\n\t\t\t\t// close, the same durability pattern as the memory ledger). A\n\t\t\t\t// failure inside the lock releases it and propagates; no\n\t\t\t\t// in-memory state of the service is touched (write-ahead).\n\t\t\t\tconst fd = openSync(path, \"a\");\n\t\t\t\ttry {\n\t\t\t\t\twriteAllSync(fd, lines.join(\"\"));\n\t\t\t\t\tfsyncSync(fd);\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\treleasePersistLock(lockPath, token);\n\t\t\t}\n\t\t},\n\t\tload() {\n\t\t\tlet raw: string;\n\t\t\ttry {\n\t\t\t\traw = readFileSync(path, \"utf8\");\n\t\t\t} catch (error) {\n\t\t\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\t\t\t\treturn { operations: [], corruptedLines: 0, unreadable: false };\n\t\t\t\t}\n\t\t\t\treturn { operations: [], corruptedLines: 0, unreadable: true };\n\t\t\t}\n\t\t\tconst operations: DurableStoreOperationV1[] = [];\n\t\t\tlet corruptedLines = 0;\n\t\t\tfor (const line of raw.split(\"\\n\")) {\n\t\t\t\tif (line.trim() === \"\") continue;\n\t\t\t\tconst operation = parseStoreOperationLine(line);\n\t\t\t\tif (operation === undefined) {\n\t\t\t\t\tcorruptedLines += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\toperations.push(operation);\n\t\t\t}\n\t\t\treturn { operations, corruptedLines, unreadable: false };\n\t\t},\n\t\tclose() {\n\t\t\t// Files are opened per append; nothing persistent to release. The\n\t\t\t// hook exists for backends that hold resources (handles, sockets).\n\t\t},\n\t\tlease,\n\t};\n}\n\n/** Flat one-line envelope: the kind discriminator plus the record fields. */\nfunction storeOperationEnvelope(operation: DurableStoreOperationV1): Record<string, unknown> {\n\treturn { kind: operation.kind, ...operation.record };\n}\n\n/** Parses one log line; undefined when it is malformed (skipped and counted). */\nfunction parseStoreOperationLine(line: string): DurableStoreOperationV1 | undefined {\n\ttry {\n\t\tconst value: unknown = JSON.parse(line);\n\t\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined;\n\t\tconst record = value as Record<string, unknown>;\n\t\tswitch (record.kind) {\n\t\t\tcase \"session\": {\n\t\t\t\tconst base = importRecordBase(record, \"sessionId\", \"Durable store session record\");\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"session\",\n\t\t\t\t\trecord: { sessionId: base.id, data: base.data, persistedAt: base.persistedAt },\n\t\t\t\t};\n\t\t\t}\n\t\t\tcase \"operation\": {\n\t\t\t\tconst base = importRecordBase(record, \"operationId\", \"Durable store operation record\");\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"operation\",\n\t\t\t\t\trecord: {\n\t\t\t\t\t\toperationId: base.id,\n\t\t\t\t\t\tsessionId: importOwningSessionId(record, \"Durable store operation record\"),\n\t\t\t\t\t\tdata: base.data,\n\t\t\t\t\t\tpersistedAt: base.persistedAt,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tcase \"artifact\": {\n\t\t\t\tconst base = importRecordBase(record, \"artifactId\", \"Durable store artifact record\");\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"artifact\",\n\t\t\t\t\trecord: {\n\t\t\t\t\t\tartifactId: base.id,\n\t\t\t\t\t\tsessionId: importOwningSessionId(record, \"Durable store artifact record\"),\n\t\t\t\t\t\tdata: base.data,\n\t\t\t\t\t\tpersistedAt: base.persistedAt,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn undefined;\n\t\t}\n\t} catch {\n\t\t// Corruption tolerance: a malformed line is skipped and counted — a\n\t\t// damaged log must never fail the startup replay.\n\t\treturn undefined;\n\t}\n}\n\nexport interface DurableServiceOptionsV1 {\n\t/** Deterministic clock for `persistedAt`/`exportedAt`; defaults to `Date.now`. */\n\treadonly now?: () => number;\n\t/**\n\t * Optional durable log backend. Without it the service stays purely\n\t * in-memory (backward-compatible MVP behavior); with it every committed\n\t * record is appended to the backend log before the in-memory maps update\n\t * (write-ahead: an append failure leaves memory untouched) and the log is\n\t * replayed at construction to restore state.\n\t */\n\treadonly backend?: DurableStoreBackend;\n}\n\nfunction assertObject(value: unknown, label: string): asserts value is Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new Error(t(\"{label} must be an object\", { label }));\n\t}\n}\n\ninterface ImportedRecordV1 {\n\treadonly id: string;\n\treadonly data: unknown;\n\treadonly persistedAt: number;\n}\n\nfunction importRecordBase(\n\tvalue: Record<string, unknown>,\n\tidField: \"sessionId\" | \"operationId\" | \"artifactId\",\n\tlabel: string,\n): ImportedRecordV1 {\n\tconst id = value[idField];\n\tif (typeof id !== \"string\" || id.length === 0)\n\t\tthrow new Error(t(\"{label} {idField} must be a non-empty string\", { label, idField }));\n\tconst persistedAt = value.persistedAt;\n\tif (typeof persistedAt !== \"number\" || !Number.isFinite(persistedAt)) {\n\t\tthrow new Error(t(\"{label} persistedAt must be a finite number\", { label }));\n\t}\n\t// Legacy (pre-单机收敛) log lines and snapshots may carry a `tenantId` key\n\t// from the removed multi-tenant machinery; it is tolerated as an ignored\n\t// extra key (D-068 增补 1), so it is simply never read here.\n\treturn { id, data: value.data, persistedAt };\n}\n\nfunction importOwningSessionId(value: Record<string, unknown>, label: string): string {\n\tconst sessionId = value.sessionId;\n\tif (typeof sessionId !== \"string\" || sessionId.length === 0) {\n\t\tthrow new Error(t(\"{label} sessionId must be a non-empty string\", { label }));\n\t}\n\treturn sessionId;\n}\n\nexport function createDurableService(options: DurableServiceOptionsV1 = {}): DurableServiceV1 {\n\tconst now = options.now ?? (() => Date.now());\n\tconst sessions = new Map<string, DurableSessionRecordV1>();\n\tconst operations = new Map<string, DurableOperationRecordV1>();\n\tconst artifacts = new Map<string, DurableArtifactRecordV1>();\n\n\t/** Current lease holder; `null` when unclaimed or crash-pending. */\n\tlet holder: string | null = null;\n\t/** Writer found dead in a restored snapshot; blocks claims and writes until recovered. */\n\tlet crashPending: string | null = null;\n\n\t// Startup replay (durable 落盘): with a backend, the log is the source of\n\t// truth for records — replay restores the maps with last-write-wins, the\n\t// same semantics as live persists. With a backend lease (D-067 phase 2\n\t// S2) the writer lease resolves against the store (cross-process mutual\n\t// exclusion, TTL crash recovery); without one it stays process-local.\n\tconst backend = options.backend;\n\tconst backendLease = backend?.lease;\n\tlet replay: DurableReplayDiagnosticsV1 = { corruptedLines: 0, unreadable: false };\n\tif (backend) {\n\t\tconst loaded = backend.load();\n\t\tfor (const operation of loaded.operations) {\n\t\t\tswitch (operation.kind) {\n\t\t\t\tcase \"session\":\n\t\t\t\t\tsessions.set(operation.record.sessionId, operation.record);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"operation\":\n\t\t\t\t\toperations.set(operation.record.operationId, operation.record);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"artifact\":\n\t\t\t\t\tartifacts.set(operation.record.artifactId, operation.record);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treplay = { corruptedLines: loaded.corruptedLines, unreadable: loaded.unreadable };\n\t}\n\n\t/** Write-ahead log append; an append failure leaves the maps untouched. */\n\tconst logOperation = (operation: DurableStoreOperationV1): void => {\n\t\tif (backend) backend.persist([operation]);\n\t};\n\n\tfunction requireWriter(writerId: string): void {\n\t\tif (holder !== writerId) {\n\t\t\tthrow new DurableServiceErrorV1(\n\t\t\t\t\"not_writer\",\n\t\t\t\tt(\"Writer {writerId} does not hold the writer lease\", { writerId }),\n\t\t\t);\n\t\t}\n\t\t// Store-level heartbeat + liveness check (D-067 phase 2 S2): another\n\t\t// process recovering our expired lease fail-closes this write instead\n\t\t// of letting a stale writer interleave records.\n\t\tbackendLease?.renew(writerId);\n\t}\n\n\treturn {\n\t\tpersistSession(input) {\n\t\t\trequireWriter(input.writerId);\n\t\t\tconst record: DurableSessionRecordV1 = {\n\t\t\t\tsessionId: input.sessionId,\n\t\t\t\tdata: input.data,\n\t\t\t\tpersistedAt: now(),\n\t\t\t};\n\t\t\tlogOperation({ kind: \"session\", record });\n\t\t\tsessions.set(input.sessionId, record);\n\t\t\treturn record;\n\t\t},\n\t\tloadSession(input) {\n\t\t\treturn sessions.get(input.sessionId);\n\t\t},\n\t\tpersistOperation(input) {\n\t\t\trequireWriter(input.writerId);\n\t\t\tconst record: DurableOperationRecordV1 = {\n\t\t\t\toperationId: input.operationId,\n\t\t\t\tsessionId: input.sessionId,\n\t\t\t\tdata: input.data,\n\t\t\t\tpersistedAt: now(),\n\t\t\t};\n\t\t\tlogOperation({ kind: \"operation\", record });\n\t\t\toperations.set(input.operationId, record);\n\t\t\treturn record;\n\t\t},\n\t\tloadOperation(input) {\n\t\t\treturn operations.get(input.operationId);\n\t\t},\n\t\tpersistArtifact(input) {\n\t\t\trequireWriter(input.writerId);\n\t\t\tconst record: DurableArtifactRecordV1 = {\n\t\t\t\tartifactId: input.artifactId,\n\t\t\t\tsessionId: input.sessionId,\n\t\t\t\tdata: input.data,\n\t\t\t\tpersistedAt: now(),\n\t\t\t};\n\t\t\tlogOperation({ kind: \"artifact\", record });\n\t\t\tartifacts.set(input.artifactId, record);\n\t\t\treturn record;\n\t\t},\n\t\tloadArtifact(input) {\n\t\t\treturn artifacts.get(input.artifactId);\n\t\t},\n\t\tlistSessions() {\n\t\t\treturn (\n\t\t\t\t[...sessions.values()]\n\t\t\t\t\t// v8 ignore next -- sessionId 是 Map 键恒唯一,相等比较路径不可达\n\t\t\t\t\t.sort((a, b) => (a.sessionId < b.sessionId ? -1 : a.sessionId > b.sessionId ? 1 : 0))\n\t\t\t);\n\t\t},\n\t\tclaimWriter(writerId) {\n\t\t\tif (crashPending !== null) return { status: \"held\", by: crashPending };\n\t\t\tif (backendLease === undefined) {\n\t\t\t\tif (holder !== null && holder !== writerId) return { status: \"held\", by: holder };\n\t\t\t\tholder = writerId;\n\t\t\t\treturn { status: \"claimed\" };\n\t\t\t}\n\t\t\tconst result = backendLease.claim(writerId);\n\t\t\tif (result.status === \"claimed\") {\n\t\t\t\tholder = writerId;\n\t\t\t\treturn { status: \"claimed\" };\n\t\t\t}\n\t\t\treturn { status: \"held\", by: result.by };\n\t\t},\n\t\treleaseWriter(writerId) {\n\t\t\tif (crashPending !== null) {\n\t\t\t\tthrow new DurableServiceErrorV1(\n\t\t\t\t\t\"not_writer\",\n\t\t\t\t\tt(\"Writer {writerId} cannot release a crashed lease; recover it instead\", { writerId }),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (backendLease === undefined) {\n\t\t\t\tif (holder !== null && holder !== writerId) {\n\t\t\t\t\tthrow new DurableServiceErrorV1(\n\t\t\t\t\t\t\"not_writer\",\n\t\t\t\t\t\tt(\"Writer {writerId} does not hold the writer lease (held by {holder})\", { writerId, holder }),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tholder = null;\n\t\t\t\treturn { status: \"released\" };\n\t\t\t}\n\t\t\tconst result = backendLease.release(writerId);\n\t\t\tholder = null;\n\t\t\treturn result;\n\t\t},\n\t\trecoverWriter(writerId) {\n\t\t\tif (crashPending !== null) {\n\t\t\t\tconst previousWriter = crashPending;\n\t\t\t\tcrashPending = null;\n\t\t\t\tholder = writerId;\n\t\t\t\treturn { status: \"recovered\", previousWriter };\n\t\t\t}\n\t\t\tif (backendLease === undefined) {\n\t\t\t\tif (holder !== null && holder !== writerId) return { status: \"held\" };\n\t\t\t\tholder = writerId;\n\t\t\t\treturn { status: \"recovered\" };\n\t\t\t}\n\t\t\tconst result = backendLease.recover(writerId);\n\t\t\tif (result.status === \"recovered\") {\n\t\t\t\tholder = writerId;\n\t\t\t\treturn result.previousWriter === undefined\n\t\t\t\t\t? { status: \"recovered\" }\n\t\t\t\t\t: { status: \"recovered\", previousWriter: result.previousWriter };\n\t\t\t}\n\t\t\treturn { status: \"held\" };\n\t\t},\n\t\treadiness() {\n\t\t\tif (crashPending !== null) {\n\t\t\t\treturn { ready: false, reason: `writer crash is pending recovery: ${crashPending}` };\n\t\t\t}\n\t\t\tif (holder === null) {\n\t\t\t\treturn { ready: false, reason: \"no writer has claimed the lease\" };\n\t\t\t}\n\t\t\treturn { ready: true };\n\t\t},\n\t\texportState() {\n\t\t\treturn {\n\t\t\t\tversion: 1 as const,\n\t\t\t\texportedAt: now(),\n\t\t\t\tsessions: [...sessions.values()],\n\t\t\t\toperations: [...operations.values()],\n\t\t\t\tartifacts: [...artifacts.values()],\n\t\t\t\twriter: { holder, crashPending },\n\t\t\t};\n\t\t},\n\t\timportState(snapshot) {\n\t\t\tassertObject(snapshot, \"Durable snapshot\");\n\t\t\tif (snapshot.version !== 1) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\tt(\"Unsupported durable snapshot version: {version}\", { version: String(snapshot.version) }),\n\t\t\t\t);\n\t\t\t}\n\t\t\tfor (const field of [\"sessions\", \"operations\", \"artifacts\"] as const) {\n\t\t\t\tif (!Array.isArray(snapshot[field]))\n\t\t\t\t\tthrow new Error(t(\"Durable snapshot {field} must be an array\", { field }));\n\t\t\t}\n\t\t\tconst writer = snapshot.writer;\n\t\t\tassertObject(writer, \"Durable snapshot writer\");\n\t\t\tconst snapshotHolder = writer.holder;\n\t\t\tconst snapshotCrashPending = writer.crashPending;\n\t\t\tif (snapshotHolder !== null && typeof snapshotHolder !== \"string\") {\n\t\t\t\tthrow new Error(t(\"Durable snapshot writer holder must be a string or null\"));\n\t\t\t}\n\t\t\tif (snapshotCrashPending !== null && typeof snapshotCrashPending !== \"string\") {\n\t\t\t\tthrow new Error(t(\"Durable snapshot writer crashPending must be a string or null\"));\n\t\t\t}\n\n\t\t\tconst importedSessions = new Map<string, DurableSessionRecordV1>();\n\t\t\tfor (const raw of snapshot.sessions as unknown[]) {\n\t\t\t\tassertObject(raw, \"Durable snapshot session\");\n\t\t\t\tconst record = importRecordBase(raw, \"sessionId\", \"Durable snapshot session\");\n\t\t\t\timportedSessions.set(record.id, {\n\t\t\t\t\tsessionId: record.id,\n\t\t\t\t\tdata: record.data,\n\t\t\t\t\tpersistedAt: record.persistedAt,\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst importedOperations = new Map<string, DurableOperationRecordV1>();\n\t\t\tfor (const raw of snapshot.operations as unknown[]) {\n\t\t\t\tassertObject(raw, \"Durable snapshot operation\");\n\t\t\t\tconst record = importRecordBase(raw, \"operationId\", \"Durable snapshot operation\");\n\t\t\t\timportedOperations.set(record.id, {\n\t\t\t\t\toperationId: record.id,\n\t\t\t\t\tsessionId: importOwningSessionId(raw, \"Durable snapshot operation\"),\n\t\t\t\t\tdata: record.data,\n\t\t\t\t\tpersistedAt: record.persistedAt,\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst importedArtifacts = new Map<string, DurableArtifactRecordV1>();\n\t\t\tfor (const raw of snapshot.artifacts as unknown[]) {\n\t\t\t\tassertObject(raw, \"Durable snapshot artifact\");\n\t\t\t\tconst record = importRecordBase(raw, \"artifactId\", \"Durable snapshot artifact\");\n\t\t\t\timportedArtifacts.set(record.id, {\n\t\t\t\t\tartifactId: record.id,\n\t\t\t\t\tsessionId: importOwningSessionId(raw, \"Durable snapshot artifact\"),\n\t\t\t\t\tdata: record.data,\n\t\t\t\t\tpersistedAt: record.persistedAt,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tsessions.clear();\n\t\t\tfor (const [id, record] of importedSessions) sessions.set(id, record);\n\t\t\toperations.clear();\n\t\t\tfor (const [id, record] of importedOperations) operations.set(id, record);\n\t\t\tartifacts.clear();\n\t\t\tfor (const [id, record] of importedArtifacts) artifacts.set(id, record);\n\n\t\t\t// A writer recorded in the snapshot belonged to the process that\n\t\t\t// produced it; it cannot be alive in this one, so its lease needs\n\t\t\t// recovery before anyone may write again.\n\t\t\tholder = null;\n\t\t\tcrashPending = snapshotHolder ?? snapshotCrashPending;\n\t\t},\n\t\treplayDiagnostics() {\n\t\t\treturn replay;\n\t\t},\n\t};\n}\n"]}
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync, writeSync, } from "node:fs";
|
|
39
39
|
import { dirname, join } from "node:path";
|
|
40
40
|
import { sleepSync } from "../utils/sync-sleep.js";
|
|
41
|
+
import { t } from "../core/i18n/index.js";
|
|
41
42
|
export class DurableServiceErrorV1 extends Error {
|
|
42
43
|
code;
|
|
43
44
|
constructor(code, message) {
|
|
@@ -52,7 +53,7 @@ export function durableLogPath(agentDir, sessionId) {
|
|
|
52
53
|
}
|
|
53
54
|
function assertNonEmptyString(value, label) {
|
|
54
55
|
if (typeof value !== "string" || value.trim().length === 0)
|
|
55
|
-
throw new Error(
|
|
56
|
+
throw new Error(t("{label} must be a non-empty string", { label }));
|
|
56
57
|
}
|
|
57
58
|
/**
|
|
58
59
|
* Writes every byte of `data` to the fd, looping over partial `writeSync`
|
|
@@ -180,7 +181,10 @@ function acquirePersistLock(lockPath, lockTimeoutMs, staleLockMs) {
|
|
|
180
181
|
continue;
|
|
181
182
|
}
|
|
182
183
|
if (Date.now() >= deadline) {
|
|
183
|
-
throw new Error(
|
|
184
|
+
throw new Error(t("durable store persist lock timeout after {timeoutMs} ms: {lockPath} is held by another writer", {
|
|
185
|
+
timeoutMs: lockTimeoutMs,
|
|
186
|
+
lockPath,
|
|
187
|
+
}));
|
|
184
188
|
}
|
|
185
189
|
sleepSync(LOCK_POLL_INTERVAL_MS);
|
|
186
190
|
}
|
|
@@ -218,13 +222,13 @@ function releasePersistLock(lockPath, token) {
|
|
|
218
222
|
export function createFileDurableStoreBackend(options) {
|
|
219
223
|
assertNonEmptyString(options.path, "Durable store path");
|
|
220
224
|
if (options.lockTimeoutMs !== undefined && (!Number.isFinite(options.lockTimeoutMs) || options.lockTimeoutMs <= 0)) {
|
|
221
|
-
throw new Error("Durable store lockTimeoutMs must be a positive number");
|
|
225
|
+
throw new Error(t("Durable store lockTimeoutMs must be a positive number"));
|
|
222
226
|
}
|
|
223
227
|
if (options.staleLockMs !== undefined && (!Number.isFinite(options.staleLockMs) || options.staleLockMs <= 0)) {
|
|
224
|
-
throw new Error("Durable store staleLockMs must be a positive number");
|
|
228
|
+
throw new Error(t("Durable store staleLockMs must be a positive number"));
|
|
225
229
|
}
|
|
226
230
|
if (options.leaseTtlMs !== undefined && (!Number.isFinite(options.leaseTtlMs) || options.leaseTtlMs <= 0)) {
|
|
227
|
-
throw new Error("Durable store leaseTtlMs must be a positive number");
|
|
231
|
+
throw new Error(t("Durable store leaseTtlMs must be a positive number"));
|
|
228
232
|
}
|
|
229
233
|
const path = options.path;
|
|
230
234
|
const lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
@@ -293,7 +297,10 @@ export function createFileDurableStoreBackend(options) {
|
|
|
293
297
|
}
|
|
294
298
|
return { status: "released" };
|
|
295
299
|
}
|
|
296
|
-
throw new DurableServiceErrorV1("not_writer",
|
|
300
|
+
throw new DurableServiceErrorV1("not_writer", t("Writer {writerId} does not hold the store lease (held by {holder})", {
|
|
301
|
+
writerId,
|
|
302
|
+
holder: current.writerId,
|
|
303
|
+
}));
|
|
297
304
|
}
|
|
298
305
|
finally {
|
|
299
306
|
releasePersistLock(lockPath, token);
|
|
@@ -318,7 +325,7 @@ export function createFileDurableStoreBackend(options) {
|
|
|
318
325
|
try {
|
|
319
326
|
const current = readLease();
|
|
320
327
|
if (current === undefined || current.writerId !== writerId || Date.now() > current.expiresAt) {
|
|
321
|
-
throw new DurableServiceErrorV1("not_writer",
|
|
328
|
+
throw new DurableServiceErrorV1("not_writer", t("Writer {writerId} lost the store lease (expired or recovered by another writer)", { writerId }));
|
|
322
329
|
}
|
|
323
330
|
writeLease(writerId, Date.now() + leaseTtlMs);
|
|
324
331
|
}
|
|
@@ -443,16 +450,16 @@ function parseStoreOperationLine(line) {
|
|
|
443
450
|
}
|
|
444
451
|
function assertObject(value, label) {
|
|
445
452
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
446
|
-
throw new Error(
|
|
453
|
+
throw new Error(t("{label} must be an object", { label }));
|
|
447
454
|
}
|
|
448
455
|
}
|
|
449
456
|
function importRecordBase(value, idField, label) {
|
|
450
457
|
const id = value[idField];
|
|
451
458
|
if (typeof id !== "string" || id.length === 0)
|
|
452
|
-
throw new Error(
|
|
459
|
+
throw new Error(t("{label} {idField} must be a non-empty string", { label, idField }));
|
|
453
460
|
const persistedAt = value.persistedAt;
|
|
454
461
|
if (typeof persistedAt !== "number" || !Number.isFinite(persistedAt)) {
|
|
455
|
-
throw new Error(
|
|
462
|
+
throw new Error(t("{label} persistedAt must be a finite number", { label }));
|
|
456
463
|
}
|
|
457
464
|
// Legacy (pre-单机收敛) log lines and snapshots may carry a `tenantId` key
|
|
458
465
|
// from the removed multi-tenant machinery; it is tolerated as an ignored
|
|
@@ -462,7 +469,7 @@ function importRecordBase(value, idField, label) {
|
|
|
462
469
|
function importOwningSessionId(value, label) {
|
|
463
470
|
const sessionId = value.sessionId;
|
|
464
471
|
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
465
|
-
throw new Error(
|
|
472
|
+
throw new Error(t("{label} sessionId must be a non-empty string", { label }));
|
|
466
473
|
}
|
|
467
474
|
return sessionId;
|
|
468
475
|
}
|
|
@@ -507,7 +514,7 @@ export function createDurableService(options = {}) {
|
|
|
507
514
|
};
|
|
508
515
|
function requireWriter(writerId) {
|
|
509
516
|
if (holder !== writerId) {
|
|
510
|
-
throw new DurableServiceErrorV1("not_writer",
|
|
517
|
+
throw new DurableServiceErrorV1("not_writer", t("Writer {writerId} does not hold the writer lease", { writerId }));
|
|
511
518
|
}
|
|
512
519
|
// Store-level heartbeat + liveness check (D-067 phase 2 S2): another
|
|
513
520
|
// process recovering our expired lease fail-closes this write instead
|
|
@@ -582,11 +589,11 @@ export function createDurableService(options = {}) {
|
|
|
582
589
|
},
|
|
583
590
|
releaseWriter(writerId) {
|
|
584
591
|
if (crashPending !== null) {
|
|
585
|
-
throw new DurableServiceErrorV1("not_writer",
|
|
592
|
+
throw new DurableServiceErrorV1("not_writer", t("Writer {writerId} cannot release a crashed lease; recover it instead", { writerId }));
|
|
586
593
|
}
|
|
587
594
|
if (backendLease === undefined) {
|
|
588
595
|
if (holder !== null && holder !== writerId) {
|
|
589
|
-
throw new DurableServiceErrorV1("not_writer",
|
|
596
|
+
throw new DurableServiceErrorV1("not_writer", t("Writer {writerId} does not hold the writer lease (held by {holder})", { writerId, holder }));
|
|
590
597
|
}
|
|
591
598
|
holder = null;
|
|
592
599
|
return { status: "released" };
|
|
@@ -639,21 +646,21 @@ export function createDurableService(options = {}) {
|
|
|
639
646
|
importState(snapshot) {
|
|
640
647
|
assertObject(snapshot, "Durable snapshot");
|
|
641
648
|
if (snapshot.version !== 1) {
|
|
642
|
-
throw new Error(
|
|
649
|
+
throw new Error(t("Unsupported durable snapshot version: {version}", { version: String(snapshot.version) }));
|
|
643
650
|
}
|
|
644
651
|
for (const field of ["sessions", "operations", "artifacts"]) {
|
|
645
652
|
if (!Array.isArray(snapshot[field]))
|
|
646
|
-
throw new Error(
|
|
653
|
+
throw new Error(t("Durable snapshot {field} must be an array", { field }));
|
|
647
654
|
}
|
|
648
655
|
const writer = snapshot.writer;
|
|
649
656
|
assertObject(writer, "Durable snapshot writer");
|
|
650
657
|
const snapshotHolder = writer.holder;
|
|
651
658
|
const snapshotCrashPending = writer.crashPending;
|
|
652
659
|
if (snapshotHolder !== null && typeof snapshotHolder !== "string") {
|
|
653
|
-
throw new Error("Durable snapshot writer holder must be a string or null");
|
|
660
|
+
throw new Error(t("Durable snapshot writer holder must be a string or null"));
|
|
654
661
|
}
|
|
655
662
|
if (snapshotCrashPending !== null && typeof snapshotCrashPending !== "string") {
|
|
656
|
-
throw new Error("Durable snapshot writer crashPending must be a string or null");
|
|
663
|
+
throw new Error(t("Durable snapshot writer crashPending must be a string or null"));
|
|
657
664
|
}
|
|
658
665
|
const importedSessions = new Map();
|
|
659
666
|
for (const raw of snapshot.sessions) {
|