@gajae-code/coding-agent 0.17.2 → 0.17.5
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 +267 -0
- package/dist/types/cli/daemon-cli.d.ts +3 -1
- package/dist/types/cli/github-release.d.ts +17 -0
- package/dist/types/cli/public-command-entry.d.ts +56 -0
- package/dist/types/cli/public-command-errors.d.ts +126 -0
- package/dist/types/cli/public-command-evidence.d.ts +99 -0
- package/dist/types/cli/public-command-help.d.ts +65 -0
- package/dist/types/cli/public-command-registry.d.ts +69 -0
- package/dist/types/cli/session-picker.d.ts +1 -1
- package/dist/types/cli/setup-cli.d.ts +1 -0
- package/dist/types/cli/skills-cli.d.ts +5 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +4 -50
- package/dist/types/commands/sdk.d.ts +20 -37
- package/dist/types/commands/setup.d.ts +3 -0
- package/dist/types/commands/skills.d.ts +9 -0
- package/dist/types/config/autorouting-tier-map.d.ts +1049 -3
- package/dist/types/config/file-lock.d.ts +13 -3
- package/dist/types/config/model-profile-activation.d.ts +45 -1
- package/dist/types/config/model-registry.d.ts +14 -3
- package/dist/types/config/model-resolver.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +11 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
- package/dist/types/daemon/operator-contract.d.ts +5 -1
- package/dist/types/eval/js/worker-protocol.d.ts +2 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +12 -0
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +4 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +11 -2
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +18 -0
- package/dist/types/extensibility/shared-events.d.ts +3 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
- package/dist/types/harness-control-plane/control-endpoint.d.ts +4 -1
- package/dist/types/harness-control-plane/finalize.d.ts +14 -3
- package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/parse.d.ts +9 -1
- package/dist/types/main.d.ts +23 -1
- package/dist/types/modes/acp/acp-agent.d.ts +55 -1
- package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
- package/dist/types/modes/components/session-selector.d.ts +5 -2
- package/dist/types/modes/components/user-message-selector.d.ts +5 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/runtime/process-lifecycle.d.ts +2 -0
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +69 -6
- package/dist/types/runtime-mcp/pool.d.ts +17 -0
- package/dist/types/runtime-mcp/redaction.d.ts +20 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
- package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
- package/dist/types/sdk/broker/broker.d.ts +17 -4
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/identity.d.ts +5 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
- package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
- package/dist/types/sdk/broker/managed-task-dag.d.ts +1335 -0
- package/dist/types/sdk/broker/managed-task-verification.d.ts +37 -0
- package/dist/types/sdk/broker/runtime.d.ts +1 -1
- package/dist/types/sdk/broker/session-index.d.ts +22 -3
- package/dist/types/sdk/broker/spawn-authority.d.ts +12 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -0
- package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
- package/dist/types/sdk/bus/index.d.ts +19 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
- package/dist/types/sdk/cli/master-cli.d.ts +6 -10
- package/dist/types/sdk/cli/rows.d.ts +19 -1
- package/dist/types/sdk/cli/session-cli.d.ts +22 -5
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/guides/cli.d.ts +3 -4
- package/dist/types/sdk/guides/verify.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +9 -0
- package/dist/types/sdk/host/session-runtime.d.ts +31 -0
- package/dist/types/sdk/host/types.d.ts +10 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +85 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +37 -0
- package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +3 -3
- package/dist/types/sdk/startup-capability.d.ts +2 -0
- package/dist/types/sdk/transport/relay.d.ts +1 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +48 -4
- package/dist/types/sdk/transport/stdio.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +175 -23
- package/dist/types/session/fallback-chain-controller.d.ts +6 -6
- package/dist/types/session/fold-coordinator.d.ts +17 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
- package/dist/types/session/session-manager.d.ts +64 -1
- package/dist/types/session/session-work-lease.d.ts +14 -0
- package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
- package/dist/types/setup/provider-onboarding.d.ts +81 -1
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tools/bash-interactive.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +37 -11
- package/dist/types/tools/fetch.d.ts +10 -1
- package/dist/types/tools/job.d.ts +1 -1
- package/dist/types/tools/steer-fold.d.ts +31 -0
- package/dist/types/tools/subagent.d.ts +8 -1
- package/dist/types/utils/git.d.ts +28 -2
- package/dist/types/utils/mupdf-embedded.d.ts +2 -0
- package/dist/types/utils/mupdf.d.ts +4 -0
- package/dist/types/utils/session-title-generation.d.ts +6 -0
- package/dist/types/utils/title-generator.d.ts +11 -2
- package/dist/types/web/scrapers/types.d.ts +3 -0
- package/package.json +18 -10
- package/scripts/build-binary.ts +9 -3
- package/scripts/compile-args.ts +0 -9
- package/scripts/embed-mupdf.ts +52 -0
- package/scripts/generate-sdk-operation-inventory.ts +31 -1
- package/scripts/vendor-markit.ts +196 -0
- package/src/async/job-manager.ts +22 -1
- package/src/cli/daemon-cli.ts +53 -18
- package/src/cli/github-release.ts +87 -5
- package/src/cli/mcp-cli.ts +38 -4
- package/src/cli/public-command-entry.ts +450 -0
- package/src/cli/public-command-errors.ts +581 -0
- package/src/cli/public-command-evidence.ts +717 -0
- package/src/cli/public-command-help.ts +360 -0
- package/src/cli/public-command-registry.ts +541 -0
- package/src/cli/session-picker.ts +2 -1
- package/src/cli/setup-cli.ts +10 -1
- package/src/cli/skills-cli.ts +46 -2
- package/src/cli/stats-cli.ts +11 -1
- package/src/cli.ts +61 -1
- package/src/commands/daemon.ts +65 -69
- package/src/commands/sdk.ts +175 -117
- package/src/commands/setup.ts +2 -0
- package/src/commands/skills.ts +11 -0
- package/src/config/autorouting-tier-map.ts +543 -3
- package/src/config/file-lock.ts +206 -65
- package/src/config/model-discovery-manager.ts +4 -4
- package/src/config/model-profile-activation.ts +417 -66
- package/src/config/model-registry.ts +254 -36
- package/src/config/model-resolver.ts +6 -1
- package/src/config/settings-schema.ts +26 -2
- package/src/coordinator-mcp/policy.ts +10 -5
- package/src/coordinator-mcp/question-gate-codec.ts +17 -0
- package/src/coordinator-mcp/question-state.ts +179 -9
- package/src/coordinator-mcp/server.ts +521 -87
- package/src/coordinator-mcp/session-reaper.ts +6 -4
- package/src/crash/record-loader.ts +6 -2
- package/src/daemon/operator-contract.ts +26 -12
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
- package/src/eval/js/context-manager.ts +8 -8
- package/src/eval/js/worker-core.ts +9 -4
- package/src/eval/js/worker-protocol.ts +2 -0
- package/src/exec/bash-executor.ts +3 -0
- package/src/extensibility/extensions/runner.ts +52 -4
- package/src/extensibility/extensions/types.ts +10 -0
- package/src/extensibility/gjc-plugins/registry.ts +196 -37
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +46 -8
- package/src/extensibility/runtime-skill-discovery.ts +61 -3
- package/src/extensibility/shared-events.ts +3 -1
- package/src/gjc-runtime/session-state-lock.ts +165 -24
- package/src/gjc-runtime/state-writer.ts +317 -6
- package/src/gjc-runtime/ultragoal-succession.ts +31 -3
- package/src/harness-control-plane/control-endpoint.ts +24 -6
- package/src/harness-control-plane/finalize.ts +312 -25
- package/src/harness-control-plane/owner.ts +323 -89
- package/src/internal-urls/docs-index.generated.ts +23 -20
- package/src/internal-urls/embedded-protocol.ts +83 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/parse.ts +21 -1
- package/src/internal-urls/router.ts +4 -2
- package/src/main.ts +192 -74
- package/src/modes/acp/acp-agent.ts +973 -56
- package/src/modes/acp/startup-provenance.ts +59 -0
- package/src/modes/components/custom-provider-wizard.ts +308 -7
- package/src/modes/components/gjc-bundle-settings.ts +21 -1
- package/src/modes/components/oauth-selector.ts +9 -0
- package/src/modes/components/session-selector.ts +47 -4
- package/src/modes/components/tool-status-header.ts +72 -6
- package/src/modes/components/user-message-selector.ts +57 -15
- package/src/modes/controllers/command-controller.ts +58 -32
- package/src/modes/controllers/extension-ui-controller.ts +33 -7
- package/src/modes/controllers/input-controller.ts +4 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +161 -15
- package/src/modes/interactive-mode.ts +21 -7
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
- package/src/modes/types.ts +2 -2
- package/src/prompts/agent-fragments/browser-aside.md +1 -0
- package/src/prompts/system/title-system.md +1 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/read.md +2 -0
- package/src/runtime/network-prewarm-service.test.ts +36 -1
- package/src/runtime/network-prewarm-service.ts +28 -3
- package/src/runtime/process-lifecycle.ts +22 -11
- package/src/runtime-mcp/config.ts +16 -1
- package/src/runtime-mcp/loader.ts +6 -3
- package/src/runtime-mcp/manager.ts +845 -109
- package/src/runtime-mcp/pool.test.ts +31 -0
- package/src/runtime-mcp/pool.ts +304 -28
- package/src/runtime-mcp/redaction.ts +42 -0
- package/src/runtime-mcp/startup-policy.ts +7 -0
- package/src/runtime-mcp/tool-bridge.ts +71 -0
- package/src/runtime-mcp/transports/http.ts +108 -25
- package/src/sdk/acp/adapter.ts +27 -8
- package/src/sdk/broker/broker.ts +1431 -198
- package/src/sdk/broker/ensure.ts +21 -3
- package/src/sdk/broker/identity.ts +22 -1
- package/src/sdk/broker/lifecycle-ledger.ts +95 -31
- package/src/sdk/broker/lifecycle.ts +690 -141
- package/src/sdk/broker/managed-task-dag.ts +1927 -0
- package/src/sdk/broker/managed-task-verification.ts +642 -0
- package/src/sdk/broker/runtime.ts +1 -1
- package/src/sdk/broker/session-index.ts +97 -4
- package/src/sdk/broker/spawn-authority.ts +175 -14
- package/src/sdk/broker/startup-failure.ts +80 -1
- package/src/sdk/broker/stderr-drainer.ts +59 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/index.ts +707 -81
- package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -3
- package/src/sdk/cli/master-cli.ts +47 -36
- package/src/sdk/cli/rows.ts +30 -7
- package/src/sdk/cli/session-cli.ts +705 -184
- package/src/sdk/client/client.ts +18 -0
- package/src/sdk/guides/catalog.ts +2 -2
- package/src/sdk/guides/cli.ts +20 -62
- package/src/sdk/guides/verify.ts +14 -0
- package/src/sdk/host/host.ts +49 -5
- package/src/sdk/host/session-runtime.test.ts +184 -2
- package/src/sdk/host/session-runtime.ts +322 -101
- package/src/sdk/host/types.ts +12 -1
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
- package/src/sdk/host/websocket-transport.ts +40 -6
- package/src/sdk/index.ts +11 -0
- package/src/sdk/lifecycle/service.ts +2 -0
- package/src/sdk/prompt-deadline-flush.ts +356 -0
- package/src/sdk/prompt-deadline-manager.ts +133 -1
- package/src/sdk/prompt-tool-boundary.ts +121 -0
- package/src/sdk/protocol/operation-inventory.generated.json +106 -0
- package/src/sdk/router/session-router.ts +9 -1
- package/src/sdk/session.ts +763 -116
- package/src/sdk/startup-capability.ts +1 -1
- package/src/sdk/transport/relay.ts +71 -6
- package/src/sdk/transport/serve-cli.ts +397 -42
- package/src/sdk/transport/socket.ts +12 -11
- package/src/sdk/transport/stdio.ts +7 -3
- package/src/session/agent-session.ts +2472 -407
- package/src/session/fallback-chain-controller.ts +6 -7
- package/src/session/fold-coordinator.ts +24 -5
- package/src/session/internal/managed-session-scope.ts +68 -18
- package/src/session/internal/managed-session-storage.ts +334 -12
- package/src/session/session-manager.ts +382 -44
- package/src/session/session-work-lease.ts +34 -0
- package/src/setup/model-onboarding-guidance.ts +1 -1
- package/src/setup/provider-onboarding.ts +397 -24
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +259 -15
- package/src/task/executor.ts +46 -17
- package/src/task/index.ts +14 -2
- package/src/task/types.ts +2 -0
- package/src/tools/bash-interactive.ts +2 -0
- package/src/tools/bash.ts +118 -78
- package/src/tools/fetch.ts +140 -25
- package/src/tools/job.ts +151 -25
- package/src/tools/output-meta.ts +15 -2
- package/src/tools/path-utils.ts +27 -3
- package/src/tools/read.ts +3 -3
- package/src/tools/steer-fold.ts +107 -0
- package/src/tools/subagent-render.ts +24 -1
- package/src/tools/subagent.ts +27 -1
- package/src/tools/tool-catalog.generated.ts +29 -2
- package/src/tools/tool-errors.ts +4 -0
- package/src/utils/git.ts +53 -3
- package/src/utils/markit.ts +55 -41
- package/src/utils/mupdf-assets.d.ts +4 -0
- package/src/utils/mupdf-embedded.ts +4 -0
- package/src/utils/mupdf.ts +136 -0
- package/src/utils/session-title-generation.ts +29 -0
- package/src/utils/title-generator.ts +46 -2
- package/src/web/scrapers/types.ts +24 -3
- package/vendor/markit-ai/LICENSE +21 -0
- package/vendor/markit-ai/MANIFEST.json +412 -0
- package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
- package/vendor/markit-ai/dist/commands/config.js +139 -0
- package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
- package/vendor/markit-ai/dist/commands/convert.js +113 -0
- package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/formats.js +92 -0
- package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/init.js +29 -0
- package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/onboard.js +61 -0
- package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
- package/vendor/markit-ai/dist/commands/plugin.js +58 -0
- package/vendor/markit-ai/dist/config.d.ts +26 -0
- package/vendor/markit-ai/dist/config.js +42 -0
- package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
- package/vendor/markit-ai/dist/converters/audio.js +104 -0
- package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
- package/vendor/markit-ai/dist/converters/csv.js +83 -0
- package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/docx.js +57 -0
- package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/epub.js +128 -0
- package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
- package/vendor/markit-ai/dist/converters/github.js +148 -0
- package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/html.js +30 -0
- package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/image.js +124 -0
- package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
- package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
- package/vendor/markit-ai/dist/converters/iwork.js +391 -0
- package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/json.js +21 -0
- package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
- package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
- package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
- package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
- package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
- package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
- package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
- package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
- package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
- package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
- package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
- package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
- package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
- package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
- package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
- package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
- package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/pptx.js +289 -0
- package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
- package/vendor/markit-ai/dist/converters/rss.js +138 -0
- package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
- package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
- package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/xml.js +18 -0
- package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/yaml.js +17 -0
- package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/zip.js +57 -0
- package/vendor/markit-ai/dist/index.d.ts +30 -0
- package/vendor/markit-ai/dist/index.js +26 -0
- package/vendor/markit-ai/dist/main.d.ts +2 -0
- package/vendor/markit-ai/dist/main.js +191 -0
- package/vendor/markit-ai/dist/markit.d.ts +45 -0
- package/vendor/markit-ai/dist/markit.js +267 -0
- package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
- package/vendor/markit-ai/dist/plugins/api.js +42 -0
- package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
- package/vendor/markit-ai/dist/plugins/index.js +3 -0
- package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
- package/vendor/markit-ai/dist/plugins/installer.js +176 -0
- package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
- package/vendor/markit-ai/dist/plugins/loader.js +61 -0
- package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
- package/vendor/markit-ai/dist/plugins/types.js +1 -0
- package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
- package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
- package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
- package/vendor/markit-ai/dist/providers/index.js +56 -0
- package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
- package/vendor/markit-ai/dist/providers/openai.js +76 -0
- package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
- package/vendor/markit-ai/dist/providers/types.js +1 -0
- package/vendor/markit-ai/dist/types.d.ts +36 -0
- package/vendor/markit-ai/dist/types.js +1 -0
- package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
- package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
- package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
- package/vendor/markit-ai/dist/utils/output.js +31 -0
- package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
- package/vendor/markit-ai/dist/utils/turndown.js +70 -0
- package/vendor/markit-ai/package.json +74 -0
- package/vendor/markit-ai.patch +49 -0
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { logger, resolveEquivalentPath } from "@gajae-code/utils";
|
|
|
8
8
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
9
9
|
import type { ModelProfileErrorDetails } from "../../config/model-profile-contract";
|
|
10
10
|
import { planLaunchWorktree } from "../../gjc-runtime/launch-worktree";
|
|
11
|
+
import { readExistingStateForMutation, withWorkflowStateLock } from "../../gjc-runtime/state-writer";
|
|
11
12
|
import { SdkClient, SdkClientError } from "../client";
|
|
12
13
|
import {
|
|
13
14
|
BROKER_RUNTIME_ABORT_CAPABILITY_FIELD,
|
|
@@ -37,7 +38,13 @@ import {
|
|
|
37
38
|
redactBrokerDiscovery,
|
|
38
39
|
} from "./discovery";
|
|
39
40
|
import { endpointIncarnation, matchesIndexedEndpointFile, readEndpointFile } from "./endpoint-authority";
|
|
40
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
deriveIdempotencyIdentity,
|
|
43
|
+
deriveLegacyIdentity,
|
|
44
|
+
deriveLegacyTargetIdentity,
|
|
45
|
+
deriveScopedIdempotencyIdentity,
|
|
46
|
+
getBrokerIdentityKey,
|
|
47
|
+
} from "./identity";
|
|
41
48
|
import {
|
|
42
49
|
canonicalDeleteLocatorPath,
|
|
43
50
|
executeLifecycle,
|
|
@@ -52,7 +59,38 @@ import {
|
|
|
52
59
|
LifecycleLedger,
|
|
53
60
|
type LifecycleStartupFailureReceipt,
|
|
54
61
|
type LifecycleState,
|
|
62
|
+
type TerminalReadBack,
|
|
55
63
|
} from "./lifecycle-ledger";
|
|
64
|
+
import {
|
|
65
|
+
cancelManagedTasks,
|
|
66
|
+
createManagedDomainBinding,
|
|
67
|
+
currentManagedRevision,
|
|
68
|
+
defineManagedTaskGraph,
|
|
69
|
+
inspectManagedAttemptByNativeIdentity,
|
|
70
|
+
loadManagedDomainBinding,
|
|
71
|
+
loadManagedEnrollmentRecord,
|
|
72
|
+
lookupManagedAttemptByNativeIdentity,
|
|
73
|
+
type ManagedAttemptRef,
|
|
74
|
+
type ManagedDomainBinding,
|
|
75
|
+
type ManagedEnrollmentRecord,
|
|
76
|
+
type ManagedTaskAttempt,
|
|
77
|
+
managedAttemptRefMatches,
|
|
78
|
+
managedIdentity,
|
|
79
|
+
managedNativeVector,
|
|
80
|
+
managedTaskDomainPath,
|
|
81
|
+
markManagedEnrollmentEstablished,
|
|
82
|
+
markManagedEnrollmentPending,
|
|
83
|
+
markManagedEnrollmentPublishing,
|
|
84
|
+
observeOrAdmitManagedTask,
|
|
85
|
+
recordManagedEnrollment,
|
|
86
|
+
recordManagedNativeObservation,
|
|
87
|
+
restoreManagedAttemptRefs,
|
|
88
|
+
reviseManagedTaskGraph,
|
|
89
|
+
transactManagedTaskDomain,
|
|
90
|
+
validateManagedTaskDomain,
|
|
91
|
+
withManagedNativeEffectAuthorized,
|
|
92
|
+
} from "./managed-task-dag";
|
|
93
|
+
import { reconcileRunningManagedVerification, verifyManagedTaskAttempt } from "./managed-task-verification";
|
|
56
94
|
import { createMasterCapabilityVerifier, readEndpoint } from "./master-capability";
|
|
57
95
|
import { sdkInternalRuntimeImage } from "./runtime";
|
|
58
96
|
import { type IndexedSession, isSessionAuthorityEligible, SessionIndex, type SessionList } from "./session-index";
|
|
@@ -209,7 +247,7 @@ export type BrokerCleanupEvidence = {
|
|
|
209
247
|
artifactsIdentity?: BrokerCleanupIdentity;
|
|
210
248
|
transcriptIdentity?: BrokerCleanupIdentity;
|
|
211
249
|
transcriptParentIdentity?: { dev: string; ino: string };
|
|
212
|
-
/** Identity-bound lifecycle
|
|
250
|
+
/** Identity-bound lifecycle metadataker retained when exact cleanup is deferred. */
|
|
213
251
|
metadataIdentity?: BrokerCleanupIdentity;
|
|
214
252
|
metadataPath?: string;
|
|
215
253
|
/** Monotonic append-only cleanup attempt. */
|
|
@@ -241,7 +279,7 @@ export type BrokerCleanupEvidence = {
|
|
|
241
279
|
/** Fully identity-bound startup-failure cleanup plan, persisted before any detach. */
|
|
242
280
|
lifecycleFiles?: BrokerLifecycleCleanupFile[];
|
|
243
281
|
lifecycleParentIdentity?: { dev: string; ino: string };
|
|
244
|
-
/** Delete metadata receipts authorize only the
|
|
282
|
+
/** Delete metadata receipts authorize only the canonicalker/ready sibling pair. */
|
|
245
283
|
lifecycleDeleteMetadata?: true;
|
|
246
284
|
};
|
|
247
285
|
export type BrokerResponse =
|
|
@@ -259,6 +297,17 @@ export type BrokerResponse =
|
|
|
259
297
|
durableEffects?: LifecycleDurableEffectsReceipt;
|
|
260
298
|
startupFailure?: LifecycleStartupFailureReceipt;
|
|
261
299
|
};
|
|
300
|
+
type VerifiedManagedOwner =
|
|
301
|
+
| {
|
|
302
|
+
kind: "verified";
|
|
303
|
+
ownerSessionId: string;
|
|
304
|
+
attestationEpoch: string;
|
|
305
|
+
controlRoot: string;
|
|
306
|
+
enrollmentId: string;
|
|
307
|
+
worktrees: string[];
|
|
308
|
+
aliases?: string[];
|
|
309
|
+
}
|
|
310
|
+
| { kind: "denied"; response: BrokerResponse };
|
|
262
311
|
const error = (code: BrokerErrorCode, message: string): BrokerResponse => ({ ok: false, error: { code, message } });
|
|
263
312
|
const spawnFailureError = (failure: SpawnSubstrateFailure): BrokerResponse => ({
|
|
264
313
|
ok: false,
|
|
@@ -334,7 +383,7 @@ function lifecycleResponseState(response: BrokerResponse): LifecycleState {
|
|
|
334
383
|
: "terminal_error";
|
|
335
384
|
}
|
|
336
385
|
|
|
337
|
-
type InputNormalization = { input: Record<string, unknown> } | BrokerResponse;
|
|
386
|
+
export type InputNormalization = { input: Record<string, unknown> } | BrokerResponse;
|
|
338
387
|
|
|
339
388
|
type SessionListCursor = {
|
|
340
389
|
sessions: IndexedSession[];
|
|
@@ -356,6 +405,7 @@ type SpawnInFlight = {
|
|
|
356
405
|
resolve: (response: BrokerResponse) => void;
|
|
357
406
|
claimId?: string;
|
|
358
407
|
phase?: SpawnClaimV2["state"];
|
|
408
|
+
managedRequestHash?: string;
|
|
359
409
|
};
|
|
360
410
|
|
|
361
411
|
/** Complete five-leg child endpoint pin captured at registration. */
|
|
@@ -411,6 +461,9 @@ type SpawnAdmissionInput = {
|
|
|
411
461
|
cwd: string;
|
|
412
462
|
modelId?: string;
|
|
413
463
|
modelPreset?: string;
|
|
464
|
+
managedAttempt?: ManagedAttemptRef;
|
|
465
|
+
managedBinding?: ManagedDomainBinding;
|
|
466
|
+
managedNativeVector?: string;
|
|
414
467
|
};
|
|
415
468
|
|
|
416
469
|
function parseSpawnInput(
|
|
@@ -505,6 +558,23 @@ const SPAWN_HOST_REGISTRATION_TIMEOUT_MS = 10_000;
|
|
|
505
558
|
const SPAWN_HOST_REGISTRATION_POLL_MS = 50;
|
|
506
559
|
const SPAWN_PROMPT_EXCHANGE_TIMEOUT_MS = 10_000;
|
|
507
560
|
const MASTER_ORPHAN_GRACE_DEFAULT_MS = 120_000;
|
|
561
|
+
const MANAGED_CLOSE_WAIT_MS = 10_000;
|
|
562
|
+
|
|
563
|
+
function settlesWithin(promise: Promise<unknown>, timeoutMs: number): Promise<boolean> {
|
|
564
|
+
const settled = Promise.withResolvers<boolean>();
|
|
565
|
+
const timer = setTimeout(() => settled.resolve(false), timeoutMs);
|
|
566
|
+
void promise.then(
|
|
567
|
+
() => {
|
|
568
|
+
clearTimeout(timer);
|
|
569
|
+
settled.resolve(true);
|
|
570
|
+
},
|
|
571
|
+
() => {
|
|
572
|
+
clearTimeout(timer);
|
|
573
|
+
settled.resolve(true);
|
|
574
|
+
},
|
|
575
|
+
);
|
|
576
|
+
return settled.promise;
|
|
577
|
+
}
|
|
508
578
|
|
|
509
579
|
/** The five legs every usable pin must carry; a partial pin is missing authority. */
|
|
510
580
|
type CompleteSpawnPinAuthority = SpawnAuthorityV1 & {
|
|
@@ -563,6 +633,26 @@ function matchesSpawnPin(candidate: IndexedSession, pinned: SpawnHostRegistratio
|
|
|
563
633
|
);
|
|
564
634
|
}
|
|
565
635
|
|
|
636
|
+
/** Exact admission predicate for the child host row published after launch. */
|
|
637
|
+
export function spawnRegistrationMatches(
|
|
638
|
+
candidate: IndexedSession,
|
|
639
|
+
expected: { childId: string; cwd: string; stateRoot: string },
|
|
640
|
+
): boolean {
|
|
641
|
+
return (
|
|
642
|
+
candidate.sessionId === expected.childId &&
|
|
643
|
+
candidate.endpointGeneration > 0 &&
|
|
644
|
+
candidate.live &&
|
|
645
|
+
!candidate.terminal &&
|
|
646
|
+
!candidate.terminalUncertain &&
|
|
647
|
+
// The child publishes a realpath-canonicalized locator while the launch
|
|
648
|
+
// spec carries the caller's lexical spelling. Compare path identity so a
|
|
649
|
+
// symlinked workspace (macOS /var vs /private/var) cannot strand a valid
|
|
650
|
+
// registration behind different spellings.
|
|
651
|
+
resolveEquivalentPath(candidate.locator.cwd) === resolveEquivalentPath(expected.cwd) &&
|
|
652
|
+
resolveEquivalentPath(candidate.locator.stateRoot) === resolveEquivalentPath(expected.stateRoot)
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
566
656
|
/** Rebuilds the provider proof from durable authority facts only. */
|
|
567
657
|
function spawnProofFromAuthority(authority: SpawnAuthorityV1): SpawnSubstrateProof {
|
|
568
658
|
return {
|
|
@@ -635,7 +725,7 @@ function normalizeAliasedString(
|
|
|
635
725
|
return { value: values[0] };
|
|
636
726
|
}
|
|
637
727
|
|
|
638
|
-
function normalizeBrokerInput(operation: string, input: Record<string, unknown>): InputNormalization {
|
|
728
|
+
export function normalizeBrokerInput(operation: string, input: Record<string, unknown>): InputNormalization {
|
|
639
729
|
const normalized: Record<string, unknown> = { ...input };
|
|
640
730
|
const session = normalizeAliasedString(input, "sessionId", ["id"]);
|
|
641
731
|
if (session.error) return error("invalid_input", session.error);
|
|
@@ -725,11 +815,47 @@ function canonicalJson(value: unknown): string {
|
|
|
725
815
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
726
816
|
const record = value as Record<string, unknown>;
|
|
727
817
|
return `{${Object.keys(record)
|
|
818
|
+
.filter(key => record[key] !== undefined)
|
|
728
819
|
.sort()
|
|
729
820
|
.map(key => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
|
|
730
821
|
.join(",")}}`;
|
|
731
822
|
}
|
|
732
823
|
|
|
824
|
+
type TerminalPersistenceVerification =
|
|
825
|
+
| { kind: "unverified" }
|
|
826
|
+
| { kind: "verified" }
|
|
827
|
+
| { kind: "uncertain"; mismatches: readonly string[] };
|
|
828
|
+
|
|
829
|
+
function verifyTerminalPersistence(
|
|
830
|
+
readBack: TerminalReadBack,
|
|
831
|
+
storedResponse: BrokerResponse,
|
|
832
|
+
durableEffects: LifecycleDurableEffectsReceipt | undefined,
|
|
833
|
+
startupFailure: LifecycleStartupFailureReceipt | undefined,
|
|
834
|
+
): TerminalPersistenceVerification {
|
|
835
|
+
// An absent read-back is a verification failure, not evidence that the
|
|
836
|
+
// operation itself is ambiguous. The terminal transition was already synced
|
|
837
|
+
// before this read, so preserve its settled outcome and let a later reader
|
|
838
|
+
// reconcile the durable row.
|
|
839
|
+
if (readBack.kind === "absent") return { kind: "unverified" };
|
|
840
|
+
// A *rejected* row is the opposite: the ledger holds something for this request
|
|
841
|
+
// and refused it. That is damaged durable state, which previously fenced the
|
|
842
|
+
// session and must keep fencing it — treating it as "not yet written" would let
|
|
843
|
+
// a corrupt row clear the fence it exists to raise.
|
|
844
|
+
if (readBack.kind === "rejected") return { kind: "uncertain", mismatches: [`readBack:${readBack.reason}`] };
|
|
845
|
+
const persisted = readBack.entry;
|
|
846
|
+
const optionalReceiptJson = (value: unknown): string | undefined =>
|
|
847
|
+
value === undefined || value === null ? undefined : canonicalJson(value);
|
|
848
|
+
// The field name is always present; only the two compared receipt renderings are
|
|
849
|
+
// optional. Naming that shape keeps the mismatch list a plain string list.
|
|
850
|
+
const comparisons: readonly (readonly [string, string | undefined, string | undefined])[] = [
|
|
851
|
+
["response", canonicalJson(persisted.response), canonicalJson(storedResponse)],
|
|
852
|
+
["durableEffects", optionalReceiptJson(persisted.durableEffects), optionalReceiptJson(durableEffects)],
|
|
853
|
+
["startupFailure", optionalReceiptJson(persisted.startupFailure), optionalReceiptJson(startupFailure)],
|
|
854
|
+
];
|
|
855
|
+
const mismatches = comparisons.filter(([, actual, expected]) => actual !== expected).map(([field]) => field);
|
|
856
|
+
return mismatches.length === 0 ? { kind: "verified" } : { kind: "uncertain", mismatches };
|
|
857
|
+
}
|
|
858
|
+
|
|
733
859
|
function credentialFreeLifecycleResponse(value: unknown): unknown {
|
|
734
860
|
if (Array.isArray(value)) return value.map(credentialFreeLifecycleResponse);
|
|
735
861
|
if (value === null || typeof value !== "object") return value;
|
|
@@ -903,15 +1029,31 @@ function lifecycleTarget(operation: string, input: Record<string, unknown>): unk
|
|
|
903
1029
|
sourceSessionPath: string(input.sourceSessionPath, input.sourcePath, input.sessionPath),
|
|
904
1030
|
};
|
|
905
1031
|
case "session.resume":
|
|
906
|
-
case "session.close":
|
|
907
1032
|
case "session.delete":
|
|
908
1033
|
case "session.reconcile_uncertain":
|
|
909
1034
|
return { sessionId: id };
|
|
1035
|
+
case "session.close":
|
|
1036
|
+
return { sessionId: id, ...closeTargetAuthority(input) };
|
|
910
1037
|
default:
|
|
911
1038
|
return { operation, root, sessionId: id };
|
|
912
1039
|
}
|
|
913
1040
|
}
|
|
914
1041
|
|
|
1042
|
+
function closeTargetAuthority(input: Record<string, unknown>): {
|
|
1043
|
+
endpointGeneration?: number;
|
|
1044
|
+
endpointIncarnation?: string;
|
|
1045
|
+
} {
|
|
1046
|
+
if (
|
|
1047
|
+
typeof input.endpointGeneration !== "number" ||
|
|
1048
|
+
!Number.isSafeInteger(input.endpointGeneration) ||
|
|
1049
|
+
input.endpointGeneration <= 0 ||
|
|
1050
|
+
typeof input.endpointIncarnation !== "string" ||
|
|
1051
|
+
!/^[a-f0-9]{64}$/u.test(input.endpointIncarnation)
|
|
1052
|
+
)
|
|
1053
|
+
return {};
|
|
1054
|
+
return { endpointGeneration: input.endpointGeneration, endpointIncarnation: input.endpointIncarnation };
|
|
1055
|
+
}
|
|
1056
|
+
|
|
915
1057
|
/**
|
|
916
1058
|
* A lifecycle request without a worktree remains serialized by its state root.
|
|
917
1059
|
* Worktree launches instead serialize on the source repository plus the
|
|
@@ -990,10 +1132,11 @@ function isBrokerLockArtifactName(name: string): boolean {
|
|
|
990
1132
|
* Decide whether one candidate directory is provably abandoned.
|
|
991
1133
|
*
|
|
992
1134
|
* Fail-closed by construction: every branch that cannot prove abandonment
|
|
993
|
-
* returns a retention reason. A tombstone
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
*
|
|
1135
|
+
* returns a retention reason. A tombstone with an owner record is only
|
|
1136
|
+
* abandoned when that record names a dead PID. A missing record is abandoned
|
|
1137
|
+
* only when the aged tombstone is empty; non-empty tombstones remain retained
|
|
1138
|
+
* as ambiguous evidence. Backup directories carry no owner contract, so an
|
|
1139
|
+
* absent record there is not ambiguity and age alone governs.
|
|
997
1140
|
*/
|
|
998
1141
|
async function classifyBrokerLockArtifact(
|
|
999
1142
|
directory: string,
|
|
@@ -1014,7 +1157,14 @@ async function classifyBrokerLockArtifact(
|
|
|
1014
1157
|
} catch (error) {
|
|
1015
1158
|
const code = (error as NodeJS.ErrnoException).code;
|
|
1016
1159
|
if (code !== "ENOENT" && code !== "ENOTDIR") return "owner-record-unreadable";
|
|
1017
|
-
|
|
1160
|
+
if (!name.startsWith(BROKER_LOCK_TOMBSTONE_PREFIX)) return "abandoned";
|
|
1161
|
+
try {
|
|
1162
|
+
const entries = await fs.readdir(target);
|
|
1163
|
+
return entries.length === 0 ? "abandoned" : "owner-record-missing";
|
|
1164
|
+
} catch (readError) {
|
|
1165
|
+
if ((readError as NodeJS.ErrnoException).code === "ENOENT") throw readError;
|
|
1166
|
+
return "owner-record-unreadable";
|
|
1167
|
+
}
|
|
1018
1168
|
}
|
|
1019
1169
|
let pid: unknown;
|
|
1020
1170
|
try {
|
|
@@ -1034,7 +1184,8 @@ async function classifyBrokerLockArtifact(
|
|
|
1034
1184
|
* of the lock's dev+ino, so a machine accrues one directory per dead owner and
|
|
1035
1185
|
* nothing ever removed them (54 on the install in #3963). Reaping is
|
|
1036
1186
|
* best-effort and fail-closed: anything live, unreadable, permission-denied, or
|
|
1037
|
-
* otherwise ambiguous is kept
|
|
1187
|
+
* otherwise ambiguous is kept. Retained paths are logged at debug level and
|
|
1188
|
+
* summarized in one warning per reap pass.
|
|
1038
1189
|
*/
|
|
1039
1190
|
export async function reapStaleBrokerLockArtifacts(input: {
|
|
1040
1191
|
agentDir: string;
|
|
@@ -1048,6 +1199,7 @@ export async function reapStaleBrokerLockArtifacts(input: {
|
|
|
1048
1199
|
const pidAlive = input.pidAlive ?? isPidAlive;
|
|
1049
1200
|
const removed: string[] = [];
|
|
1050
1201
|
const retained: BrokerLockArtifactRetention[] = [];
|
|
1202
|
+
const retainedByReason = new Map<BrokerLockArtifactRetentionReason, number>();
|
|
1051
1203
|
let names: string[];
|
|
1052
1204
|
try {
|
|
1053
1205
|
names = await fs.readdir(directory);
|
|
@@ -1069,7 +1221,10 @@ export async function reapStaleBrokerLockArtifacts(input: {
|
|
|
1069
1221
|
}
|
|
1070
1222
|
if (verdict !== "abandoned") {
|
|
1071
1223
|
retained.push({ path: target, reason: verdict });
|
|
1072
|
-
if (verdict !== "within-grace")
|
|
1224
|
+
if (verdict !== "within-grace") {
|
|
1225
|
+
logger.debug(`sdk broker: retained stale lock artifact ${name} (${verdict})`);
|
|
1226
|
+
retainedByReason.set(verdict, (retainedByReason.get(verdict) ?? 0) + 1);
|
|
1227
|
+
}
|
|
1073
1228
|
continue;
|
|
1074
1229
|
}
|
|
1075
1230
|
try {
|
|
@@ -1078,9 +1233,18 @@ export async function reapStaleBrokerLockArtifacts(input: {
|
|
|
1078
1233
|
} catch (error) {
|
|
1079
1234
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
1080
1235
|
retained.push({ path: target, reason: "removal-failed" });
|
|
1081
|
-
logger.
|
|
1236
|
+
logger.debug(`sdk broker: retained stale lock artifact ${name} (removal-failed)`);
|
|
1237
|
+
retainedByReason.set("removal-failed", (retainedByReason.get("removal-failed") ?? 0) + 1);
|
|
1082
1238
|
}
|
|
1083
1239
|
}
|
|
1240
|
+
if (retainedByReason.size > 0) {
|
|
1241
|
+
const details = [...retainedByReason.entries()]
|
|
1242
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1243
|
+
.map(([reason, count]) => `${reason}: ${count}`)
|
|
1244
|
+
.join(", ");
|
|
1245
|
+
const retainedCount = [...retainedByReason.values()].reduce((total, count) => total + count, 0);
|
|
1246
|
+
logger.warn(`sdk broker: retained ${retainedCount} stale lock artifact(s) (${details})`);
|
|
1247
|
+
}
|
|
1084
1248
|
if (removed.length > 0) logger.info(`sdk broker: reaped ${removed.length} stale lock artifact(s)`);
|
|
1085
1249
|
return { removed, retained };
|
|
1086
1250
|
}
|
|
@@ -1283,6 +1447,7 @@ const publicationObservationOverridesForTest = new WeakMap<Broker, BrokerPublica
|
|
|
1283
1447
|
const lockArtifactGraceOverridesForTest = new WeakMap<Broker, number>();
|
|
1284
1448
|
const livenessGraceOverridesForTest = new WeakMap<Broker, number>();
|
|
1285
1449
|
const heartbeatStallOverridesForTest = new WeakMap<Broker, PromiseWithResolvers<void>>();
|
|
1450
|
+
const managedCloseWaitOverridesForTest = new WeakMap<Broker, number>();
|
|
1286
1451
|
|
|
1287
1452
|
export class Broker {
|
|
1288
1453
|
readonly settings: ResolvedBrokerSettings;
|
|
@@ -1295,6 +1460,9 @@ export class Broker {
|
|
|
1295
1460
|
#chains = new Map<string, Promise<void>>();
|
|
1296
1461
|
#spawnInFlight = new Map<string, SpawnInFlight>();
|
|
1297
1462
|
#spawnTasks = new WeakMap<SpawnInFlight, string>();
|
|
1463
|
+
#managedAttempts = new Map<string, ManagedAttemptRef>();
|
|
1464
|
+
#managedFailedRoots = new Set<string>();
|
|
1465
|
+
#managedEnrollmentFailed = false;
|
|
1298
1466
|
#spawnAuthority: SpawnAuthorityStore | null = null;
|
|
1299
1467
|
#spawnPromptLayer: SpawnPromptLayer;
|
|
1300
1468
|
#spawnReapInFlight = false;
|
|
@@ -1369,11 +1537,17 @@ export class Broker {
|
|
|
1369
1537
|
async #handleSpawn(input: Record<string, unknown>, idempotencyKey: string | undefined): Promise<BrokerResponse> {
|
|
1370
1538
|
const admission = parseSpawnInput(input, idempotencyKey);
|
|
1371
1539
|
if (isBrokerResponse(admission)) return admission;
|
|
1372
|
-
|
|
1540
|
+
// session.spawn predates target-bound lifecycle identities and its durable
|
|
1541
|
+
// authority store already binds the raw request. Keep the v3 identity for
|
|
1542
|
+
// new claims, while recognizing the v4 identity written by the short-lived
|
|
1543
|
+
// target-bound implementation so an upgrade cannot admit a second child.
|
|
1544
|
+
const targetBoundIdentity = await deriveIdempotencyIdentity(
|
|
1373
1545
|
this.settings.agentDir,
|
|
1374
1546
|
"session.spawn",
|
|
1375
1547
|
idempotencyKey!,
|
|
1376
1548
|
);
|
|
1549
|
+
const legacyIdentity = await deriveLegacyIdentity(this.settings.agentDir, "session.spawn", idempotencyKey!);
|
|
1550
|
+
const lifecycleIdentity = this.#spawnAuthority?.claim(targetBoundIdentity) ? targetBoundIdentity : legacyIdentity;
|
|
1377
1551
|
const active = this.#spawnInFlight.get(lifecycleIdentity);
|
|
1378
1552
|
if (active) {
|
|
1379
1553
|
if (this.#spawnTasks.get(active) !== admission.task)
|
|
@@ -1404,119 +1578,12 @@ export class Broker {
|
|
|
1404
1578
|
admission.masterCapability = "";
|
|
1405
1579
|
}
|
|
1406
1580
|
if (!verified.allowed) return finish(error("spawn_failed", "master capability verification was denied"));
|
|
1407
|
-
const
|
|
1408
|
-
|
|
1409
|
-
if (!authority) return finish(error("unavailable", "spawn authority is unavailable"));
|
|
1410
|
-
const requestBindingMac = spawnBindingMac(
|
|
1411
|
-
key,
|
|
1412
|
-
admission,
|
|
1413
|
-
admission.modelId ?? null,
|
|
1414
|
-
admission.modelPreset ?? null,
|
|
1415
|
-
);
|
|
1416
|
-
const existing = authority.claim(lifecycleIdentity);
|
|
1417
|
-
const resolveModels = async (): Promise<BrokerResponse | undefined> => {
|
|
1418
|
-
// Validate the optional profile BEFORE any new durable claim or substrate
|
|
1419
|
-
// effect. Generic lifecycle create validates it; spawn must do the same.
|
|
1420
|
-
if (admission.modelPreset !== undefined) {
|
|
1421
|
-
const validated = validateBrokerModelPresetSync(this.settings.agentDir, admission.modelPreset);
|
|
1422
|
-
if (isBrokerResponse(validated)) return validated;
|
|
1423
|
-
admission.modelPreset = validated;
|
|
1424
|
-
}
|
|
1425
|
-
// Resolve the explicit model pin before a new durable claim. Raw
|
|
1426
|
-
// selectors must never reach a newly launched child.
|
|
1427
|
-
if (admission.modelId !== undefined) {
|
|
1428
|
-
const resolved = await this.#resolveModelPin(admission.modelId, { cwd: admission.cwd });
|
|
1429
|
-
if (!resolved.ok) return error("unknown_model", resolved.error);
|
|
1430
|
-
if (resolved.model === null)
|
|
1431
|
-
return error("unknown_model", "session.spawn modelId could not be resolved.");
|
|
1432
|
-
admission.modelId = resolved.model;
|
|
1433
|
-
}
|
|
1434
|
-
return undefined;
|
|
1435
|
-
};
|
|
1436
|
-
let decision: SpawnClaimDecision;
|
|
1437
|
-
if (existing !== undefined) {
|
|
1438
|
-
// An existing claim carries the raw-selector binding, so terminal and
|
|
1439
|
-
// replay responses do not depend on today's model/profile catalog.
|
|
1440
|
-
let existingBindingMac: string | undefined;
|
|
1441
|
-
if (existing.requestBindingMac === undefined) {
|
|
1442
|
-
// Claims written before raw-selector evidence was introduced only have
|
|
1443
|
-
// the canonical binding. Re-resolve that historical selector before
|
|
1444
|
-
// comparison; if the catalog no longer proves it, fail closed rather
|
|
1445
|
-
// than guessing or weakening idempotency conflict protection.
|
|
1446
|
-
const modelError = await resolveModels();
|
|
1447
|
-
if (modelError) return finish(modelError);
|
|
1448
|
-
existingBindingMac = spawnBindingMac(
|
|
1449
|
-
key,
|
|
1450
|
-
admission,
|
|
1451
|
-
admission.modelId ?? null,
|
|
1452
|
-
admission.modelPreset ?? null,
|
|
1453
|
-
);
|
|
1454
|
-
}
|
|
1455
|
-
decision = await authority.claimOrJoin(
|
|
1456
|
-
lifecycleIdentity,
|
|
1457
|
-
existingBindingMac,
|
|
1458
|
-
existingBindingMac ?? requestBindingMac,
|
|
1459
|
-
);
|
|
1460
|
-
if (decision.kind === "owner") {
|
|
1461
|
-
becameOwner = true;
|
|
1462
|
-
// A recovered pre-send claim may still need to launch. Preserve the
|
|
1463
|
-
// fail-closed model validation for that effectful path and reject a
|
|
1464
|
-
// catalog remap rather than launching a different model.
|
|
1465
|
-
const modelError = await resolveModels();
|
|
1466
|
-
if (modelError) return finish(modelError);
|
|
1467
|
-
const recoveredBindingMac = spawnBindingMac(
|
|
1468
|
-
key,
|
|
1469
|
-
admission,
|
|
1470
|
-
admission.modelId ?? null,
|
|
1471
|
-
admission.modelPreset ?? null,
|
|
1472
|
-
);
|
|
1473
|
-
if (recoveredBindingMac !== decision.claim.bindingMac)
|
|
1474
|
-
return finish(
|
|
1475
|
-
error("idempotency_conflict", "session.spawn model selection differs from its durable claim"),
|
|
1476
|
-
);
|
|
1477
|
-
}
|
|
1478
|
-
} else {
|
|
1479
|
-
const modelError = await resolveModels();
|
|
1480
|
-
if (modelError) {
|
|
1481
|
-
// A concurrent claimant may have durably established this exact raw
|
|
1482
|
-
// request while catalog loading was in flight. Re-check its opaque
|
|
1483
|
-
// binding before returning the validation failure.
|
|
1484
|
-
const raced = authority.claim(lifecycleIdentity);
|
|
1485
|
-
if (raced === undefined) return finish(modelError);
|
|
1486
|
-
decision = await authority.claimOrJoin(lifecycleIdentity, undefined, requestBindingMac);
|
|
1487
|
-
if (decision.kind === "owner") {
|
|
1488
|
-
becameOwner = true;
|
|
1489
|
-
return finish(modelError);
|
|
1490
|
-
}
|
|
1491
|
-
} else {
|
|
1492
|
-
const bindingMac = spawnBindingMac(
|
|
1493
|
-
key,
|
|
1494
|
-
admission,
|
|
1495
|
-
admission.modelId ?? null,
|
|
1496
|
-
admission.modelPreset ?? null,
|
|
1497
|
-
);
|
|
1498
|
-
decision = await authority.claimOrJoin(lifecycleIdentity, bindingMac, requestBindingMac);
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
if (decision.kind === "idempotency_conflict")
|
|
1502
|
-
return finish(
|
|
1503
|
-
error("idempotency_conflict", "idempotency key conflicts with an existing session.spawn claim"),
|
|
1504
|
-
);
|
|
1505
|
-
if (decision.kind === "in_progress")
|
|
1506
|
-
return finish(error("spawn_in_progress", `session.spawn is ${decision.claim.state}`));
|
|
1507
|
-
if (decision.kind === "terminal_uncertain")
|
|
1508
|
-
return finish(error("terminal_uncertain", "session.spawn outcome is uncertain"));
|
|
1509
|
-
if (decision.kind === "terminal") return finish(this.#spawnTerminalResponse(decision.claim));
|
|
1510
|
-
if (decision.kind === "replay")
|
|
1511
|
-
return finish(await this.#reconcileSpawnReplay(lifecycleIdentity, decision.claim));
|
|
1512
|
-
becameOwner = true;
|
|
1513
|
-
inFlight.claimId = decision.claim.claimId;
|
|
1514
|
-
inFlight.phase = decision.claim.state;
|
|
1515
|
-
// Claim fsync precedes this audit mirror. Substrate work may start
|
|
1516
|
-
// only after the mirror succeeds; a startup reconciler rebuilds it claim-first.
|
|
1517
|
-
await this.ledger.begin(lifecycleIdentity, decision.claim.bindingMac);
|
|
1581
|
+
const managedDenied = await this.#denyOrdinaryManagedSpawn(lifecycleIdentity, idempotencyKey!);
|
|
1582
|
+
if (managedDenied) return finish(managedDenied);
|
|
1518
1583
|
return finish(
|
|
1519
|
-
await this.#
|
|
1584
|
+
await this.#driveVerifiedSpawn(lifecycleIdentity, admission, inFlight, owner => {
|
|
1585
|
+
becameOwner = owner;
|
|
1586
|
+
}),
|
|
1520
1587
|
);
|
|
1521
1588
|
} catch {
|
|
1522
1589
|
return finish(error("spawn_failed", "session.spawn admission could not be durably established"));
|
|
@@ -1527,6 +1594,641 @@ export class Broker {
|
|
|
1527
1594
|
if (becameOwner) await this.#spawnAuthority?.releaseOwner(lifecycleIdentity);
|
|
1528
1595
|
}
|
|
1529
1596
|
}
|
|
1597
|
+
async #denyOrdinaryManagedSpawn(lifecycleIdentity: string, callerKey: string): Promise<BrokerResponse | undefined> {
|
|
1598
|
+
if (this.#managedAttempts.has(lifecycleIdentity))
|
|
1599
|
+
return error("spawn_failed", "managed native identity cannot re-enter ordinary session.spawn");
|
|
1600
|
+
if (this.#managedEnrollmentFailed) return error("spawn_failed", "managed enrollment membership is unprovable");
|
|
1601
|
+
let enrolled: ManagedEnrollmentRecord;
|
|
1602
|
+
try {
|
|
1603
|
+
enrolled = await loadManagedEnrollmentRecord(this.settings.agentDir);
|
|
1604
|
+
} catch {
|
|
1605
|
+
return error("spawn_failed", "managed enrollment membership is unprovable");
|
|
1606
|
+
}
|
|
1607
|
+
if (enrolled.nativeIdentities.includes(lifecycleIdentity))
|
|
1608
|
+
return error("spawn_failed", "managed native identity cannot re-enter ordinary session.spawn");
|
|
1609
|
+
for (const root of enrolled.controlRoots) {
|
|
1610
|
+
if (this.#managedFailedRoots.has(root)) continue;
|
|
1611
|
+
try {
|
|
1612
|
+
const scopedIdentity = await deriveScopedIdempotencyIdentity(
|
|
1613
|
+
this.settings.agentDir,
|
|
1614
|
+
"session.spawn",
|
|
1615
|
+
callerKey,
|
|
1616
|
+
root,
|
|
1617
|
+
);
|
|
1618
|
+
if (this.#managedAttempts.has(scopedIdentity) || enrolled.nativeIdentities.includes(scopedIdentity))
|
|
1619
|
+
return error("spawn_failed", "managed native identity cannot re-enter ordinary session.spawn");
|
|
1620
|
+
const read = await readExistingStateForMutation(managedTaskDomainPath(root));
|
|
1621
|
+
if (read.kind !== "valid") continue;
|
|
1622
|
+
const state = validateManagedTaskDomain(read.value);
|
|
1623
|
+
const attempt =
|
|
1624
|
+
lookupManagedAttemptByNativeIdentity(state, lifecycleIdentity) ??
|
|
1625
|
+
lookupManagedAttemptByNativeIdentity(state, scopedIdentity) ??
|
|
1626
|
+
state.graphs.flatMap(graph => graph.attempts).find(item => item.native.key === callerKey);
|
|
1627
|
+
if (attempt) return error("spawn_failed", "managed native identity cannot re-enter ordinary session.spawn");
|
|
1628
|
+
} catch {}
|
|
1629
|
+
}
|
|
1630
|
+
return undefined;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
async #driveVerifiedSpawn(
|
|
1634
|
+
lifecycleIdentity: string,
|
|
1635
|
+
admission: SpawnAdmissionInput,
|
|
1636
|
+
inFlight: SpawnInFlight,
|
|
1637
|
+
setOwner: (owned: boolean) => void,
|
|
1638
|
+
): Promise<BrokerResponse> {
|
|
1639
|
+
const key = await getBrokerIdentityKey(this.settings.agentDir);
|
|
1640
|
+
const authority = this.#spawnAuthority;
|
|
1641
|
+
if (!authority) return error("unavailable", "spawn authority is unavailable");
|
|
1642
|
+
const requestBindingMac = spawnBindingMac(
|
|
1643
|
+
key,
|
|
1644
|
+
admission,
|
|
1645
|
+
admission.modelId ?? null,
|
|
1646
|
+
admission.modelPreset ?? null,
|
|
1647
|
+
);
|
|
1648
|
+
const existing = authority.claim(lifecycleIdentity);
|
|
1649
|
+
const resolveModels = async (): Promise<BrokerResponse | undefined> => {
|
|
1650
|
+
if (admission.modelPreset !== undefined) {
|
|
1651
|
+
const validated = validateBrokerModelPresetSync(this.settings.agentDir, admission.modelPreset);
|
|
1652
|
+
if (isBrokerResponse(validated)) return validated;
|
|
1653
|
+
admission.modelPreset = validated;
|
|
1654
|
+
}
|
|
1655
|
+
if (admission.modelId !== undefined) {
|
|
1656
|
+
const resolved = await this.#resolveModelPin(admission.modelId, { cwd: admission.cwd });
|
|
1657
|
+
if (!resolved.ok) return error("unknown_model", resolved.error);
|
|
1658
|
+
if (resolved.model === null) return error("unknown_model", "session.spawn modelId could not be resolved.");
|
|
1659
|
+
admission.modelId = resolved.model;
|
|
1660
|
+
}
|
|
1661
|
+
return undefined;
|
|
1662
|
+
};
|
|
1663
|
+
let decision: SpawnClaimDecision | undefined;
|
|
1664
|
+
if (existing !== undefined) {
|
|
1665
|
+
let existingBindingMac: string | undefined;
|
|
1666
|
+
if (existing.requestBindingMac === undefined) {
|
|
1667
|
+
const modelError = await resolveModels();
|
|
1668
|
+
if (modelError) return modelError;
|
|
1669
|
+
existingBindingMac = spawnBindingMac(
|
|
1670
|
+
key,
|
|
1671
|
+
admission,
|
|
1672
|
+
admission.modelId ?? null,
|
|
1673
|
+
admission.modelPreset ?? null,
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
decision = await authority.claimOrJoin(
|
|
1677
|
+
lifecycleIdentity,
|
|
1678
|
+
existingBindingMac,
|
|
1679
|
+
existingBindingMac ?? requestBindingMac,
|
|
1680
|
+
);
|
|
1681
|
+
if (decision.kind === "owner") {
|
|
1682
|
+
setOwner(true);
|
|
1683
|
+
const modelError = await resolveModels();
|
|
1684
|
+
if (modelError) return modelError;
|
|
1685
|
+
const recoveredBindingMac = spawnBindingMac(
|
|
1686
|
+
key,
|
|
1687
|
+
admission,
|
|
1688
|
+
admission.modelId ?? null,
|
|
1689
|
+
admission.modelPreset ?? null,
|
|
1690
|
+
);
|
|
1691
|
+
if (recoveredBindingMac !== decision.claim.bindingMac)
|
|
1692
|
+
return error("idempotency_conflict", "session.spawn model selection differs from its durable claim");
|
|
1693
|
+
}
|
|
1694
|
+
} else {
|
|
1695
|
+
const modelError = await resolveModels();
|
|
1696
|
+
if (modelError) {
|
|
1697
|
+
const raced = authority.claim(lifecycleIdentity);
|
|
1698
|
+
if (raced === undefined) return modelError;
|
|
1699
|
+
decision = await authority.claimOrJoin(lifecycleIdentity, undefined, requestBindingMac);
|
|
1700
|
+
if (decision.kind === "owner") {
|
|
1701
|
+
setOwner(true);
|
|
1702
|
+
return modelError;
|
|
1703
|
+
}
|
|
1704
|
+
} else {
|
|
1705
|
+
const bindingMac = spawnBindingMac(
|
|
1706
|
+
key,
|
|
1707
|
+
admission,
|
|
1708
|
+
admission.modelId ?? null,
|
|
1709
|
+
admission.modelPreset ?? null,
|
|
1710
|
+
);
|
|
1711
|
+
decision = await authority.claimOrJoin(lifecycleIdentity, bindingMac, requestBindingMac);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
if (!decision) return error("spawn_failed", "session.spawn admission could not be durably established");
|
|
1715
|
+
if (decision.kind === "managed_key_conflict")
|
|
1716
|
+
return error("idempotency_conflict", "idempotency key is reserved by a managed task.dag attempt");
|
|
1717
|
+
if (decision.kind === "idempotency_conflict")
|
|
1718
|
+
return error("idempotency_conflict", "idempotency key conflicts with an existing session.spawn claim");
|
|
1719
|
+
if (decision.kind === "in_progress")
|
|
1720
|
+
return error("spawn_in_progress", `session.spawn is ${decision.claim.state}`);
|
|
1721
|
+
if (decision.kind === "terminal_uncertain") {
|
|
1722
|
+
if (decision.claim.substrateProof !== undefined && decision.claim.authorityRef === undefined)
|
|
1723
|
+
return await this.#reconcileUncertainRegistration(lifecycleIdentity, decision.claim);
|
|
1724
|
+
return error("terminal_uncertain", "session.spawn outcome is uncertain");
|
|
1725
|
+
}
|
|
1726
|
+
if (decision.kind === "terminal") return this.#spawnTerminalResponse(decision.claim);
|
|
1727
|
+
if (decision.kind === "replay") return await this.#reconcileSpawnReplay(lifecycleIdentity, decision.claim);
|
|
1728
|
+
setOwner(true);
|
|
1729
|
+
inFlight.claimId = decision.claim.claimId;
|
|
1730
|
+
inFlight.phase = decision.claim.state;
|
|
1731
|
+
await this.ledger.begin(lifecycleIdentity, decision.claim.bindingMac);
|
|
1732
|
+
return await this.#driveSpawn(lifecycleIdentity, decision.claim, admission, inFlight, decision.recovery);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
async #handleManagedTaskDag(
|
|
1736
|
+
input: Record<string, unknown>,
|
|
1737
|
+
idempotencyKey: string | undefined,
|
|
1738
|
+
): Promise<BrokerResponse> {
|
|
1739
|
+
if (input.action === "define") return this.#managedDefine(input);
|
|
1740
|
+
if (input.action === "advance") return this.#managedAdvance(input, idempotencyKey);
|
|
1741
|
+
if (input.action === "status") return this.#managedStatus(input);
|
|
1742
|
+
if (input.action === "revise") return this.#managedRevise(input);
|
|
1743
|
+
if (input.action === "cancel") return this.#managedCancel(input);
|
|
1744
|
+
if (input.action === "verify") return this.#managedVerify(input);
|
|
1745
|
+
return error("invalid_input", "unknown task.dag action");
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
async #verifiedManagedOwner(input: Record<string, unknown>): Promise<VerifiedManagedOwner> {
|
|
1749
|
+
const ownerSessionId = input.ownerSessionId;
|
|
1750
|
+
const attestationEpoch = input.attestationEpoch;
|
|
1751
|
+
const masterCapability = input.masterCapability;
|
|
1752
|
+
const controlRoot = input.controlRoot;
|
|
1753
|
+
const enrollmentId = input.enrollmentId;
|
|
1754
|
+
if (
|
|
1755
|
+
typeof ownerSessionId !== "string" ||
|
|
1756
|
+
!isCanonicalSessionId(ownerSessionId) ||
|
|
1757
|
+
typeof attestationEpoch !== "string" ||
|
|
1758
|
+
attestationEpoch.length === 0 ||
|
|
1759
|
+
attestationEpoch.length > 512 ||
|
|
1760
|
+
typeof masterCapability !== "string" ||
|
|
1761
|
+
masterCapability.length === 0 ||
|
|
1762
|
+
masterCapability.length > 16_384 ||
|
|
1763
|
+
typeof controlRoot !== "string" ||
|
|
1764
|
+
!path.isAbsolute(controlRoot) ||
|
|
1765
|
+
path.normalize(controlRoot) !== controlRoot ||
|
|
1766
|
+
typeof enrollmentId !== "string" ||
|
|
1767
|
+
enrollmentId.trim().length === 0 ||
|
|
1768
|
+
enrollmentId.length > 256
|
|
1769
|
+
)
|
|
1770
|
+
return { kind: "denied", response: error("invalid_input", "task.dag authorization is invalid") };
|
|
1771
|
+
if ("accepted" in input || "pass" in input || "receipt" in input)
|
|
1772
|
+
return { kind: "denied", response: error("invalid_input", "public verification material is not authority") };
|
|
1773
|
+
if (input.worktrees !== undefined) {
|
|
1774
|
+
if (!Array.isArray(input.worktrees) || input.worktrees.length === 0)
|
|
1775
|
+
return { kind: "denied", response: error("invalid_input", "worktrees are invalid") };
|
|
1776
|
+
if (
|
|
1777
|
+
input.worktrees.some(
|
|
1778
|
+
value => typeof value !== "string" || !path.isAbsolute(value) || path.normalize(value) !== value,
|
|
1779
|
+
)
|
|
1780
|
+
)
|
|
1781
|
+
return { kind: "denied", response: error("invalid_input", "worktrees are invalid") };
|
|
1782
|
+
}
|
|
1783
|
+
if (input.aliases !== undefined) {
|
|
1784
|
+
if (!Array.isArray(input.aliases))
|
|
1785
|
+
return { kind: "denied", response: error("invalid_input", "aliases are invalid") };
|
|
1786
|
+
if (
|
|
1787
|
+
input.aliases.some(
|
|
1788
|
+
value =>
|
|
1789
|
+
typeof value !== "string" || value.length === 0 || path.normalize(value) !== path.resolve(value),
|
|
1790
|
+
)
|
|
1791
|
+
)
|
|
1792
|
+
return { kind: "denied", response: error("invalid_input", "aliases are invalid") };
|
|
1793
|
+
}
|
|
1794
|
+
let allowed = false;
|
|
1795
|
+
try {
|
|
1796
|
+
allowed = (await this.verifyMasterCapability(ownerSessionId, masterCapability, attestationEpoch)).allowed;
|
|
1797
|
+
} finally {
|
|
1798
|
+
if (typeof input.masterCapability === "string") input.masterCapability = "";
|
|
1799
|
+
}
|
|
1800
|
+
if (!allowed)
|
|
1801
|
+
return { kind: "denied", response: error("spawn_failed", "master capability verification was denied") };
|
|
1802
|
+
const worktrees = Array.isArray(input.worktrees)
|
|
1803
|
+
? input.worktrees.filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
1804
|
+
: [controlRoot];
|
|
1805
|
+
const aliases = Array.isArray(input.aliases)
|
|
1806
|
+
? input.aliases.filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
1807
|
+
: undefined;
|
|
1808
|
+
return {
|
|
1809
|
+
kind: "verified",
|
|
1810
|
+
ownerSessionId,
|
|
1811
|
+
attestationEpoch,
|
|
1812
|
+
controlRoot,
|
|
1813
|
+
enrollmentId,
|
|
1814
|
+
worktrees: worktrees.length > 0 ? worktrees : [controlRoot],
|
|
1815
|
+
...(aliases && aliases.length > 0 ? { aliases } : {}),
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
#managedPublic(
|
|
1820
|
+
stateRevision: number,
|
|
1821
|
+
graphs: Array<{ id: string; revision: number; attempts: ManagedTaskAttempt[] }>,
|
|
1822
|
+
): Record<string, unknown> {
|
|
1823
|
+
return {
|
|
1824
|
+
stateRevision,
|
|
1825
|
+
graphs: graphs.map(graph => ({
|
|
1826
|
+
id: graph.id,
|
|
1827
|
+
revision: graph.revision,
|
|
1828
|
+
attempts: graph.attempts.length,
|
|
1829
|
+
nativeRefs: graph.attempts.map(attempt => attempt.native.identity),
|
|
1830
|
+
fences: graph.attempts.map(attempt => attempt.fence),
|
|
1831
|
+
validations: graph.attempts.map(attempt => attempt.validation),
|
|
1832
|
+
accepted: graph.attempts.map(attempt => attempt.accepted !== null),
|
|
1833
|
+
})),
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
async #managedDefine(input: Record<string, unknown>): Promise<BrokerResponse> {
|
|
1838
|
+
if (
|
|
1839
|
+
typeof input.expectedRevision !== "number" ||
|
|
1840
|
+
!Number.isSafeInteger(input.expectedRevision) ||
|
|
1841
|
+
input.expectedRevision < 0
|
|
1842
|
+
)
|
|
1843
|
+
return error("invalid_input", "expectedRevision is required");
|
|
1844
|
+
if (typeof input.graphId !== "string" || input.graphId.trim().length === 0)
|
|
1845
|
+
return error("invalid_input", "graphId is required");
|
|
1846
|
+
const auth = await this.#verifiedManagedOwner(input);
|
|
1847
|
+
if (auth.kind === "denied") return auth.response;
|
|
1848
|
+
try {
|
|
1849
|
+
const binding = await createManagedDomainBinding({
|
|
1850
|
+
controlRoot: auth.controlRoot,
|
|
1851
|
+
agentDir: this.settings.agentDir,
|
|
1852
|
+
enrollmentId: auth.enrollmentId,
|
|
1853
|
+
worktrees: auth.worktrees,
|
|
1854
|
+
...(auth.aliases ? { aliases: auth.aliases } : {}),
|
|
1855
|
+
});
|
|
1856
|
+
let rootWasEstablished = false;
|
|
1857
|
+
try {
|
|
1858
|
+
rootWasEstablished = (await loadManagedEnrollmentRecord(this.settings.agentDir)).establishedRoots.includes(
|
|
1859
|
+
binding.controlRoot,
|
|
1860
|
+
);
|
|
1861
|
+
} catch {
|
|
1862
|
+
throw new Error("native managed evidence exists");
|
|
1863
|
+
}
|
|
1864
|
+
// Publish the enrollment before the first domain snapshot. The index is
|
|
1865
|
+
// the recovery root list, so this ordering leaves only a recoverable
|
|
1866
|
+
// stale index entry if the process dies before the state publication.
|
|
1867
|
+
await recordManagedEnrollment(this.settings.agentDir, binding.controlRoot);
|
|
1868
|
+
const result = await transactManagedTaskDomain(
|
|
1869
|
+
{
|
|
1870
|
+
binding,
|
|
1871
|
+
expectedRevision: input.expectedRevision,
|
|
1872
|
+
...(input.expectedRevision === 0
|
|
1873
|
+
? {
|
|
1874
|
+
assertNoManagedEvidence: async () => {
|
|
1875
|
+
let enrolled: ManagedEnrollmentRecord;
|
|
1876
|
+
try {
|
|
1877
|
+
enrolled = await loadManagedEnrollmentRecord(this.settings.agentDir);
|
|
1878
|
+
} catch {
|
|
1879
|
+
throw new Error("native managed evidence exists");
|
|
1880
|
+
}
|
|
1881
|
+
if (
|
|
1882
|
+
rootWasEstablished ||
|
|
1883
|
+
enrolled.establishedRoots.includes(binding.controlRoot) ||
|
|
1884
|
+
enrolled.publishingRoots.includes(binding.controlRoot) ||
|
|
1885
|
+
(enrolled.byRoot[binding.controlRoot] ?? []).length > 0
|
|
1886
|
+
)
|
|
1887
|
+
throw new Error("native managed evidence exists");
|
|
1888
|
+
await recordManagedEnrollment(this.settings.agentDir, binding.controlRoot);
|
|
1889
|
+
},
|
|
1890
|
+
beforeFirstPublication: async () => {
|
|
1891
|
+
await markManagedEnrollmentPublishing(this.settings.agentDir, binding.controlRoot);
|
|
1892
|
+
},
|
|
1893
|
+
onFirstPublication: async () => {
|
|
1894
|
+
await markManagedEnrollmentEstablished(this.settings.agentDir, binding.controlRoot);
|
|
1895
|
+
},
|
|
1896
|
+
onFirstPublicationAborted: async () => {
|
|
1897
|
+
await markManagedEnrollmentPending(this.settings.agentDir, binding.controlRoot);
|
|
1898
|
+
},
|
|
1899
|
+
}
|
|
1900
|
+
: {}),
|
|
1901
|
+
},
|
|
1902
|
+
async state =>
|
|
1903
|
+
defineManagedTaskGraph(state, {
|
|
1904
|
+
id: input.graphId as string,
|
|
1905
|
+
owner: auth.ownerSessionId,
|
|
1906
|
+
nodes: input.nodes,
|
|
1907
|
+
}),
|
|
1908
|
+
);
|
|
1909
|
+
await markManagedEnrollmentEstablished(this.settings.agentDir, binding.controlRoot);
|
|
1910
|
+
return {
|
|
1911
|
+
ok: true,
|
|
1912
|
+
result: {
|
|
1913
|
+
...this.#managedPublic(result.state.state_revision, result.state.graphs),
|
|
1914
|
+
graphId: result.result.id,
|
|
1915
|
+
},
|
|
1916
|
+
};
|
|
1917
|
+
} catch (caught) {
|
|
1918
|
+
return error("spawn_failed", caught instanceof Error ? caught.message : "managed define failed");
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
async #managedAdvance(input: Record<string, unknown>, idempotencyKey: string | undefined): Promise<BrokerResponse> {
|
|
1923
|
+
if (!idempotencyKey || idempotencyKey.length > 512)
|
|
1924
|
+
return error("invalid_input", "idempotencyKey is required for task.dag advance");
|
|
1925
|
+
if (
|
|
1926
|
+
typeof input.expectedRevision !== "number" ||
|
|
1927
|
+
!Number.isSafeInteger(input.expectedRevision) ||
|
|
1928
|
+
input.expectedRevision < 0
|
|
1929
|
+
)
|
|
1930
|
+
return error("invalid_input", "expectedRevision is required");
|
|
1931
|
+
if (typeof input.graphId !== "string" || typeof input.nodeId !== "string")
|
|
1932
|
+
return error("invalid_input", "graphId and nodeId are required");
|
|
1933
|
+
const maxAdmissions = input.maxAdmissions === undefined ? 1 : input.maxAdmissions;
|
|
1934
|
+
if (typeof maxAdmissions !== "number" || !Number.isSafeInteger(maxAdmissions) || maxAdmissions < 1)
|
|
1935
|
+
return error("invalid_input", "maxAdmissions is invalid");
|
|
1936
|
+
const auth = await this.#verifiedManagedOwner(input);
|
|
1937
|
+
if (auth.kind === "denied") return auth.response;
|
|
1938
|
+
try {
|
|
1939
|
+
const binding = await createManagedDomainBinding({
|
|
1940
|
+
controlRoot: auth.controlRoot,
|
|
1941
|
+
agentDir: this.settings.agentDir,
|
|
1942
|
+
enrollmentId: auth.enrollmentId,
|
|
1943
|
+
worktrees: auth.worktrees,
|
|
1944
|
+
...(auth.aliases ? { aliases: auth.aliases } : {}),
|
|
1945
|
+
});
|
|
1946
|
+
const scopedIdentity = await deriveScopedIdempotencyIdentity(
|
|
1947
|
+
this.settings.agentDir,
|
|
1948
|
+
"session.spawn",
|
|
1949
|
+
idempotencyKey,
|
|
1950
|
+
binding.controlRoot,
|
|
1951
|
+
);
|
|
1952
|
+
const ordinaryAliases = await Promise.all([
|
|
1953
|
+
deriveIdempotencyIdentity(this.settings.agentDir, "session.spawn", idempotencyKey),
|
|
1954
|
+
deriveLegacyIdentity(this.settings.agentDir, "session.spawn", idempotencyKey),
|
|
1955
|
+
]);
|
|
1956
|
+
const authority = this.#spawnAuthority;
|
|
1957
|
+
if (!authority) return error("unavailable", "spawn authority is unavailable");
|
|
1958
|
+
const requestHash = managedIdentity({
|
|
1959
|
+
graphId: input.graphId,
|
|
1960
|
+
nodeId: input.nodeId,
|
|
1961
|
+
ownerSessionId: auth.ownerSessionId,
|
|
1962
|
+
controlRoot: binding.controlRoot,
|
|
1963
|
+
});
|
|
1964
|
+
const active = this.#spawnInFlight.get(scopedIdentity);
|
|
1965
|
+
if (active) {
|
|
1966
|
+
if (active.managedRequestHash !== requestHash)
|
|
1967
|
+
return error("idempotency_conflict", "idempotency key conflicts with a live managed task.dag advance");
|
|
1968
|
+
return await active.completion;
|
|
1969
|
+
}
|
|
1970
|
+
const reserved = await transactManagedTaskDomain(
|
|
1971
|
+
{ binding, expectedRevision: input.expectedRevision },
|
|
1972
|
+
async state => {
|
|
1973
|
+
const graph = state.graphs.find(item => item.id === input.graphId);
|
|
1974
|
+
if (!graph) throw new Error("graph owner mismatch");
|
|
1975
|
+
const node = graph.nodes.find(item => item.definition.id === input.nodeId);
|
|
1976
|
+
if (!node) throw new Error("node not ready");
|
|
1977
|
+
const workspace = path.resolve(node.definition.workspace);
|
|
1978
|
+
if (typeof input.cwd === "string" && input.cwd.length > 0 && path.resolve(input.cwd) !== workspace)
|
|
1979
|
+
throw new Error("cwd must match admitted workspace");
|
|
1980
|
+
const prior = state.graphs
|
|
1981
|
+
.flatMap(item => item.attempts)
|
|
1982
|
+
.find(
|
|
1983
|
+
attempt =>
|
|
1984
|
+
!attempt.retired &&
|
|
1985
|
+
attempt.native.key === idempotencyKey &&
|
|
1986
|
+
attempt.native.requestHash === requestHash &&
|
|
1987
|
+
attempt.nodeId === input.nodeId &&
|
|
1988
|
+
attempt.id === `attempt-${idempotencyKey}`,
|
|
1989
|
+
);
|
|
1990
|
+
const admitted = await observeOrAdmitManagedTask(state, {
|
|
1991
|
+
graphId: graph.id,
|
|
1992
|
+
owner: auth.ownerSessionId,
|
|
1993
|
+
nodeId: input.nodeId as string,
|
|
1994
|
+
attemptId: `attempt-${idempotencyKey}`,
|
|
1995
|
+
native: {
|
|
1996
|
+
key: idempotencyKey,
|
|
1997
|
+
identity: prior?.native.identity ?? scopedIdentity,
|
|
1998
|
+
requestHash,
|
|
1999
|
+
},
|
|
2000
|
+
});
|
|
2001
|
+
if (
|
|
2002
|
+
!(await authority.reserveManagedKeyAliases(
|
|
2003
|
+
ordinaryAliases,
|
|
2004
|
+
admitted.nativeIdentity,
|
|
2005
|
+
binding.controlRoot,
|
|
2006
|
+
))
|
|
2007
|
+
)
|
|
2008
|
+
throw new Error("idempotency key conflicts with an existing ordinary session.spawn claim");
|
|
2009
|
+
return admitted;
|
|
2010
|
+
},
|
|
2011
|
+
);
|
|
2012
|
+
const attemptRef = reserved.result;
|
|
2013
|
+
if (
|
|
2014
|
+
!managedAttemptRefMatches(
|
|
2015
|
+
attemptRef,
|
|
2016
|
+
reserved.state.graphs.flatMap(g => g.attempts).find(a => a.id === attemptRef.attemptId)!,
|
|
2017
|
+
binding,
|
|
2018
|
+
)
|
|
2019
|
+
)
|
|
2020
|
+
return error("spawn_failed", "managed attempt ref mismatch");
|
|
2021
|
+
const activeAfterAdmission = this.#spawnInFlight.get(attemptRef.nativeIdentity);
|
|
2022
|
+
if (activeAfterAdmission) {
|
|
2023
|
+
if (activeAfterAdmission.managedRequestHash !== requestHash)
|
|
2024
|
+
return error("idempotency_conflict", "idempotency key conflicts with a live managed task.dag advance");
|
|
2025
|
+
return await activeAfterAdmission.completion;
|
|
2026
|
+
}
|
|
2027
|
+
const node = reserved.state.graphs
|
|
2028
|
+
.find(graph => graph.id === attemptRef.graphId)
|
|
2029
|
+
?.nodes.find(item => item.definition.id === attemptRef.nodeId);
|
|
2030
|
+
const persisted = reserved.state.graphs
|
|
2031
|
+
.flatMap(graph => graph.attempts)
|
|
2032
|
+
?.find(item => item.id === attemptRef.attemptId);
|
|
2033
|
+
if (!node || !persisted) return error("spawn_failed", "admitted node missing");
|
|
2034
|
+
this.#managedAttempts.set(attemptRef.nativeIdentity, attemptRef);
|
|
2035
|
+
const spawnAdmission: SpawnAdmissionInput = {
|
|
2036
|
+
task: node.definition.task,
|
|
2037
|
+
masterCapability: "",
|
|
2038
|
+
ownerSessionId: auth.ownerSessionId,
|
|
2039
|
+
attestationEpoch: auth.attestationEpoch,
|
|
2040
|
+
cwd: path.resolve(node.definition.workspace),
|
|
2041
|
+
managedAttempt: attemptRef,
|
|
2042
|
+
managedBinding: binding,
|
|
2043
|
+
managedNativeVector: managedNativeVector(persisted),
|
|
2044
|
+
};
|
|
2045
|
+
const completion = Promise.withResolvers<BrokerResponse>();
|
|
2046
|
+
const inFlight: SpawnInFlight = {
|
|
2047
|
+
completion: completion.promise,
|
|
2048
|
+
resolve: completion.resolve,
|
|
2049
|
+
managedRequestHash: requestHash,
|
|
2050
|
+
};
|
|
2051
|
+
this.#spawnInFlight.set(attemptRef.nativeIdentity, inFlight);
|
|
2052
|
+
this.#spawnTasks.set(inFlight, spawnAdmission.task);
|
|
2053
|
+
let becameOwner = false;
|
|
2054
|
+
let completionResult: BrokerResponse = error("spawn_failed", "managed advance did not complete");
|
|
2055
|
+
try {
|
|
2056
|
+
await recordManagedEnrollment(this.settings.agentDir, binding.controlRoot, attemptRef.nativeIdentity);
|
|
2057
|
+
const driven = await this.#driveVerifiedSpawn(
|
|
2058
|
+
attemptRef.nativeIdentity,
|
|
2059
|
+
spawnAdmission,
|
|
2060
|
+
inFlight,
|
|
2061
|
+
owner => {
|
|
2062
|
+
becameOwner = owner;
|
|
2063
|
+
},
|
|
2064
|
+
);
|
|
2065
|
+
completionResult = driven;
|
|
2066
|
+
if (!driven.ok) return driven;
|
|
2067
|
+
const liveRevision = await currentManagedRevision(binding);
|
|
2068
|
+
completionResult = {
|
|
2069
|
+
ok: true,
|
|
2070
|
+
result: {
|
|
2071
|
+
...this.#managedPublic(liveRevision, reserved.state.graphs),
|
|
2072
|
+
attemptId: attemptRef.attemptId,
|
|
2073
|
+
nativeIdentity: attemptRef.nativeIdentity,
|
|
2074
|
+
spawn: driven.result,
|
|
2075
|
+
},
|
|
2076
|
+
};
|
|
2077
|
+
return completionResult;
|
|
2078
|
+
} catch (caught) {
|
|
2079
|
+
completionResult = error(
|
|
2080
|
+
"spawn_failed",
|
|
2081
|
+
caught instanceof Error ? caught.message : "managed advance failed",
|
|
2082
|
+
);
|
|
2083
|
+
throw caught;
|
|
2084
|
+
} finally {
|
|
2085
|
+
completion.resolve(completionResult);
|
|
2086
|
+
this.#spawnTasks.delete(inFlight);
|
|
2087
|
+
if (this.#spawnInFlight.get(attemptRef.nativeIdentity) === inFlight)
|
|
2088
|
+
this.#spawnInFlight.delete(attemptRef.nativeIdentity);
|
|
2089
|
+
if (becameOwner) await this.#spawnAuthority?.releaseOwner(attemptRef.nativeIdentity);
|
|
2090
|
+
}
|
|
2091
|
+
} catch (caught) {
|
|
2092
|
+
return error("spawn_failed", caught instanceof Error ? caught.message : "managed advance failed");
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
async #managedStatus(input: Record<string, unknown>): Promise<BrokerResponse> {
|
|
2097
|
+
const auth = await this.#verifiedManagedOwner(input);
|
|
2098
|
+
if (auth.kind === "denied") return auth.response;
|
|
2099
|
+
try {
|
|
2100
|
+
const binding = await createManagedDomainBinding({
|
|
2101
|
+
controlRoot: auth.controlRoot,
|
|
2102
|
+
agentDir: this.settings.agentDir,
|
|
2103
|
+
enrollmentId: auth.enrollmentId,
|
|
2104
|
+
worktrees: auth.worktrees,
|
|
2105
|
+
...(auth.aliases ? { aliases: auth.aliases } : {}),
|
|
2106
|
+
});
|
|
2107
|
+
const target = managedTaskDomainPath(binding.controlRoot);
|
|
2108
|
+
const state = await withWorkflowStateLock(
|
|
2109
|
+
target,
|
|
2110
|
+
async () => {
|
|
2111
|
+
const read = await readExistingStateForMutation(target);
|
|
2112
|
+
if (read.kind !== "valid") throw new Error("established authority missing");
|
|
2113
|
+
const parsed = validateManagedTaskDomain(read.value);
|
|
2114
|
+
if (managedIdentity(parsed.binding) !== managedIdentity(binding))
|
|
2115
|
+
throw new Error("domain binding mismatch");
|
|
2116
|
+
return parsed;
|
|
2117
|
+
},
|
|
2118
|
+
{ cwd: binding.controlRoot, privateDurable: { directory: path.dirname(target) } },
|
|
2119
|
+
);
|
|
2120
|
+
return { ok: true, result: this.#managedPublic(state.state_revision, state.graphs) };
|
|
2121
|
+
} catch (caught) {
|
|
2122
|
+
return error("spawn_failed", caught instanceof Error ? caught.message : "managed status failed");
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
async #managedRevise(input: Record<string, unknown>): Promise<BrokerResponse> {
|
|
2127
|
+
if (typeof input.expectedRevision !== "number" || !Number.isSafeInteger(input.expectedRevision))
|
|
2128
|
+
return error("invalid_input", "expectedRevision is required");
|
|
2129
|
+
if (typeof input.graphId !== "string") return error("invalid_input", "graphId is required");
|
|
2130
|
+
const auth = await this.#verifiedManagedOwner(input);
|
|
2131
|
+
if (auth.kind === "denied") return auth.response;
|
|
2132
|
+
if (this.#managedEnrollmentFailed || this.#managedFailedRoots.has(auth.controlRoot))
|
|
2133
|
+
return error("terminal_uncertain", "managed enrollment membership is unprovable; revision was not applied");
|
|
2134
|
+
try {
|
|
2135
|
+
const binding = await createManagedDomainBinding({
|
|
2136
|
+
controlRoot: auth.controlRoot,
|
|
2137
|
+
agentDir: this.settings.agentDir,
|
|
2138
|
+
enrollmentId: auth.enrollmentId,
|
|
2139
|
+
worktrees: auth.worktrees,
|
|
2140
|
+
...(auth.aliases ? { aliases: auth.aliases } : {}),
|
|
2141
|
+
});
|
|
2142
|
+
const result = await transactManagedTaskDomain(
|
|
2143
|
+
{ binding, expectedRevision: input.expectedRevision },
|
|
2144
|
+
async state => {
|
|
2145
|
+
const graph = state.graphs.find(item => item.id === input.graphId);
|
|
2146
|
+
if (!graph || graph.owner !== auth.ownerSessionId) throw new Error("graph owner mismatch");
|
|
2147
|
+
return reviseManagedTaskGraph(state, graph, input.nodes);
|
|
2148
|
+
},
|
|
2149
|
+
);
|
|
2150
|
+
if (!(await this.#closeManagedAttempts(binding.controlRoot, input.graphId as string, result.result)))
|
|
2151
|
+
return error("terminal_uncertain", "managed child close remains unresolved after graph revision");
|
|
2152
|
+
return {
|
|
2153
|
+
ok: true,
|
|
2154
|
+
result: {
|
|
2155
|
+
...this.#managedPublic(result.state.state_revision, result.state.graphs),
|
|
2156
|
+
affected: result.result,
|
|
2157
|
+
},
|
|
2158
|
+
};
|
|
2159
|
+
} catch (caught) {
|
|
2160
|
+
return error("spawn_failed", caught instanceof Error ? caught.message : "managed revise failed");
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
async #managedCancel(input: Record<string, unknown>): Promise<BrokerResponse> {
|
|
2165
|
+
if (typeof input.expectedRevision !== "number" || !Number.isSafeInteger(input.expectedRevision))
|
|
2166
|
+
return error("invalid_input", "expectedRevision is required");
|
|
2167
|
+
if (typeof input.graphId !== "string") return error("invalid_input", "graphId is required");
|
|
2168
|
+
const ids = Array.isArray(input.nodeIds)
|
|
2169
|
+
? input.nodeIds.filter((value): value is string => typeof value === "string")
|
|
2170
|
+
: [];
|
|
2171
|
+
if (ids.length === 0) return error("invalid_input", "nodeIds are required");
|
|
2172
|
+
const auth = await this.#verifiedManagedOwner(input);
|
|
2173
|
+
if (auth.kind === "denied") return auth.response;
|
|
2174
|
+
if (this.#managedEnrollmentFailed || this.#managedFailedRoots.has(auth.controlRoot))
|
|
2175
|
+
return error(
|
|
2176
|
+
"terminal_uncertain",
|
|
2177
|
+
"managed enrollment membership is unprovable; cancellation was not applied",
|
|
2178
|
+
);
|
|
2179
|
+
try {
|
|
2180
|
+
const binding = await createManagedDomainBinding({
|
|
2181
|
+
controlRoot: auth.controlRoot,
|
|
2182
|
+
agentDir: this.settings.agentDir,
|
|
2183
|
+
enrollmentId: auth.enrollmentId,
|
|
2184
|
+
worktrees: auth.worktrees,
|
|
2185
|
+
...(auth.aliases ? { aliases: auth.aliases } : {}),
|
|
2186
|
+
});
|
|
2187
|
+
const result = await transactManagedTaskDomain(
|
|
2188
|
+
{ binding, expectedRevision: input.expectedRevision },
|
|
2189
|
+
async state => {
|
|
2190
|
+
const graph = state.graphs.find(item => item.id === input.graphId);
|
|
2191
|
+
if (!graph || graph.owner !== auth.ownerSessionId) throw new Error("graph owner mismatch");
|
|
2192
|
+
return cancelManagedTasks(graph, ids);
|
|
2193
|
+
},
|
|
2194
|
+
);
|
|
2195
|
+
if (!(await this.#closeManagedAttempts(binding.controlRoot, input.graphId as string, result.result)))
|
|
2196
|
+
return error("terminal_uncertain", "managed child close remains unresolved after cancellation");
|
|
2197
|
+
return {
|
|
2198
|
+
ok: true,
|
|
2199
|
+
result: {
|
|
2200
|
+
...this.#managedPublic(result.state.state_revision, result.state.graphs),
|
|
2201
|
+
affected: result.result,
|
|
2202
|
+
},
|
|
2203
|
+
};
|
|
2204
|
+
} catch (caught) {
|
|
2205
|
+
return error("spawn_failed", caught instanceof Error ? caught.message : "managed cancel failed");
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
async #managedVerify(input: Record<string, unknown>): Promise<BrokerResponse> {
|
|
2209
|
+
if (typeof input.graphId !== "string" || typeof input.nodeId !== "string")
|
|
2210
|
+
return error("invalid_input", "graphId and nodeId are required");
|
|
2211
|
+
const auth = await this.#verifiedManagedOwner(input);
|
|
2212
|
+
if (auth.kind === "denied") return auth.response;
|
|
2213
|
+
try {
|
|
2214
|
+
const binding = await createManagedDomainBinding({
|
|
2215
|
+
controlRoot: auth.controlRoot,
|
|
2216
|
+
agentDir: this.settings.agentDir,
|
|
2217
|
+
enrollmentId: auth.enrollmentId,
|
|
2218
|
+
worktrees: auth.worktrees,
|
|
2219
|
+
...(auth.aliases ? { aliases: auth.aliases } : {}),
|
|
2220
|
+
});
|
|
2221
|
+
const verified = await verifyManagedTaskAttempt({
|
|
2222
|
+
binding,
|
|
2223
|
+
graphId: input.graphId,
|
|
2224
|
+
nodeId: input.nodeId,
|
|
2225
|
+
owner: auth.ownerSessionId,
|
|
2226
|
+
});
|
|
2227
|
+
return { ok: true, result: { ...verified, commandsStarted: verified.started } };
|
|
2228
|
+
} catch (caught) {
|
|
2229
|
+
return error("spawn_failed", caught instanceof Error ? caught.message : "managed verify failed");
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
1530
2232
|
|
|
1531
2233
|
/** Safe, allowlisted spawn result projection; never carries request material. */
|
|
1532
2234
|
#spawnResult(code: "spawn_accepted" | "spawn_replayed", claim: SpawnClaimV2): Record<string, unknown> {
|
|
@@ -1560,6 +2262,53 @@ export class Broker {
|
|
|
1560
2262
|
return error("resource_gone", "session.spawn claim is closed");
|
|
1561
2263
|
}
|
|
1562
2264
|
|
|
2265
|
+
/** Re-prove an unbound launch on an idempotent retry instead of fencing it forever. */
|
|
2266
|
+
async #reconcileUncertainRegistration(lifecycleIdentity: string, claim: SpawnClaimV2): Promise<BrokerResponse> {
|
|
2267
|
+
const proof = claim.substrateProof;
|
|
2268
|
+
if (claim.state !== "uncertain" || claim.authorityRef !== undefined || !claim.childId || proof === undefined)
|
|
2269
|
+
return error("terminal_uncertain", "session.spawn registration authority is unavailable for reconciliation");
|
|
2270
|
+
const provider = this.#spawnSubstrateProvider();
|
|
2271
|
+
let verdict: "verified" | "mismatch" | "gone";
|
|
2272
|
+
try {
|
|
2273
|
+
verdict = await provider.verify(proof);
|
|
2274
|
+
} catch {
|
|
2275
|
+
return error("terminal_uncertain", "session.spawn registration proof could not be re-verified");
|
|
2276
|
+
}
|
|
2277
|
+
if (verdict === "mismatch")
|
|
2278
|
+
return error("terminal_uncertain", "session.spawn registration proof no longer matches the launch");
|
|
2279
|
+
if (verdict === "verified") {
|
|
2280
|
+
try {
|
|
2281
|
+
const closed = await provider.close(proof);
|
|
2282
|
+
if (!closed.ok)
|
|
2283
|
+
return error(
|
|
2284
|
+
"terminal_uncertain",
|
|
2285
|
+
"session.spawn child registration remains unresolved after the bounded close attempt",
|
|
2286
|
+
);
|
|
2287
|
+
} catch {
|
|
2288
|
+
return error("terminal_uncertain", "session.spawn child registration close could not be verified");
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
const failure: SpawnSubstrateFailure = {
|
|
2292
|
+
substrateKind: proof.substrateKind,
|
|
2293
|
+
code: "child_registration_reconciled_failed",
|
|
2294
|
+
message:
|
|
2295
|
+
verdict === "gone"
|
|
2296
|
+
? "session.spawn child host exited before registration"
|
|
2297
|
+
: "session.spawn child host was closed before registration",
|
|
2298
|
+
};
|
|
2299
|
+
try {
|
|
2300
|
+
await this.#spawnAuthority?.persistTransition(lifecycleIdentity, {
|
|
2301
|
+
claimId: claim.claimId,
|
|
2302
|
+
from: "uncertain",
|
|
2303
|
+
to: "pre_send_rejected",
|
|
2304
|
+
failure,
|
|
2305
|
+
});
|
|
2306
|
+
} catch {
|
|
2307
|
+
return error("terminal_uncertain", "session.spawn registration reconciliation could not be persisted");
|
|
2308
|
+
}
|
|
2309
|
+
return spawnFailureError(failure);
|
|
2310
|
+
}
|
|
2311
|
+
|
|
1563
2312
|
#spawnSubstrateProvider(): SpawnSubstrateProvider {
|
|
1564
2313
|
if (this.settings.spawnSubstrateProvider) return this.settings.spawnSubstrateProvider;
|
|
1565
2314
|
this.settings.spawnSubstrateProvider = createSpawnSubstrateProvider();
|
|
@@ -1595,14 +2344,39 @@ export class Broker {
|
|
|
1595
2344
|
...(admission.modelId === undefined ? {} : { modelId: admission.modelId }),
|
|
1596
2345
|
...(admission.modelPreset === undefined ? {} : { modelPreset: admission.modelPreset }),
|
|
1597
2346
|
});
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
2347
|
+
if (admission.managedAttempt && admission.managedBinding && admission.managedNativeVector) {
|
|
2348
|
+
try {
|
|
2349
|
+
current = await withManagedNativeEffectAuthorized(
|
|
2350
|
+
admission.managedBinding,
|
|
2351
|
+
admission.managedAttempt.nativeIdentity,
|
|
2352
|
+
admission.managedNativeVector,
|
|
2353
|
+
"launch",
|
|
2354
|
+
async () =>
|
|
2355
|
+
(
|
|
2356
|
+
await store.persistTransition(lifecycleIdentity, {
|
|
2357
|
+
claimId: current.claimId,
|
|
2358
|
+
from: "prepared",
|
|
2359
|
+
to: "substrate_starting",
|
|
2360
|
+
childId: prep.childId,
|
|
2361
|
+
})
|
|
2362
|
+
).claim,
|
|
2363
|
+
);
|
|
2364
|
+
} catch (caught) {
|
|
2365
|
+
return error(
|
|
2366
|
+
"spawn_failed",
|
|
2367
|
+
caught instanceof Error ? caught.message : "managed native fence denied",
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2370
|
+
} else {
|
|
2371
|
+
current = (
|
|
2372
|
+
await store.persistTransition(lifecycleIdentity, {
|
|
2373
|
+
claimId: current.claimId,
|
|
2374
|
+
from: "prepared",
|
|
2375
|
+
to: "substrate_starting",
|
|
2376
|
+
childId: prep.childId,
|
|
2377
|
+
})
|
|
2378
|
+
).claim;
|
|
2379
|
+
}
|
|
1606
2380
|
inFlight.phase = current.state;
|
|
1607
2381
|
const launched = await provider.launch({
|
|
1608
2382
|
childSessionId: prep.childId,
|
|
@@ -1635,8 +2409,26 @@ export class Broker {
|
|
|
1635
2409
|
code: "substrate_proof_failed",
|
|
1636
2410
|
message: "session.spawn substrate lacks lifecycle process authority",
|
|
1637
2411
|
};
|
|
1638
|
-
await this.#releaseUnownedSubstrate(provider, launchedProof);
|
|
1639
|
-
|
|
2412
|
+
const release = await this.#releaseUnownedSubstrate(provider, launchedProof);
|
|
2413
|
+
if (release === "unresolved") {
|
|
2414
|
+
current = (
|
|
2415
|
+
await store.persistTransition(lifecycleIdentity, {
|
|
2416
|
+
claimId: current.claimId,
|
|
2417
|
+
from: "substrate_starting",
|
|
2418
|
+
to: "uncertain",
|
|
2419
|
+
failure: {
|
|
2420
|
+
substrateKind: launched.proof.substrateKind,
|
|
2421
|
+
code: "child_registration_release_unproven",
|
|
2422
|
+
message: "session.spawn child registration could not prove substrate release",
|
|
2423
|
+
},
|
|
2424
|
+
})
|
|
2425
|
+
).claim;
|
|
2426
|
+
launchedProof = undefined;
|
|
2427
|
+
return error(
|
|
2428
|
+
"terminal_uncertain",
|
|
2429
|
+
"session.spawn substrate release could not be proven after the launch proof failed",
|
|
2430
|
+
);
|
|
2431
|
+
}
|
|
1640
2432
|
current = (
|
|
1641
2433
|
await store.persistTransition(lifecycleIdentity, {
|
|
1642
2434
|
claimId: current.claimId,
|
|
@@ -1645,8 +2437,68 @@ export class Broker {
|
|
|
1645
2437
|
failure,
|
|
1646
2438
|
})
|
|
1647
2439
|
).claim;
|
|
2440
|
+
launchedProof = undefined;
|
|
1648
2441
|
return spawnFailureError(failure);
|
|
1649
2442
|
}
|
|
2443
|
+
current = (
|
|
2444
|
+
await store.persistTransition(lifecycleIdentity, {
|
|
2445
|
+
claimId: current.claimId,
|
|
2446
|
+
from: "substrate_starting",
|
|
2447
|
+
to: "substrate_starting",
|
|
2448
|
+
childId: prep.childId,
|
|
2449
|
+
substrateProof: launched.proof,
|
|
2450
|
+
})
|
|
2451
|
+
).claim;
|
|
2452
|
+
inFlight.phase = current.state;
|
|
2453
|
+
if (admission.managedAttempt && admission.managedBinding) {
|
|
2454
|
+
let attempt: ManagedTaskAttempt | undefined;
|
|
2455
|
+
try {
|
|
2456
|
+
attempt = await inspectManagedAttemptByNativeIdentity(
|
|
2457
|
+
admission.managedBinding,
|
|
2458
|
+
admission.managedAttempt.nativeIdentity,
|
|
2459
|
+
);
|
|
2460
|
+
} catch {
|
|
2461
|
+
// A failed fence read cannot prove the launch is still authorized.
|
|
2462
|
+
}
|
|
2463
|
+
if (attempt?.fence !== "current" || attempt?.retired) {
|
|
2464
|
+
const release = await this.#releaseUnownedSubstrate(provider, launched.proof);
|
|
2465
|
+
let gone = release === "gone";
|
|
2466
|
+
if (release === "closed") {
|
|
2467
|
+
try {
|
|
2468
|
+
gone = (await provider.verify(launched.proof)) === "gone";
|
|
2469
|
+
} catch {
|
|
2470
|
+
gone = false;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
if (gone) {
|
|
2474
|
+
current = (
|
|
2475
|
+
await store.persistTransition(lifecycleIdentity, {
|
|
2476
|
+
claimId: current.claimId,
|
|
2477
|
+
from: "substrate_starting",
|
|
2478
|
+
to: "closed",
|
|
2479
|
+
})
|
|
2480
|
+
).claim;
|
|
2481
|
+
inFlight.phase = current.state;
|
|
2482
|
+
launchedProof = undefined;
|
|
2483
|
+
return error("spawn_failed", "managed attempt was fenced while its child was launching");
|
|
2484
|
+
}
|
|
2485
|
+
current = (
|
|
2486
|
+
await store.persistTransition(lifecycleIdentity, {
|
|
2487
|
+
claimId: current.claimId,
|
|
2488
|
+
from: "substrate_starting",
|
|
2489
|
+
to: "uncertain",
|
|
2490
|
+
failure: {
|
|
2491
|
+
substrateKind: launched.proof.substrateKind,
|
|
2492
|
+
code: "managed_child_close_unresolved",
|
|
2493
|
+
message: "managed child close could not be proven after its attempt was fenced",
|
|
2494
|
+
},
|
|
2495
|
+
})
|
|
2496
|
+
).claim;
|
|
2497
|
+
inFlight.phase = current.state;
|
|
2498
|
+
launchedProof = undefined;
|
|
2499
|
+
return error("terminal_uncertain", "managed child close is unresolved after its attempt was fenced");
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
1650
2502
|
const marker = { pid, incarnation, effectMarker: prep.effectMarker };
|
|
1651
2503
|
await writeEffectMarker(prep.stateRoot, prep.childId, marker);
|
|
1652
2504
|
const registration = await this.#spawnPromptLayer.awaitRegistration({
|
|
@@ -1656,18 +2508,44 @@ export class Broker {
|
|
|
1656
2508
|
});
|
|
1657
2509
|
if (!registration.ok) {
|
|
1658
2510
|
const startupFailure = await readSessionLifecycleFailure(prep.stateRoot, prep.childId, marker);
|
|
1659
|
-
await this.#releaseUnownedSubstrate(provider, launchedProof);
|
|
1660
|
-
|
|
2511
|
+
const release = await this.#releaseUnownedSubstrate(provider, launchedProof);
|
|
2512
|
+
if (release === "closed" || release === "gone") {
|
|
2513
|
+
const failure: SpawnSubstrateFailure = {
|
|
2514
|
+
substrateKind: launched.proof.substrateKind,
|
|
2515
|
+
code: startupFailure === undefined ? "child_registration_timeout" : "child_registration_failed",
|
|
2516
|
+
message:
|
|
2517
|
+
startupFailure === undefined
|
|
2518
|
+
? "session.spawn child host did not register before the bounded admission deadline"
|
|
2519
|
+
: `session.spawn child host failed during ${startupFailure.phase}/${startupFailure.reason}`,
|
|
2520
|
+
};
|
|
2521
|
+
current = (
|
|
2522
|
+
await store.persistTransition(lifecycleIdentity, {
|
|
2523
|
+
claimId: current.claimId,
|
|
2524
|
+
from: "substrate_starting",
|
|
2525
|
+
to: "pre_send_rejected",
|
|
2526
|
+
failure,
|
|
2527
|
+
})
|
|
2528
|
+
).claim;
|
|
2529
|
+
launchedProof = undefined;
|
|
2530
|
+
return spawnFailureError(failure);
|
|
2531
|
+
}
|
|
2532
|
+
const unresolvedRelease: SpawnSubstrateFailure = {
|
|
2533
|
+
substrateKind: launched.proof.substrateKind,
|
|
2534
|
+
code: "child_registration_release_unproven",
|
|
2535
|
+
message: "session.spawn child registration could not prove substrate release",
|
|
2536
|
+
};
|
|
1661
2537
|
current = (
|
|
1662
2538
|
await store.persistTransition(lifecycleIdentity, {
|
|
1663
2539
|
claimId: current.claimId,
|
|
1664
2540
|
from: "substrate_starting",
|
|
1665
2541
|
to: "uncertain",
|
|
2542
|
+
failure: unresolvedRelease,
|
|
1666
2543
|
})
|
|
1667
2544
|
).claim;
|
|
2545
|
+
launchedProof = undefined;
|
|
1668
2546
|
return error(
|
|
1669
2547
|
"terminal_uncertain",
|
|
1670
|
-
"session.spawn child registration is uncertain" +
|
|
2548
|
+
"session.spawn child registration is uncertain (substrate release could not be proven)" +
|
|
1671
2549
|
(startupFailure ? ` (${startupFailure.phase}/${startupFailure.reason})` : ""),
|
|
1672
2550
|
);
|
|
1673
2551
|
}
|
|
@@ -1766,26 +2644,59 @@ export class Broker {
|
|
|
1766
2644
|
}
|
|
1767
2645
|
const childId = current.childId;
|
|
1768
2646
|
const preparedSeed = current.seed;
|
|
2647
|
+
const preSendLeaseEpoch = current.preSendLease.epoch;
|
|
2648
|
+
const exactPinnedRegistration = pinnedRegistration;
|
|
1769
2649
|
// Dispatch without a complete pin must never consume the pre-send lease.
|
|
1770
|
-
if (
|
|
2650
|
+
if (exactPinnedRegistration === undefined)
|
|
1771
2651
|
return error("terminal_uncertain", "session.spawn endpoint pin is unavailable before dispatch");
|
|
1772
|
-
|
|
1773
|
-
await
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
)
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
2652
|
+
const dispatchSeed = async (): Promise<SpawnPromptDispatch> =>
|
|
2653
|
+
await this.#spawnPromptLayer.dispatch({
|
|
2654
|
+
sessionId: childId,
|
|
2655
|
+
task: admission.task,
|
|
2656
|
+
clientRef: preparedSeed.clientRef,
|
|
2657
|
+
pinned: exactPinnedRegistration,
|
|
2658
|
+
});
|
|
2659
|
+
let dispatched: SpawnPromptDispatch;
|
|
2660
|
+
if (admission.managedAttempt && admission.managedBinding && admission.managedNativeVector) {
|
|
2661
|
+
try {
|
|
2662
|
+
dispatched = await withManagedNativeEffectAuthorized(
|
|
2663
|
+
admission.managedBinding,
|
|
2664
|
+
admission.managedAttempt.nativeIdentity,
|
|
2665
|
+
admission.managedNativeVector,
|
|
2666
|
+
"seed",
|
|
2667
|
+
async () => {
|
|
2668
|
+
current = (
|
|
2669
|
+
await store.persistTransition(lifecycleIdentity, {
|
|
2670
|
+
claimId: current.claimId,
|
|
2671
|
+
from: "seed_prepared",
|
|
2672
|
+
to: "dispatching",
|
|
2673
|
+
leaseEpoch: preSendLeaseEpoch,
|
|
2674
|
+
seed: { ...preparedSeed, phase: "dispatching" },
|
|
2675
|
+
})
|
|
2676
|
+
).claim;
|
|
2677
|
+
inFlight.phase = current.state;
|
|
2678
|
+
handedOff = true;
|
|
2679
|
+
return await dispatchSeed();
|
|
2680
|
+
},
|
|
2681
|
+
);
|
|
2682
|
+
} catch (caught) {
|
|
2683
|
+
if (handedOff) throw caught;
|
|
2684
|
+
return error("spawn_failed", caught instanceof Error ? caught.message : "managed native fence denied");
|
|
2685
|
+
}
|
|
2686
|
+
} else {
|
|
2687
|
+
current = (
|
|
2688
|
+
await store.persistTransition(lifecycleIdentity, {
|
|
2689
|
+
claimId: current.claimId,
|
|
2690
|
+
from: "seed_prepared",
|
|
2691
|
+
to: "dispatching",
|
|
2692
|
+
leaseEpoch: current.preSendLease.epoch,
|
|
2693
|
+
seed: { ...preparedSeed, phase: "dispatching" },
|
|
2694
|
+
})
|
|
2695
|
+
).claim;
|
|
2696
|
+
inFlight.phase = current.state;
|
|
2697
|
+
handedOff = true;
|
|
2698
|
+
dispatched = await dispatchSeed();
|
|
2699
|
+
}
|
|
1789
2700
|
if (dispatched.kind === "accepted") {
|
|
1790
2701
|
current = (
|
|
1791
2702
|
await store.persistTransition(lifecycleIdentity, {
|
|
@@ -1833,15 +2744,76 @@ export class Broker {
|
|
|
1833
2744
|
} catch {
|
|
1834
2745
|
// Once a substrate exists the outcome is ambiguous even before handoff:
|
|
1835
2746
|
// reporting an ordinary failure would downgrade retained uncertainty.
|
|
1836
|
-
|
|
2747
|
+
let ambiguous = handedOff || launchedProof !== undefined;
|
|
1837
2748
|
// A launched substrate with no persisted authority is invisible to the
|
|
1838
2749
|
// reaper, so in-process cleanup is its only chance. This must run on
|
|
1839
2750
|
// EVERY post-launch failure exit, not just a returned registration
|
|
1840
2751
|
// failure: a throw from awaitRegistration, verify, close, or a durable
|
|
1841
2752
|
// transition all land here.
|
|
1842
|
-
if (!handedOff)
|
|
2753
|
+
if (!handedOff) {
|
|
2754
|
+
const release = await this.#releaseUnownedSubstrate(provider, launchedProof);
|
|
2755
|
+
if ((release === "closed" || release === "gone") && current.state === "substrate_starting") {
|
|
2756
|
+
const failure: SpawnSubstrateFailure = {
|
|
2757
|
+
substrateKind: launchedProof?.substrateKind ?? "headless",
|
|
2758
|
+
code: "child_registration_failed",
|
|
2759
|
+
message: "session.spawn child host failed before registration",
|
|
2760
|
+
};
|
|
2761
|
+
try {
|
|
2762
|
+
await store.persistTransition(lifecycleIdentity, {
|
|
2763
|
+
claimId: current.claimId,
|
|
2764
|
+
from: "substrate_starting",
|
|
2765
|
+
to: "pre_send_rejected",
|
|
2766
|
+
failure,
|
|
2767
|
+
});
|
|
2768
|
+
launchedProof = undefined;
|
|
2769
|
+
return spawnFailureError(failure);
|
|
2770
|
+
} catch {
|
|
2771
|
+
// The substrate is closed, but a failed terminal write still leaves
|
|
2772
|
+
// the durable claim unresolved for the next broker recovery pass.
|
|
2773
|
+
}
|
|
2774
|
+
} else if (release === "unresolved" && current.state === "substrate_starting") {
|
|
2775
|
+
try {
|
|
2776
|
+
current = (
|
|
2777
|
+
await store.persistTransition(lifecycleIdentity, {
|
|
2778
|
+
claimId: current.claimId,
|
|
2779
|
+
from: "substrate_starting",
|
|
2780
|
+
to: "uncertain",
|
|
2781
|
+
failure: {
|
|
2782
|
+
substrateKind: launchedProof?.substrateKind ?? "headless",
|
|
2783
|
+
code: "child_registration_release_unproven",
|
|
2784
|
+
message: "session.spawn child registration could not prove substrate release",
|
|
2785
|
+
},
|
|
2786
|
+
})
|
|
2787
|
+
).claim;
|
|
2788
|
+
launchedProof = undefined;
|
|
2789
|
+
} catch {
|
|
2790
|
+
// Preserve the generic durable-state uncertainty if the reason itself
|
|
2791
|
+
// cannot be appended.
|
|
2792
|
+
}
|
|
2793
|
+
} else if (release === "absent" && current.state === "substrate_starting") {
|
|
2794
|
+
try {
|
|
2795
|
+
current = (
|
|
2796
|
+
await store.persistTransition(lifecycleIdentity, {
|
|
2797
|
+
claimId: current.claimId,
|
|
2798
|
+
from: "substrate_starting",
|
|
2799
|
+
to: "uncertain",
|
|
2800
|
+
})
|
|
2801
|
+
).claim;
|
|
2802
|
+
ambiguous = true;
|
|
2803
|
+
} catch {
|
|
2804
|
+
// Keep the generic failure when even the uncertainty transition cannot be written.
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
if (ambiguous && admission.managedBinding)
|
|
2809
|
+
await recordManagedNativeObservation(admission.managedBinding, lifecycleIdentity, "unknown").catch(
|
|
2810
|
+
() => undefined,
|
|
2811
|
+
);
|
|
1843
2812
|
return ambiguous
|
|
1844
|
-
? error(
|
|
2813
|
+
? error(
|
|
2814
|
+
"terminal_uncertain",
|
|
2815
|
+
current.failure?.message ?? "session.spawn state could not be advanced durably",
|
|
2816
|
+
)
|
|
1845
2817
|
: error("spawn_failed", "session.spawn could not be advanced durably");
|
|
1846
2818
|
}
|
|
1847
2819
|
}
|
|
@@ -1855,22 +2827,26 @@ export class Broker {
|
|
|
1855
2827
|
async #releaseUnownedSubstrate(
|
|
1856
2828
|
provider: SpawnSubstrateProvider,
|
|
1857
2829
|
proof: SpawnSubstrateProof | undefined,
|
|
1858
|
-
): Promise<
|
|
1859
|
-
|
|
2830
|
+
): Promise<"closed" | "gone" | "absent" | "unresolved"> {
|
|
2831
|
+
// A launch that throws before returning proof has no substrate identity to
|
|
2832
|
+
// classify as gone; keep the claim uncertain instead of fabricating a kind.
|
|
2833
|
+
if (!proof) return "absent";
|
|
1860
2834
|
let verdict: "verified" | "mismatch" | "gone";
|
|
1861
2835
|
try {
|
|
1862
2836
|
verdict = await provider.verify(proof);
|
|
1863
2837
|
} catch {
|
|
1864
2838
|
// An unprovable substrate is never mutated; uncertainty is retained by
|
|
1865
2839
|
// the caller's durable transition instead.
|
|
1866
|
-
return;
|
|
2840
|
+
return "unresolved";
|
|
1867
2841
|
}
|
|
1868
|
-
if (verdict
|
|
2842
|
+
if (verdict === "gone") return "gone";
|
|
2843
|
+
if (verdict !== "verified") return "unresolved";
|
|
1869
2844
|
try {
|
|
1870
|
-
await provider.close(proof);
|
|
2845
|
+
return (await provider.close(proof)).ok ? "closed" : "unresolved";
|
|
1871
2846
|
} catch {
|
|
1872
2847
|
// The substrate may survive. The caller still records uncertainty, which
|
|
1873
2848
|
// is the honest durable outcome for an unclosed unowned substrate.
|
|
2849
|
+
return "unresolved";
|
|
1874
2850
|
}
|
|
1875
2851
|
}
|
|
1876
2852
|
|
|
@@ -2032,6 +3008,148 @@ export class Broker {
|
|
|
2032
3008
|
}
|
|
2033
3009
|
}
|
|
2034
3010
|
|
|
3011
|
+
async #restoreManagedAttempts(): Promise<void> {
|
|
3012
|
+
this.#managedAttempts.clear();
|
|
3013
|
+
this.#managedFailedRoots.clear();
|
|
3014
|
+
this.#managedEnrollmentFailed = false;
|
|
3015
|
+
try {
|
|
3016
|
+
const restored = await restoreManagedAttemptRefs(this.settings.agentDir);
|
|
3017
|
+
for (const ref of restored.refs) this.#managedAttempts.set(ref.nativeIdentity, ref);
|
|
3018
|
+
for (const root of restored.failedRoots) this.#managedFailedRoots.add(root);
|
|
3019
|
+
} catch {
|
|
3020
|
+
this.#managedEnrollmentFailed = true;
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
async #observeManagedNative(
|
|
3025
|
+
lifecycleIdentity: string,
|
|
3026
|
+
worker: "no-effect" | "authorized" | "unknown" | "closed",
|
|
3027
|
+
): Promise<void> {
|
|
3028
|
+
const ref = this.#managedAttempts.get(lifecycleIdentity);
|
|
3029
|
+
if (!ref) return;
|
|
3030
|
+
const binding = await loadManagedDomainBinding(ref.controlRoot);
|
|
3031
|
+
if (!binding) return;
|
|
3032
|
+
await recordManagedNativeObservation(binding, lifecycleIdentity, worker);
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
async #reconcileManagedNativeLifetimes(): Promise<void> {
|
|
3036
|
+
const store = this.#spawnAuthority;
|
|
3037
|
+
if (!store || this.#managedEnrollmentFailed) return;
|
|
3038
|
+
for (const ref of this.#managedAttempts.values()) {
|
|
3039
|
+
if (this.#managedFailedRoots.has(ref.controlRoot)) continue;
|
|
3040
|
+
const binding = await loadManagedDomainBinding(ref.controlRoot);
|
|
3041
|
+
if (!binding) throw new Error("managed attempt domain is unavailable during startup reconciliation");
|
|
3042
|
+
const attempt = await inspectManagedAttemptByNativeIdentity(binding, ref.nativeIdentity);
|
|
3043
|
+
if (!attempt) throw new Error("managed attempt authority is missing during startup reconciliation");
|
|
3044
|
+
const claim = store.claim(ref.nativeIdentity);
|
|
3045
|
+
if (attempt.fence !== "current" || attempt.retired) {
|
|
3046
|
+
if (!claim) {
|
|
3047
|
+
await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
|
|
3048
|
+
continue;
|
|
3049
|
+
}
|
|
3050
|
+
if (claim.state === "prepared") {
|
|
3051
|
+
await store.persistTransition(ref.nativeIdentity, {
|
|
3052
|
+
claimId: claim.claimId,
|
|
3053
|
+
from: "prepared",
|
|
3054
|
+
to: "closed",
|
|
3055
|
+
});
|
|
3056
|
+
await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
|
|
3057
|
+
continue;
|
|
3058
|
+
}
|
|
3059
|
+
if (claim.state === "closed" && !store.authority(ref.nativeIdentity) && !claim.substrateProof) {
|
|
3060
|
+
await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
|
|
3061
|
+
continue;
|
|
3062
|
+
}
|
|
3063
|
+
if (claim.state === "pre_send_rejected" && !store.authority(ref.nativeIdentity) && !claim.substrateProof) {
|
|
3064
|
+
await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
|
|
3065
|
+
continue;
|
|
3066
|
+
}
|
|
3067
|
+
const outcome = await this.#closeSpawnAuthority(ref.nativeIdentity);
|
|
3068
|
+
if (outcome !== "closed") {
|
|
3069
|
+
await this.#observeManagedNative(ref.nativeIdentity, "unknown").catch(() => undefined);
|
|
3070
|
+
throw new Error("fenced managed child could not be exactly closed during startup reconciliation");
|
|
3071
|
+
}
|
|
3072
|
+
await this.#observeManagedNative(ref.nativeIdentity, "closed");
|
|
3073
|
+
continue;
|
|
3074
|
+
}
|
|
3075
|
+
if (!claim) {
|
|
3076
|
+
await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
|
|
3077
|
+
continue;
|
|
3078
|
+
}
|
|
3079
|
+
if (claim.state === "uncertain" || claim.state === "dispatching" || claim.state === "substrate_starting") {
|
|
3080
|
+
await this.#observeManagedNative(ref.nativeIdentity, "unknown");
|
|
3081
|
+
continue;
|
|
3082
|
+
}
|
|
3083
|
+
if (claim.state === "closed") {
|
|
3084
|
+
const authority = store.authority(ref.nativeIdentity);
|
|
3085
|
+
if (authority?.closeState === "closed" || (!authority && claim.substrateProof))
|
|
3086
|
+
await this.#observeManagedNative(ref.nativeIdentity, "closed");
|
|
3087
|
+
else if (!authority) await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
|
|
3088
|
+
else await this.#observeManagedNative(ref.nativeIdentity, "unknown");
|
|
3089
|
+
continue;
|
|
3090
|
+
}
|
|
3091
|
+
if (claim.state === "pre_send_rejected") {
|
|
3092
|
+
const authority = store.authority(ref.nativeIdentity);
|
|
3093
|
+
if (!authority && claim.substrateProof) {
|
|
3094
|
+
const outcome = await this.#closeSpawnAuthority(ref.nativeIdentity);
|
|
3095
|
+
await this.#observeManagedNative(ref.nativeIdentity, outcome === "closed" ? "closed" : "unknown");
|
|
3096
|
+
} else if (!authority) await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
|
|
3097
|
+
else await this.#observeManagedNative(ref.nativeIdentity, "unknown");
|
|
3098
|
+
continue;
|
|
3099
|
+
}
|
|
3100
|
+
if (claim.state === "prepared") continue;
|
|
3101
|
+
await this.#observeManagedNative(ref.nativeIdentity, "authorized");
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
async #closeManagedAttempts(controlRoot: string, graphId: string, nodeIds: string[]): Promise<boolean> {
|
|
3106
|
+
const affected = [...this.#managedAttempts.values()].filter(
|
|
3107
|
+
ref => ref.controlRoot === controlRoot && ref.graphId === graphId && nodeIds.includes(ref.nodeId),
|
|
3108
|
+
);
|
|
3109
|
+
let allClosed = true;
|
|
3110
|
+
for (const ref of affected) {
|
|
3111
|
+
const inFlight = this.#spawnInFlight.get(ref.nativeIdentity);
|
|
3112
|
+
const waitMs = managedCloseWaitOverridesForTest.get(this) ?? MANAGED_CLOSE_WAIT_MS;
|
|
3113
|
+
if (inFlight && !(await settlesWithin(inFlight.completion, waitMs))) {
|
|
3114
|
+
this.#scheduleManagedCloseAfterFlight(ref, inFlight);
|
|
3115
|
+
await this.#observeManagedNative(ref.nativeIdentity, "unknown").catch(() => undefined);
|
|
3116
|
+
allClosed = false;
|
|
3117
|
+
continue;
|
|
3118
|
+
}
|
|
3119
|
+
const claim = this.#spawnAuthority?.claim(ref.nativeIdentity);
|
|
3120
|
+
const authority = this.#spawnAuthority?.authority(ref.nativeIdentity);
|
|
3121
|
+
if (!claim || (claim.state === "pre_send_rejected" && !authority && !claim.substrateProof)) {
|
|
3122
|
+
try {
|
|
3123
|
+
await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
|
|
3124
|
+
} catch {
|
|
3125
|
+
await this.#observeManagedNative(ref.nativeIdentity, "unknown").catch(() => undefined);
|
|
3126
|
+
allClosed = false;
|
|
3127
|
+
}
|
|
3128
|
+
continue;
|
|
3129
|
+
}
|
|
3130
|
+
const outcome = await this.#closeSpawnAuthority(ref.nativeIdentity);
|
|
3131
|
+
if (outcome === "closed") await this.#observeManagedNative(ref.nativeIdentity, "closed");
|
|
3132
|
+
else {
|
|
3133
|
+
await this.#observeManagedNative(ref.nativeIdentity, "unknown").catch(() => undefined);
|
|
3134
|
+
allClosed = false;
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
return allClosed;
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
#scheduleManagedCloseAfterFlight(ref: ManagedAttemptRef, inFlight: SpawnInFlight): void {
|
|
3141
|
+
void inFlight.completion
|
|
3142
|
+
.then(async () => {
|
|
3143
|
+
const outcome = await this.#closeSpawnAuthority(ref.nativeIdentity);
|
|
3144
|
+
if (outcome === "closed") await this.#observeManagedNative(ref.nativeIdentity, "closed");
|
|
3145
|
+
else await this.#observeManagedNative(ref.nativeIdentity, "unknown");
|
|
3146
|
+
})
|
|
3147
|
+
.catch(async caught => {
|
|
3148
|
+
logger.warn(`sdk broker: deferred managed child close failed for ${ref.nativeIdentity}: ${String(caught)}`);
|
|
3149
|
+
await this.#observeManagedNative(ref.nativeIdentity, "unknown").catch(() => undefined);
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
|
|
2035
3153
|
async #awaitSpawnHostRegistration(input: {
|
|
2036
3154
|
childId: string;
|
|
2037
3155
|
cwd: string;
|
|
@@ -2043,21 +3161,9 @@ export class Broker {
|
|
|
2043
3161
|
await this.index.refresh();
|
|
2044
3162
|
// The launch locator is authority: a same-id row registered by an
|
|
2045
3163
|
// unrelated workspace must never be adopted as this spawn's child.
|
|
2046
|
-
const row = this.index
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
candidate.endpointGeneration > 0 &&
|
|
2050
|
-
candidate.live &&
|
|
2051
|
-
!candidate.terminal &&
|
|
2052
|
-
!candidate.terminalUncertain &&
|
|
2053
|
-
// Path IDENTITY, not spelling. The child publishes a
|
|
2054
|
-
// realpath-canonicalized locator while the launch spec carries the
|
|
2055
|
-
// caller's lexical path, so `path.resolve` never reconciles a
|
|
2056
|
-
// symlinked workspace (macOS /var vs /private/var) and a
|
|
2057
|
-
// legitimate child would never match.
|
|
2058
|
-
resolveEquivalentPath(candidate.locator.cwd) === resolveEquivalentPath(input.cwd) &&
|
|
2059
|
-
resolveEquivalentPath(candidate.locator.stateRoot) === resolveEquivalentPath(input.stateRoot),
|
|
2060
|
-
);
|
|
3164
|
+
const row = this.index
|
|
3165
|
+
.listSessionIdentities()
|
|
3166
|
+
.find(candidate => spawnRegistrationMatches(candidate, input));
|
|
2061
3167
|
if (row) {
|
|
2062
3168
|
const incarnation = row.hostIncarnation ?? row.processIncarnation;
|
|
2063
3169
|
// An incarnation-less row is incomplete endpoint evidence; a partial pin
|
|
@@ -2235,7 +3341,10 @@ export class Broker {
|
|
|
2235
3341
|
const claim = store.claims().find(candidate => candidate.childId === sessionId);
|
|
2236
3342
|
if (!claim) return undefined;
|
|
2237
3343
|
const outcome = await this.#closeSpawnAuthority(claim.lifecycleIdentity);
|
|
2238
|
-
if (outcome === "closed")
|
|
3344
|
+
if (outcome === "closed") {
|
|
3345
|
+
await this.#observeManagedNative(claim.lifecycleIdentity, "closed").catch(() => undefined);
|
|
3346
|
+
return { ok: true, result: { code: "spawn_child_closed", sessionId } };
|
|
3347
|
+
}
|
|
2239
3348
|
if (outcome === "uncertain")
|
|
2240
3349
|
return error("terminal_uncertain", "session.close substrate identity could not be re-proven");
|
|
2241
3350
|
return error("close_refused", "session.close could not complete for the spawned child");
|
|
@@ -2252,8 +3361,47 @@ export class Broker {
|
|
|
2252
3361
|
try {
|
|
2253
3362
|
const claim = store.claim(lifecycleIdentity);
|
|
2254
3363
|
let authority = store.authority(lifecycleIdentity);
|
|
2255
|
-
if (!claim
|
|
2256
|
-
if (claim.state === "closed" && authority.closeState === "closed") return "closed";
|
|
3364
|
+
if (!claim) return "retained";
|
|
3365
|
+
if (claim.state === "closed" && (!authority || authority.closeState === "closed")) return "closed";
|
|
3366
|
+
if (!authority) {
|
|
3367
|
+
if (!claim.substrateProof || !claim.childId) return "retained";
|
|
3368
|
+
const provider = this.#spawnSubstrateProvider();
|
|
3369
|
+
const proof = claim.substrateProof;
|
|
3370
|
+
let verdict: "verified" | "mismatch" | "gone";
|
|
3371
|
+
try {
|
|
3372
|
+
verdict = await provider.verify(proof);
|
|
3373
|
+
} catch {
|
|
3374
|
+
verdict = "mismatch";
|
|
3375
|
+
}
|
|
3376
|
+
let gone = verdict === "gone";
|
|
3377
|
+
if (verdict === "verified") {
|
|
3378
|
+
try {
|
|
3379
|
+
gone = (await provider.close(proof)).ok && (await provider.verify(proof)) === "gone";
|
|
3380
|
+
} catch {
|
|
3381
|
+
gone = false;
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
if (!gone) {
|
|
3385
|
+
if (claim.state !== "uncertain")
|
|
3386
|
+
await store.persistTransition(lifecycleIdentity, {
|
|
3387
|
+
claimId: claim.claimId,
|
|
3388
|
+
from: claim.state,
|
|
3389
|
+
to: "uncertain",
|
|
3390
|
+
failure: {
|
|
3391
|
+
substrateKind: proof.substrateKind,
|
|
3392
|
+
code: "child_close_unresolved",
|
|
3393
|
+
message: "session.spawn child close could not be proven",
|
|
3394
|
+
},
|
|
3395
|
+
});
|
|
3396
|
+
return verdict === "mismatch" ? "uncertain" : "retained";
|
|
3397
|
+
}
|
|
3398
|
+
await store.persistTransition(lifecycleIdentity, {
|
|
3399
|
+
claimId: claim.claimId,
|
|
3400
|
+
from: claim.state,
|
|
3401
|
+
to: "closed",
|
|
3402
|
+
});
|
|
3403
|
+
return "closed";
|
|
3404
|
+
}
|
|
2257
3405
|
if (authority.closeState === "closed") return "closed";
|
|
2258
3406
|
const provider = this.#spawnSubstrateProvider();
|
|
2259
3407
|
const proof = spawnProofFromAuthority(authority);
|
|
@@ -2280,13 +3428,11 @@ export class Broker {
|
|
|
2280
3428
|
})
|
|
2281
3429
|
).authority!;
|
|
2282
3430
|
}
|
|
2283
|
-
if (verdict === "verified") {
|
|
3431
|
+
if (verdict === "verified" || verdict === "gone") {
|
|
2284
3432
|
const closedSubstrate = await provider.close(proof);
|
|
2285
3433
|
if (!closedSubstrate.ok) return "retained";
|
|
2286
|
-
// Signal delivery is not terminal evidence: only a second exact proof
|
|
2287
|
-
// that observes this substrate gone permits the durable close.
|
|
2288
3434
|
if ((await provider.verify(proof)) !== "gone") return "retained";
|
|
2289
|
-
}
|
|
3435
|
+
} else return "retained";
|
|
2290
3436
|
const at = Math.max(Date.now(), authority.updatedAt + 1);
|
|
2291
3437
|
const closedAuthority: SpawnAuthorityV1 = { ...authority, closeState: "closed", closedAt: at, updatedAt: at };
|
|
2292
3438
|
const currentClaim = store.claim(lifecycleIdentity);
|
|
@@ -2617,7 +3763,20 @@ export class Broker {
|
|
|
2617
3763
|
else if (mirror.requestHash !== claim.bindingMac)
|
|
2618
3764
|
throw new Error("Spawn claim lifecycle mirror binding differs from durable authority.");
|
|
2619
3765
|
}
|
|
3766
|
+
await this.#restoreManagedAttempts();
|
|
3767
|
+
if (this.#managedEnrollmentFailed || this.#managedFailedRoots.size > 0)
|
|
3768
|
+
throw new Error("Broker cannot establish complete managed enrollment membership.");
|
|
2620
3769
|
await this.#recoverSpawnClaims();
|
|
3770
|
+
await this.#reconcileManagedNativeLifetimes();
|
|
3771
|
+
const reconciledRoots = new Set<string>();
|
|
3772
|
+
for (const ref of this.#managedAttempts.values()) {
|
|
3773
|
+
if (this.#managedFailedRoots.has(ref.controlRoot) || reconciledRoots.has(ref.controlRoot)) continue;
|
|
3774
|
+
reconciledRoots.add(ref.controlRoot);
|
|
3775
|
+
try {
|
|
3776
|
+
const binding = await loadManagedDomainBinding(ref.controlRoot);
|
|
3777
|
+
if (binding) await reconcileRunningManagedVerification(binding);
|
|
3778
|
+
} catch {}
|
|
3779
|
+
}
|
|
2621
3780
|
const now = Date.now();
|
|
2622
3781
|
const incarnation = brokerProcessIncarnation(process.pid);
|
|
2623
3782
|
if (!incarnation) throw new Error("Broker process incarnation is unavailable.");
|
|
@@ -3390,7 +4549,9 @@ export class Broker {
|
|
|
3390
4549
|
return error("invalid_input", "session.spawn does not support lifecycle lookup");
|
|
3391
4550
|
if (!LIFECYCLE_OPERATIONS.has(requestedOperation)) return error("not_found", "lifecycle operation was not found");
|
|
3392
4551
|
const identity = await deriveIdempotencyIdentity(this.settings.agentDir, requestedOperation, idempotencyKey);
|
|
3393
|
-
const entry =
|
|
4552
|
+
const entry =
|
|
4553
|
+
this.ledger.get(identity) ??
|
|
4554
|
+
this.ledger.findByOperationKey(`${requestedOperation}\0${idempotencyKey}`, requestedFingerprint);
|
|
3394
4555
|
if (!entry) return error("not_found", "lifecycle operation was not found");
|
|
3395
4556
|
if (entry.fingerprint !== requestedFingerprint)
|
|
3396
4557
|
return error("idempotency_conflict", "lifecycle request fingerprint differs");
|
|
@@ -3436,6 +4597,7 @@ export class Broker {
|
|
|
3436
4597
|
// Spawn bypasses generic raw-input transforms. They must never observe task
|
|
3437
4598
|
// or capability fields, even transiently.
|
|
3438
4599
|
if (operation === "session.spawn") return this.#handleSpawn(input, idempotencyKey);
|
|
4600
|
+
if (operation === "task.dag") return this.#handleManagedTaskDag(input, idempotencyKey);
|
|
3439
4601
|
if (operation === "session.close") {
|
|
3440
4602
|
const spawnClose = await this.#maybeCloseSpawnChild(input);
|
|
3441
4603
|
if (spawnClose) return spawnClose;
|
|
@@ -3450,10 +4612,10 @@ export class Broker {
|
|
|
3450
4612
|
lifecycleFingerprint(lookup.operation, lookup.target),
|
|
3451
4613
|
);
|
|
3452
4614
|
}
|
|
3453
|
-
const fingerprint = lifecycleFingerprint(operation, input);
|
|
3454
4615
|
const normalization = normalizeBrokerInput(operation, input);
|
|
3455
4616
|
if (isBrokerResponse(normalization)) return normalization;
|
|
3456
4617
|
input = normalization.input;
|
|
4618
|
+
const fingerprint = lifecycleFingerprint(operation, input);
|
|
3457
4619
|
if (operation === "session.list") {
|
|
3458
4620
|
if (input.cursor === undefined) await this.index.refresh();
|
|
3459
4621
|
const page = await this.#sessionListPage(input, this.index.listSessions());
|
|
@@ -3523,8 +4685,64 @@ export class Broker {
|
|
|
3523
4685
|
const target = createHash("sha256")
|
|
3524
4686
|
.update(canonicalJson(lifecycleTarget(operation, input)))
|
|
3525
4687
|
.digest("hex");
|
|
3526
|
-
const identity = await deriveIdempotencyIdentity(
|
|
4688
|
+
const identity = await deriveIdempotencyIdentity(
|
|
4689
|
+
this.settings.agentDir,
|
|
4690
|
+
operation,
|
|
4691
|
+
idempotencyKey,
|
|
4692
|
+
operation === "session.create" ? undefined : target,
|
|
4693
|
+
);
|
|
3527
4694
|
const operationKey = `${operation}\0${idempotencyKey}`;
|
|
4695
|
+
const requestedRequestHash = createHash("sha256")
|
|
4696
|
+
.update(canonicalJson({ operation, input: lifecycleRequestIdentity(input) }))
|
|
4697
|
+
.digest("hex");
|
|
4698
|
+
const legacyIdentity = await deriveLegacyIdentity(this.settings.agentDir, operation, idempotencyKey);
|
|
4699
|
+
const legacyTargetIdentity = await deriveLegacyTargetIdentity(
|
|
4700
|
+
this.settings.agentDir,
|
|
4701
|
+
operation,
|
|
4702
|
+
idempotencyKey,
|
|
4703
|
+
target,
|
|
4704
|
+
);
|
|
4705
|
+
const metadata = {
|
|
4706
|
+
operationKey,
|
|
4707
|
+
fingerprint,
|
|
4708
|
+
...(operation === "session.close" && typeof input.sessionId === "string"
|
|
4709
|
+
? { intendedSessionId: input.sessionId }
|
|
4710
|
+
: {}),
|
|
4711
|
+
};
|
|
4712
|
+
if (!this.ledger.get(identity)) {
|
|
4713
|
+
const matchingOperation = this.ledger.findAnyByOperationKey(operationKey);
|
|
4714
|
+
const sameCloseSession =
|
|
4715
|
+
operation === "session.close" &&
|
|
4716
|
+
(matchingOperation?.intendedSessionId === input.sessionId ||
|
|
4717
|
+
matchingOperation?.resultSessionId === input.sessionId);
|
|
4718
|
+
if (
|
|
4719
|
+
matchingOperation &&
|
|
4720
|
+
!sameCloseSession &&
|
|
4721
|
+
(matchingOperation.fingerprint !== fingerprint || matchingOperation.requestHash !== requestedRequestHash)
|
|
4722
|
+
)
|
|
4723
|
+
return error("idempotency_conflict", "idempotency key was used with a different request");
|
|
4724
|
+
const matchingLegacy = matchingOperation;
|
|
4725
|
+
if (matchingLegacy) {
|
|
4726
|
+
if (matchingLegacy.identity === legacyIdentity || matchingLegacy.identity === legacyTargetIdentity)
|
|
4727
|
+
await this.ledger.migrateIdentity(matchingLegacy.identity, identity, metadata);
|
|
4728
|
+
} else if (this.ledger.get(legacyIdentity)) {
|
|
4729
|
+
return error("idempotency_conflict", "idempotency key was used with a different request");
|
|
4730
|
+
} else {
|
|
4731
|
+
const legacyTargetEntry = this.ledger.get(legacyTargetIdentity);
|
|
4732
|
+
if (legacyTargetEntry) {
|
|
4733
|
+
if (legacyTargetEntry.requestHash !== requestedRequestHash)
|
|
4734
|
+
return error("idempotency_conflict", "idempotency key was used with a different request");
|
|
4735
|
+
await this.ledger.migrateIdentity(legacyTargetIdentity, identity, metadata);
|
|
4736
|
+
// Exact target-derived legacy identity migrated without granting new authority.
|
|
4737
|
+
} else if (
|
|
4738
|
+
this.ledger.hasLegacyIdentity(
|
|
4739
|
+
new Set(this.#spawnAuthority?.claims().map(claim => claim.lifecycleIdentity) ?? []),
|
|
4740
|
+
)
|
|
4741
|
+
) {
|
|
4742
|
+
return error("idempotency_conflict", "legacy lifecycle request has an ambiguous target");
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
3528
4746
|
|
|
3529
4747
|
let reconstructedDeleteCleanup: BrokerCleanupEvidence | undefined;
|
|
3530
4748
|
if (operation === "session.delete" && input.cwd === undefined && input.sessionPath === undefined) {
|
|
@@ -3594,7 +4812,7 @@ export class Broker {
|
|
|
3594
4812
|
await prev;
|
|
3595
4813
|
try {
|
|
3596
4814
|
const beforeBegin = this.ledger.get(identity);
|
|
3597
|
-
const begun = await this.ledger.begin(identity, requestHash,
|
|
4815
|
+
const begun = await this.ledger.begin(identity, requestHash, metadata);
|
|
3598
4816
|
if (begun.kind === "replay") {
|
|
3599
4817
|
const replay = begun.entry.response as BrokerResponse;
|
|
3600
4818
|
const cleanup = cleanupFromResponse(replay) ?? reconstructedDeleteCleanup;
|
|
@@ -3710,12 +4928,21 @@ export class Broker {
|
|
|
3710
4928
|
});
|
|
3711
4929
|
if (isCleanupPending(response)) return response;
|
|
3712
4930
|
const persisted = await this.ledger.readTerminal(identity, requestHash);
|
|
3713
|
-
const
|
|
3714
|
-
persisted
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
4931
|
+
const persistenceVerification = verifyTerminalPersistence(
|
|
4932
|
+
persisted,
|
|
4933
|
+
storedResponse,
|
|
4934
|
+
outcome.durableEffects,
|
|
4935
|
+
outcome.startupFailure,
|
|
4936
|
+
);
|
|
4937
|
+
if (persistenceVerification.kind === "unverified") {
|
|
4938
|
+
logger.warn("sdk broker terminal persistence read-back was unavailable; preserving settled outcome", {
|
|
4939
|
+
identity,
|
|
4940
|
+
});
|
|
4941
|
+
} else if (persistenceVerification.kind === "uncertain") {
|
|
4942
|
+
logger.warn("sdk broker terminal persistence verification found conflicting evidence", {
|
|
4943
|
+
identity,
|
|
4944
|
+
mismatches: persistenceVerification.mismatches,
|
|
4945
|
+
});
|
|
3719
4946
|
const uncertain = error(
|
|
3720
4947
|
"terminal_uncertain",
|
|
3721
4948
|
"Lifecycle terminal evidence could not be verified after persistence; retained artifacts require reconciliation.",
|
|
@@ -3762,6 +4989,12 @@ export function setLivenessGraceForTest(broker: Broker, graceMs: number | undefi
|
|
|
3762
4989
|
else livenessGraceOverridesForTest.set(broker, graceMs);
|
|
3763
4990
|
}
|
|
3764
4991
|
|
|
4992
|
+
/** Test-only hook for shortening the bounded wait before a deferred managed close is queued. */
|
|
4993
|
+
export function setManagedCloseWaitForTest(broker: Broker, waitMs: number | undefined): void {
|
|
4994
|
+
if (waitMs === undefined) managedCloseWaitOverridesForTest.delete(broker);
|
|
4995
|
+
else managedCloseWaitOverridesForTest.set(broker, Math.max(0, waitMs));
|
|
4996
|
+
}
|
|
4997
|
+
|
|
3765
4998
|
/**
|
|
3766
4999
|
* Test-only hook for stalling the heartbeat write, reproducing a publication tick
|
|
3767
5000
|
* whose awaited IO does not settle. Clearing it releases the stalled tick the way
|