@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
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
type VerifiedSessionDeleteResult,
|
|
53
53
|
type VerifiedSessionDeleteTarget,
|
|
54
54
|
} from "../../session/session-storage";
|
|
55
|
+
import type { SessionWorkLease } from "../../session/session-work-lease";
|
|
55
56
|
import type { SessionLifecycleMcpServer } from "../acp/mcp";
|
|
56
57
|
import { SdkClient, SdkClientError } from "../client/client";
|
|
57
58
|
import { BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD } from "../host/control/runtime-gate";
|
|
@@ -63,6 +64,7 @@ import {
|
|
|
63
64
|
resolveManagedSessionScope,
|
|
64
65
|
} from "../session-directory";
|
|
65
66
|
import {
|
|
67
|
+
lifecycleKnownSecrets,
|
|
66
68
|
normalizeSdkStartupFailure,
|
|
67
69
|
type SdkStartupFailure,
|
|
68
70
|
type SdkStartupRollbackResult,
|
|
@@ -87,6 +89,7 @@ import {
|
|
|
87
89
|
} from "./process-incarnation";
|
|
88
90
|
import { resolveSdkInternalSpawnCommand, type SdkInternalSpawnCommand } from "./runtime";
|
|
89
91
|
import {
|
|
92
|
+
DEAD_HOST_HEARTBEAT_SILENCE_MS,
|
|
90
93
|
type IndexedSession,
|
|
91
94
|
isSessionAuthorityEligible,
|
|
92
95
|
resolveSessionLocator,
|
|
@@ -119,15 +122,308 @@ const MAX_EFFECT_MARKER_LENGTH = 128;
|
|
|
119
122
|
const MAX_PROCESS_INCARNATION_LENGTH = 256;
|
|
120
123
|
const DEAD_LIFECYCLE_MARKER_EXPIRY_MS = 60 * 60 * 1000;
|
|
121
124
|
const READY_THEN_EXIT_MESSAGE = "became ready then exited before live admission";
|
|
125
|
+
/** Keep detached host stderr diagnostic-only and bounded on disk. */
|
|
126
|
+
const CHILD_STDERR_TAIL_BYTES = 512;
|
|
127
|
+
const LIFECYCLE_DIAGNOSTIC_MARKER = "[lifecycle-diagnostic-v1]";
|
|
128
|
+
|
|
129
|
+
type ChildStderrCapture = {
|
|
130
|
+
tail(): string;
|
|
131
|
+
exitCode(): number | null | undefined;
|
|
132
|
+
signalCode(): NodeJS.Signals | null | undefined;
|
|
133
|
+
flush(): Promise<void>;
|
|
134
|
+
cleanup(): Promise<void>;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
type ChildStderrSink = {
|
|
138
|
+
path: string;
|
|
139
|
+
drainer: ChildProcess;
|
|
140
|
+
drainExited: Promise<void>;
|
|
141
|
+
parentReleased: boolean;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const CHILD_STDERR_LOG_NAME = /^lifecycle-spawn\.[A-Za-z0-9-]+\.log$/u;
|
|
145
|
+
const CHILD_STDERR_ORPHAN_EXPIRY_MS = 60 * 60 * 1000;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Detached hosts outlive the broker, so stderr must not use a broker-owned
|
|
149
|
+
* pipe. A detached drainer owns the read side and writes only the bounded tail
|
|
150
|
+
* to disk, keeping both the host and the artifact safe across broker restarts.
|
|
151
|
+
*/
|
|
152
|
+
function openChildStderrSink(agentDir: string): ChildStderrSink | undefined {
|
|
153
|
+
// Bun on Windows cannot hand a pipe stream without an underlying fd to a
|
|
154
|
+
// child's stdio ("Passing a stream.Readable without an underlying file
|
|
155
|
+
// descriptor as stdio[2] is not yet implemented"), which would turn every
|
|
156
|
+
// launch into spawn_failed. Windows keeps stdio ignored; the stage/child
|
|
157
|
+
// diagnostics still apply, only the stderr tail reads as `none`.
|
|
158
|
+
if (process.platform === "win32") return undefined;
|
|
159
|
+
let logPath: string | undefined;
|
|
160
|
+
try {
|
|
161
|
+
const directory = path.join(agentDir, "sdk");
|
|
162
|
+
fsSync.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
163
|
+
logPath = path.join(directory, `lifecycle-spawn.${randomUUID()}.log`);
|
|
164
|
+
const logDescriptor = fsSync.openSync(logPath, "w", 0o600);
|
|
165
|
+
fsSync.closeSync(logDescriptor);
|
|
166
|
+
const command = resolveSdkInternalSpawnCommand("stderr-drain-internal");
|
|
167
|
+
const drainer = spawn(
|
|
168
|
+
command.file,
|
|
169
|
+
[...command.args, "--path", logPath, "--max-bytes", String(CHILD_STDERR_TAIL_BYTES)],
|
|
170
|
+
{
|
|
171
|
+
...(command.cwd === undefined ? {} : { cwd: command.cwd }),
|
|
172
|
+
detached: true,
|
|
173
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
174
|
+
env: command.env,
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
if (!drainer.stdin) throw new Error("lifecycle stderr drainer has no stdin");
|
|
178
|
+
const drainExit = Promise.withResolvers<void>();
|
|
179
|
+
const resolveDrainExit = (): void => drainExit.resolve();
|
|
180
|
+
drainer.once("exit", resolveDrainExit);
|
|
181
|
+
drainer.once("error", resolveDrainExit);
|
|
182
|
+
drainer.unref();
|
|
183
|
+
void reapOrphanedChildStderrLogs(agentDir).catch(() => undefined);
|
|
184
|
+
return { path: logPath, drainer, drainExited: drainExit.promise, parentReleased: false };
|
|
185
|
+
} catch {
|
|
186
|
+
if (logPath) {
|
|
187
|
+
try {
|
|
188
|
+
fsSync.unlinkSync(logPath);
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function closeChildStderrSink(sink: ChildStderrSink | undefined): void {
|
|
196
|
+
if (!sink || sink.parentReleased) return;
|
|
197
|
+
sink.parentReleased = true;
|
|
198
|
+
// `end()` closes only the broker's duplicate write end after the child has
|
|
199
|
+
// inherited its own descriptor. `destroy()` can close the shared descriptor
|
|
200
|
+
// before a fast child starts writing under Bun's stream implementation.
|
|
201
|
+
sink.drainer.stdin?.end();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function abortChildStderrSink(sink: ChildStderrSink | undefined): Promise<void> {
|
|
205
|
+
if (!sink) return;
|
|
206
|
+
closeChildStderrSink(sink);
|
|
207
|
+
if (sink.drainer.exitCode === null) sink.drainer.kill();
|
|
208
|
+
await Promise.race([sink.drainExited, Bun.sleep(100)]);
|
|
209
|
+
await fs.unlink(sink.path).catch(() => undefined);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function readChildStderrTail(logPath: string): string {
|
|
213
|
+
let descriptor: number | undefined;
|
|
214
|
+
try {
|
|
215
|
+
descriptor = fsSync.openSync(logPath, fsSync.constants.O_RDONLY);
|
|
216
|
+
const stat = fsSync.fstatSync(descriptor);
|
|
217
|
+
if (!stat.isFile() || stat.size <= 0) return "";
|
|
218
|
+
const length = Math.min(CHILD_STDERR_TAIL_BYTES, stat.size);
|
|
219
|
+
const buffer = Buffer.alloc(length);
|
|
220
|
+
fsSync.readSync(descriptor, buffer, 0, length, stat.size - length);
|
|
221
|
+
return buffer.toString("utf8");
|
|
222
|
+
} catch {
|
|
223
|
+
return "";
|
|
224
|
+
} finally {
|
|
225
|
+
if (descriptor !== undefined) {
|
|
226
|
+
try {
|
|
227
|
+
fsSync.closeSync(descriptor);
|
|
228
|
+
} catch {
|
|
229
|
+
// Best effort diagnostic read.
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function captureChildStderr(child: ChildProcess, sink: ChildStderrSink | undefined): ChildStderrCapture {
|
|
236
|
+
let observedExitCode: number | null | undefined;
|
|
237
|
+
let observedSignalCode: NodeJS.Signals | null | undefined;
|
|
238
|
+
child.once("exit", (code, signal) => {
|
|
239
|
+
observedExitCode = code;
|
|
240
|
+
observedSignalCode = signal;
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
tail: () => (sink ? readChildStderrTail(sink.path) : ""),
|
|
244
|
+
exitCode: () => observedExitCode,
|
|
245
|
+
signalCode: () => observedSignalCode,
|
|
246
|
+
flush: async () => {
|
|
247
|
+
if (
|
|
248
|
+
child.exitCode === null &&
|
|
249
|
+
child.signalCode === null &&
|
|
250
|
+
observedExitCode === undefined &&
|
|
251
|
+
observedSignalCode === undefined
|
|
252
|
+
)
|
|
253
|
+
return;
|
|
254
|
+
if (sink) await Promise.race([sink.drainExited, Bun.sleep(250)]);
|
|
255
|
+
},
|
|
256
|
+
cleanup: async () => {
|
|
257
|
+
if (!sink) return;
|
|
258
|
+
closeChildStderrSink(sink);
|
|
259
|
+
// The drainer writes through a descriptor it already holds, so unlinking
|
|
260
|
+
// here retires the artifact for successful and failed launches alike: any
|
|
261
|
+
// later host output lands on an unlinked inode and dies with the drainer.
|
|
262
|
+
await fs.unlink(sink.path).catch(() => undefined);
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Remove lifecycle stderr artifacts abandoned by a broker or detached host crash. */
|
|
268
|
+
export async function reapOrphanedChildStderrLogs(
|
|
269
|
+
agentDir: string,
|
|
270
|
+
limit = BROKER_DEAD_REGISTRATION_SWEEP_LIMIT,
|
|
271
|
+
): Promise<number> {
|
|
272
|
+
let directory: fsSync.Dir;
|
|
273
|
+
try {
|
|
274
|
+
directory = await fs.opendir(path.join(agentDir, "sdk"));
|
|
275
|
+
} catch {
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
const cutoff = Date.now() - CHILD_STDERR_ORPHAN_EXPIRY_MS;
|
|
279
|
+
let inspected = 0;
|
|
280
|
+
let reaped = 0;
|
|
281
|
+
for await (const entry of directory) {
|
|
282
|
+
if (inspected >= Math.max(0, limit)) break;
|
|
283
|
+
inspected += 1;
|
|
284
|
+
if (!entry.isFile() || !CHILD_STDERR_LOG_NAME.test(entry.name)) continue;
|
|
285
|
+
try {
|
|
286
|
+
const target = path.join(agentDir, "sdk", entry.name);
|
|
287
|
+
const stat = await fs.stat(target);
|
|
288
|
+
if (stat.mtimeMs >= cutoff) continue;
|
|
289
|
+
await fs.unlink(target);
|
|
290
|
+
reaped += 1;
|
|
291
|
+
} catch {
|
|
292
|
+
// A concurrent drainer or another broker may own the artifact.
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return reaped;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function settleChildExitObservation(child: ChildProcess, stderr?: ChildStderrCapture): Promise<void> {
|
|
299
|
+
const exitCode = stderr?.exitCode();
|
|
300
|
+
const signalCode = stderr?.signalCode();
|
|
301
|
+
if (
|
|
302
|
+
child.exitCode !== null ||
|
|
303
|
+
child.signalCode !== null ||
|
|
304
|
+
(exitCode !== null && exitCode !== undefined) ||
|
|
305
|
+
(signalCode !== null && signalCode !== undefined)
|
|
306
|
+
)
|
|
307
|
+
return;
|
|
308
|
+
const exited = Promise.withResolvers<void>();
|
|
309
|
+
child.once("exit", () => exited.resolve());
|
|
310
|
+
await Promise.race([exited.promise, Bun.sleep(100)]);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
type ChildLifecycleStatus = {
|
|
314
|
+
state: "alive" | "exited" | "uncertain";
|
|
315
|
+
pid?: number;
|
|
316
|
+
exitCode: number | null;
|
|
317
|
+
signalCode: NodeJS.Signals | null;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
function childLifecycleStatus(
|
|
321
|
+
broker: Broker,
|
|
322
|
+
child: ChildProcess,
|
|
323
|
+
authority: EffectMarker | undefined,
|
|
324
|
+
stderrCapture?: ChildStderrCapture,
|
|
325
|
+
): ChildLifecycleStatus {
|
|
326
|
+
const exitCode = child.exitCode ?? stderrCapture?.exitCode() ?? null;
|
|
327
|
+
const signalCode = child.signalCode ?? stderrCapture?.signalCode() ?? null;
|
|
328
|
+
if (exitCode !== null || signalCode !== null)
|
|
329
|
+
return { state: "exited", ...(child.pid === undefined ? {} : { pid: child.pid }), exitCode, signalCode };
|
|
330
|
+
if (authority) {
|
|
331
|
+
const observation = observeProcess(authority.pid, authority.incarnation, value =>
|
|
332
|
+
processIncarnationForBroker(broker, value),
|
|
333
|
+
);
|
|
334
|
+
return {
|
|
335
|
+
state: observation,
|
|
336
|
+
pid: authority.pid,
|
|
337
|
+
exitCode,
|
|
338
|
+
signalCode,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
state: "uncertain",
|
|
343
|
+
...(child.pid === undefined ? {} : { pid: child.pid }),
|
|
344
|
+
exitCode,
|
|
345
|
+
signalCode,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* The drainer keeps only the last `CHILD_STDERR_TAIL_BYTES`, so a credential
|
|
351
|
+
* that straddles the cutoff (or exceeds the tail) survives as a bare suffix that
|
|
352
|
+
* `sanitizeSdkStartupMessage` can no longer match exactly. Any tail prefix that
|
|
353
|
+
* is a suffix of a known secret is therefore redacted before sanitization.
|
|
354
|
+
*/
|
|
355
|
+
function redactTruncatedSecretPrefix(tail: string, knownSecrets: Iterable<unknown>): string {
|
|
356
|
+
if (!tail) return tail;
|
|
357
|
+
const leadingReplacement = tail.startsWith("\uFFFD") ? 1 : 0;
|
|
358
|
+
const body = tail.slice(leadingReplacement);
|
|
359
|
+
// A one- or two-character overlap is as likely to be coincidence as a cut
|
|
360
|
+
// credential and reveals nothing usable; keep the diagnostic readable there.
|
|
361
|
+
const minimumOverlap = 3;
|
|
362
|
+
let longestMatch = 0;
|
|
363
|
+
for (const secret of knownSecrets) {
|
|
364
|
+
if (typeof secret !== "string" || secret.length < minimumOverlap) continue;
|
|
365
|
+
const max = Math.min(secret.length, body.length);
|
|
366
|
+
for (let length = max; length > Math.max(longestMatch, minimumOverlap - 1); length--) {
|
|
367
|
+
if (body.startsWith(secret.slice(secret.length - length))) {
|
|
368
|
+
longestMatch = length;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return longestMatch === 0 ? tail : `[redacted-secret]${body.slice(longestMatch)}`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function lifecycleChildDiagnostic(
|
|
377
|
+
stage: string,
|
|
378
|
+
waitingFor: string,
|
|
379
|
+
status: ChildLifecycleStatus,
|
|
380
|
+
stderr: ChildStderrCapture | undefined,
|
|
381
|
+
cause?: string,
|
|
382
|
+
knownSecrets: Iterable<unknown> = lifecycleKnownSecrets(),
|
|
383
|
+
): string {
|
|
384
|
+
const parts = [LIFECYCLE_DIAGNOSTIC_MARKER, `stage=${stage}`, `waiting_for=${waitingFor}`, `child=${status.state}`];
|
|
385
|
+
if (status.pid !== undefined) parts.push(`pid=${status.pid}`);
|
|
386
|
+
if (status.state === "exited") {
|
|
387
|
+
parts.push(`exit=${status.exitCode ?? "null"}`, `signal=${status.signalCode ?? "none"}`);
|
|
388
|
+
} else {
|
|
389
|
+
parts.push("exit=not_observed", "signal=not_observed");
|
|
390
|
+
}
|
|
391
|
+
const rawStderr = redactTruncatedSecretPrefix(stderr?.tail() ?? "", knownSecrets).trim();
|
|
392
|
+
if (rawStderr) parts.push(`stderr=${JSON.stringify(sanitizeSdkStartupMessage(rawStderr, knownSecrets))}`);
|
|
393
|
+
else parts.push("stderr=none");
|
|
394
|
+
if (cause) parts.push(`cause=${sanitizeSdkStartupMessage(cause, knownSecrets)}`);
|
|
395
|
+
return parts.join(" ");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function validateLifecycleExecutable(file: string): void {
|
|
399
|
+
// Bun's Node-compatible spawn can emit an ENOENT error before the returned
|
|
400
|
+
// ChildProcess has an opportunity to receive an `error` listener. Validate
|
|
401
|
+
// absolute launch paths first so a missing or non-executable runtime becomes a
|
|
402
|
+
// normal broker response instead of an uncaught broker exception. Bare names
|
|
403
|
+
// still resolve through PATH at spawn time.
|
|
404
|
+
if (!path.isAbsolute(file)) return;
|
|
405
|
+
const stat = fsSync.statSync(file);
|
|
406
|
+
if (!stat.isFile()) throw new Error(`Lifecycle runtime executable is not a regular file: ${file}`);
|
|
407
|
+
fsSync.accessSync(file, fsSync.constants.X_OK);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function lifecycleChildExitDiagnostic(child?: ChildProcess): string | undefined {
|
|
411
|
+
if (!child) return undefined;
|
|
412
|
+
const parts: string[] = [];
|
|
413
|
+
if (child.exitCode !== null && child.exitCode !== 0) parts.push(`exit=${child.exitCode}`);
|
|
414
|
+
if (child.signalCode) parts.push(`signal=${child.signalCode}`);
|
|
415
|
+
return parts.length > 0 ? parts.join(", ") : undefined;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function lifecycleFailureMessage(message: string, child?: ChildProcess): string {
|
|
419
|
+
const details = [lifecycleChildExitDiagnostic(child)].filter((value): value is string => value !== undefined);
|
|
420
|
+
return details.length > 0 ? `${message} (${details.join("; ")})` : message;
|
|
421
|
+
}
|
|
122
422
|
|
|
123
423
|
function readyThenExitedResponse(id: string, child?: ChildProcess): BrokerResponse {
|
|
124
424
|
const parts = [`Session ${id} ${READY_THEN_EXIT_MESSAGE}.`];
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
// Host stderr is deliberately excluded: the detached host receives launch
|
|
128
|
-
// configuration and inherited credentials, and no pattern-based redaction can
|
|
129
|
-
// prove arbitrary child output free of that material; its stderr is discarded
|
|
130
|
-
// by the OS and never captured (#4712 review).
|
|
425
|
+
const diagnostic = lifecycleFailureMessage("", child).trim();
|
|
426
|
+
if (diagnostic) parts.push(diagnostic);
|
|
131
427
|
return fail("ready_then_exited", parts.join(" "));
|
|
132
428
|
}
|
|
133
429
|
|
|
@@ -135,20 +431,34 @@ const STARTUP_CLEANUP_UNCERTAIN_MESSAGE =
|
|
|
135
431
|
"Lifecycle startup cleanup could not be proven; retained artifacts require reconciliation.";
|
|
136
432
|
|
|
137
433
|
export function terminalUncertainStartupMessage(response: BrokerResponse): string {
|
|
138
|
-
if (response.ok
|
|
434
|
+
if (response.ok) return STARTUP_CLEANUP_UNCERTAIN_MESSAGE;
|
|
435
|
+
// Ordinary spawn errors may contain executable paths; keep the historical
|
|
436
|
+
// path-free fallback for those. Lifecycle diagnostics are deliberately
|
|
437
|
+
// recognizable and already bounded/redacted, so preserve their stage/cause
|
|
438
|
+
// fields even when cleanup proof is the part that failed.
|
|
439
|
+
const original = response.error.message.includes(LIFECYCLE_DIAGNOSTIC_MARKER)
|
|
440
|
+
? sanitizeSdkStartupMessage(response.error.message)
|
|
441
|
+
: "SDK internal process could not be started.";
|
|
139
442
|
logger.warn("sdk broker retained a sanitized launch failure after uncertain startup cleanup", {
|
|
140
|
-
message:
|
|
443
|
+
message: original,
|
|
141
444
|
});
|
|
142
|
-
return `${STARTUP_CLEANUP_UNCERTAIN_MESSAGE} Original launch failure:
|
|
445
|
+
return `${STARTUP_CLEANUP_UNCERTAIN_MESSAGE} Original launch failure: ${original}`;
|
|
143
446
|
}
|
|
144
447
|
|
|
145
448
|
export async function waitForChildSpawn(
|
|
146
|
-
spawned: Pick<ChildProcess, "off" | "on" | "once">,
|
|
449
|
+
spawned: Pick<ChildProcess, "off" | "on" | "once" | "pid">,
|
|
147
450
|
onPostSpawnError: (error: Error) => void = error =>
|
|
148
451
|
logger.warn("sdk session child emitted an error after successful spawn", {
|
|
149
452
|
message: sanitizeSdkStartupMessage(error),
|
|
150
453
|
}),
|
|
151
454
|
): Promise<void> {
|
|
455
|
+
// Bun may publish the `spawn` event synchronously for native executables.
|
|
456
|
+
// A populated PID is already the same ownership fact the event conveys, so
|
|
457
|
+
// do not wait forever for an event that was emitted before this listener ran.
|
|
458
|
+
if (spawned.pid !== undefined) {
|
|
459
|
+
spawned.on("error", onPostSpawnError);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
152
462
|
const spawnOutcome = Promise.withResolvers<void>();
|
|
153
463
|
const onSpawn = () => {
|
|
154
464
|
spawned.off("error", onError);
|
|
@@ -618,6 +928,33 @@ type SessionLaunch = {
|
|
|
618
928
|
worktreePlan?: GjcLaunchWorktreePlan;
|
|
619
929
|
};
|
|
620
930
|
|
|
931
|
+
const CREDENTIAL_NAME_PATTERN = /(?:token|secret|password|credential|api[_-]?key|auth|cookie)/iu;
|
|
932
|
+
|
|
933
|
+
function lifecycleLaunchKnownSecrets(launch: SessionLaunch): string[] {
|
|
934
|
+
const secrets = [
|
|
935
|
+
...lifecycleKnownSecrets(),
|
|
936
|
+
...(launch.coordinatorSidecarSigningKey ? [launch.coordinatorSidecarSigningKey] : []),
|
|
937
|
+
...(launch.mcpServers ?? []).flatMap(server => {
|
|
938
|
+
if ("url" in server) {
|
|
939
|
+
// Only credential-bearing headers are secrets. Treating every header
|
|
940
|
+
// value (`X-Tenant: a`) as one would redact ordinary characters out of
|
|
941
|
+
// the diagnostic this path exists to expose.
|
|
942
|
+
return Object.entries(server.headers ?? {}).flatMap(([name, value]) => {
|
|
943
|
+
const scheme = /^(?:Bearer|Basic)\s+/iu;
|
|
944
|
+
if (!CREDENTIAL_NAME_PATTERN.test(name) && !scheme.test(value)) return [];
|
|
945
|
+
const bare = value.replace(scheme, "");
|
|
946
|
+
return bare === value ? [value] : [value, bare];
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
if (!server.env) return [];
|
|
950
|
+
return Object.entries(server.env)
|
|
951
|
+
.filter(([name, value]) => value && CREDENTIAL_NAME_PATTERN.test(name))
|
|
952
|
+
.map(([, value]) => value);
|
|
953
|
+
}),
|
|
954
|
+
];
|
|
955
|
+
return [...new Set(secrets.filter(secret => typeof secret === "string" && secret.length > 0))];
|
|
956
|
+
}
|
|
957
|
+
|
|
621
958
|
type CleanupEvidence = BrokerCleanupEvidence;
|
|
622
959
|
type CleanupIdentity = {
|
|
623
960
|
dev: bigint;
|
|
@@ -1141,7 +1478,7 @@ type ReadinessResult =
|
|
|
1141
1478
|
| { kind: "ready_then_exited" }
|
|
1142
1479
|
| { kind: "ready_probe_failed"; probe: ReadyAuthorityProbe }
|
|
1143
1480
|
| { kind: "child_exited" }
|
|
1144
|
-
| { kind: "timeout" };
|
|
1481
|
+
| { kind: "timeout"; stage: "readiness"; waitingFor: string };
|
|
1145
1482
|
type BrokerIndex = Pick<Broker, "index">;
|
|
1146
1483
|
const processIncarnationReadersForTest = new WeakMap<BrokerIndex, (pid: number) => string | undefined>();
|
|
1147
1484
|
|
|
@@ -2449,6 +2786,43 @@ async function executeUncertainRetirement(
|
|
|
2449
2786
|
return retirementProof(retirementIdentity, receipt.indexSeq);
|
|
2450
2787
|
}
|
|
2451
2788
|
|
|
2789
|
+
/**
|
|
2790
|
+
* Fence a coordinator's dead-host cleanup against the broker's current index
|
|
2791
|
+
* authority. This is deliberately separate from ordinary `session.close`: the
|
|
2792
|
+
* runtime endpoint is already known to be unavailable, so the broker must make
|
|
2793
|
+
* the exact generation terminal under its index lock before coordinator state is
|
|
2794
|
+
* removed. A fresh heartbeat, successor, ambiguous row, or terminal-uncertain
|
|
2795
|
+
* authority refuses the fence.
|
|
2796
|
+
*/
|
|
2797
|
+
async function executeStaleSessionRetirement(broker: Broker, input: Input): Promise<BrokerResponse> {
|
|
2798
|
+
const id = sessionId(input);
|
|
2799
|
+
if (!id) return fail("invalid_input", "sessionId is required.");
|
|
2800
|
+
if (!isCanonicalSessionId(id)) return fail("invalid_input", "sessionId must be a canonical safe identifier.");
|
|
2801
|
+
const requestedAuthority = requestedCloseAuthority(input);
|
|
2802
|
+
if ("error" in requestedAuthority) return requestedAuthority.error;
|
|
2803
|
+
if (!requestedAuthority.authority)
|
|
2804
|
+
return fail("invalid_input", "stale session retirement requires endpoint authority.");
|
|
2805
|
+
const cwd = lifecycleCwd(input);
|
|
2806
|
+
if (!cwd) return fail("invalid_input", "stale session retirement requires cwd.");
|
|
2807
|
+
const result = await broker.index.retireStaleIfCurrent({
|
|
2808
|
+
sessionId: id,
|
|
2809
|
+
workspace: cwd,
|
|
2810
|
+
endpointGeneration: requestedAuthority.authority.endpointGeneration,
|
|
2811
|
+
endpointIncarnation: requestedAuthority.authority.endpointIncarnation,
|
|
2812
|
+
});
|
|
2813
|
+
if (result === "not_found") return fail("not_found", "session is not indexed");
|
|
2814
|
+
if (result === "stale") return fail("endpoint_stale", "session endpoint authority changed or is unresolved.");
|
|
2815
|
+
return {
|
|
2816
|
+
ok: true,
|
|
2817
|
+
result: {
|
|
2818
|
+
sessionId: id,
|
|
2819
|
+
retired: true,
|
|
2820
|
+
...(result === "already_retired" ? { alreadyRetired: true } : {}),
|
|
2821
|
+
heartbeatSilenceMs: DEAD_HOST_HEARTBEAT_SILENCE_MS,
|
|
2822
|
+
},
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2452
2826
|
function validateLifecycleCleanupFile(root: string, id: string, file: LifecycleCleanupFile): boolean {
|
|
2453
2827
|
const directory = path.join(path.resolve(root), "sdk");
|
|
2454
2828
|
const original = path.resolve(file.path);
|
|
@@ -3722,6 +4096,52 @@ async function terminateSpawnedChild(
|
|
|
3722
4096
|
failure.artifact.rollback.brokerRegistrationReleased;
|
|
3723
4097
|
if (!rollbackComplete) {
|
|
3724
4098
|
if (!readyThenExitToleranceEnabled()) {
|
|
4099
|
+
// A child that exited with a non-zero status before publishing any
|
|
4100
|
+
// lifecycle receipt has no durable rollback to wait for. Prove the
|
|
4101
|
+
// owned artifacts are gone and release its registration, but retain
|
|
4102
|
+
// terminal uncertainty for timeouts, clean exits, and child-authored
|
|
4103
|
+
// startup receipts whose rollback is incomplete.
|
|
4104
|
+
if (failure || child.exitCode === null || child.exitCode === 0) return failClosed();
|
|
4105
|
+
if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
|
|
4106
|
+
const publishedReady = (await probePublishedReadyAuthority(root, id, expected)).kind === "matched";
|
|
4107
|
+
if (publishedReady) return failClosed();
|
|
4108
|
+
const artifactsRemoved = await removeOwnedLifecycleArtifacts(
|
|
4109
|
+
root,
|
|
4110
|
+
id,
|
|
4111
|
+
expected,
|
|
4112
|
+
undefined,
|
|
4113
|
+
undefined,
|
|
4114
|
+
proofBudget,
|
|
4115
|
+
);
|
|
4116
|
+
if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
|
|
4117
|
+
await broker.index.refresh();
|
|
4118
|
+
if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
|
|
4119
|
+
const registered = broker.index
|
|
4120
|
+
.listSessions()
|
|
4121
|
+
.sessions.find(session => session.sessionId === id && session.pid === pid);
|
|
4122
|
+
let registrationReleased =
|
|
4123
|
+
!broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker) ||
|
|
4124
|
+
registered?.terminal === true ||
|
|
4125
|
+
registered?.terminalUncertain === true;
|
|
4126
|
+
if (registered && !registrationReleased) {
|
|
4127
|
+
registrationReleased = await broker.index.unregisterIfCurrent(registered);
|
|
4128
|
+
if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
|
|
4129
|
+
await broker.index.refresh();
|
|
4130
|
+
registrationReleased =
|
|
4131
|
+
registrationReleased || !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker);
|
|
4132
|
+
}
|
|
4133
|
+
const stillExited =
|
|
4134
|
+
observeProcess(pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) ===
|
|
4135
|
+
"exited";
|
|
4136
|
+
const endpointGone = await endpointRemoved(root, id);
|
|
4137
|
+
if (
|
|
4138
|
+
stillExited &&
|
|
4139
|
+
artifactsRemoved &&
|
|
4140
|
+
endpointGone &&
|
|
4141
|
+
registrationReleased &&
|
|
4142
|
+
lifecycleProofWithinDeadline(proofBudget)
|
|
4143
|
+
)
|
|
4144
|
+
return true;
|
|
3725
4145
|
return failClosed();
|
|
3726
4146
|
}
|
|
3727
4147
|
if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
|
|
@@ -4216,6 +4636,7 @@ async function waitForReady(
|
|
|
4216
4636
|
deadline: number,
|
|
4217
4637
|
expected: EffectMarker,
|
|
4218
4638
|
timing: LifecycleTiming,
|
|
4639
|
+
child?: ChildProcess,
|
|
4219
4640
|
signal: "session_ready" | typeof SESSION_PREPARED_EVENT = "session_ready",
|
|
4220
4641
|
): Promise<ReadinessResult> {
|
|
4221
4642
|
const classifyExitedAfterReady = async (): Promise<ReadinessResult> => {
|
|
@@ -4238,10 +4659,11 @@ async function waitForReady(
|
|
|
4238
4659
|
}
|
|
4239
4660
|
return { kind: "startup_failed", failure: startupFailure };
|
|
4240
4661
|
}
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
)
|
|
4662
|
+
const childExited = child !== undefined && (child.exitCode !== null || child.signalCode !== null);
|
|
4663
|
+
const processObservation = observeProcess(expected.pid, expected.incarnation, value =>
|
|
4664
|
+
processIncarnationForBroker(broker, value),
|
|
4665
|
+
);
|
|
4666
|
+
if (childExited || processObservation === "exited") {
|
|
4245
4667
|
const finalStartupFailure = await readSessionLifecycleFailure(root, id, expected);
|
|
4246
4668
|
if (finalStartupFailure) {
|
|
4247
4669
|
const afterReady = await classifyExitedAfterReady();
|
|
@@ -4306,7 +4728,7 @@ async function waitForReady(
|
|
|
4306
4728
|
const remaining = deadline - timing.now();
|
|
4307
4729
|
if (remaining > 0) await timing.sleep(Math.min(POLL_MS, remaining));
|
|
4308
4730
|
}
|
|
4309
|
-
return { kind: "timeout" };
|
|
4731
|
+
return { kind: "timeout", stage: "readiness", waitingFor: signal };
|
|
4310
4732
|
}
|
|
4311
4733
|
|
|
4312
4734
|
function worktreeIntent(plan: GjcLaunchWorktreePlan | undefined): LifecycleWorktreeIntent | undefined {
|
|
@@ -5091,8 +5513,20 @@ async function executeLifecycleResponse(
|
|
|
5091
5513
|
return fail("invalid_input", "sourceSessionId must be a canonical safe identifier.");
|
|
5092
5514
|
if (operation === "session.create" || operation === "session.fork" || operation === "session.resume") {
|
|
5093
5515
|
await broker.index.refresh();
|
|
5094
|
-
|
|
5095
|
-
|
|
5516
|
+
if (operation === "session.create") {
|
|
5517
|
+
// Creation has no existing session authority to reconcile. Keep the
|
|
5518
|
+
// broker's periodic/startup heartbeat checkpoint as the reaping owner,
|
|
5519
|
+
// but do not make a new session wait behind an O(hosts) process-table
|
|
5520
|
+
// pass on the shared agent directory.
|
|
5521
|
+
void broker.heartbeatSessions().catch(error => {
|
|
5522
|
+
logger.warn(`sdk broker: deferred create heartbeat checkpoint failed: ${String(error)}`);
|
|
5523
|
+
});
|
|
5524
|
+
} else {
|
|
5525
|
+
await broker.heartbeatSessions();
|
|
5526
|
+
// The heartbeat mutates the durable index; resume/fork must re-read its
|
|
5527
|
+
// authority before validating the source session below.
|
|
5528
|
+
await broker.index.refresh();
|
|
5529
|
+
}
|
|
5096
5530
|
if (operation === "session.resume") {
|
|
5097
5531
|
const requestedSessionId = sessionId(input);
|
|
5098
5532
|
const authority = requestedSessionId
|
|
@@ -5420,20 +5854,26 @@ async function executeLifecycleResponse(
|
|
|
5420
5854
|
...(launch.coordinatorSessionBranch ? { coordinatorSessionBranch: launch.coordinatorSessionBranch } : {}),
|
|
5421
5855
|
};
|
|
5422
5856
|
let child: ChildProcess | undefined;
|
|
5857
|
+
let childStderr: ChildStderrCapture | undefined;
|
|
5423
5858
|
let spawnedAuthority: EffectMarker | undefined;
|
|
5424
5859
|
let childSpawned = false;
|
|
5860
|
+
const knownSecrets = lifecycleLaunchKnownSecrets(launch);
|
|
5861
|
+
const childStderrSink = openChildStderrSink(broker.settings.agentDir);
|
|
5862
|
+
// Runs once every diagnostic that reads the stderr tail has been composed.
|
|
5863
|
+
const cleanupChildStderr = async (): Promise<void> => {
|
|
5864
|
+
if (childStderr) await childStderr.cleanup();
|
|
5865
|
+
else await abortChildStderrSink(childStderrSink);
|
|
5866
|
+
};
|
|
5425
5867
|
try {
|
|
5426
5868
|
const authorizedSpawn = broker.runSynchronousEffectWithFreshPublicationAuthority(() => {
|
|
5427
5869
|
const cmd = command(broker);
|
|
5870
|
+
validateLifecycleExecutable(cmd.file);
|
|
5428
5871
|
return spawn(cmd.file, cmd.args, {
|
|
5429
5872
|
cwd: launch.cwd,
|
|
5430
5873
|
detached: true,
|
|
5431
|
-
//
|
|
5432
|
-
//
|
|
5433
|
-
|
|
5434
|
-
// through restart/shutdown). Host stderr is therefore discarded
|
|
5435
|
-
// by the OS rather than captured (#4712 review).
|
|
5436
|
-
stdio: "ignore",
|
|
5874
|
+
// The detached drainer owns the read side, so the host's stderr remains
|
|
5875
|
+
// usable after this broker exits while the artifact stays bounded.
|
|
5876
|
+
stdio: ["ignore", "ignore", childStderrSink?.drainer.stdin ?? "ignore"],
|
|
5437
5877
|
env: {
|
|
5438
5878
|
// Master capability is process-local to direct Bash children and must
|
|
5439
5879
|
// never cross a broker lifecycle launch boundary.
|
|
@@ -5479,6 +5919,7 @@ async function executeLifecycleResponse(
|
|
|
5479
5919
|
});
|
|
5480
5920
|
});
|
|
5481
5921
|
if (!authorizedSpawn.authorized) {
|
|
5922
|
+
await cleanupChildStderr();
|
|
5482
5923
|
return fail(
|
|
5483
5924
|
"startup_admission_refused",
|
|
5484
5925
|
"SDK host startup was refused because the broker no longer owns the session root.",
|
|
@@ -5486,6 +5927,8 @@ async function executeLifecycleResponse(
|
|
|
5486
5927
|
}
|
|
5487
5928
|
const spawned = authorizedSpawn.value;
|
|
5488
5929
|
child = spawned;
|
|
5930
|
+
childStderr = captureChildStderr(spawned, childStderrSink);
|
|
5931
|
+
closeChildStderrSink(childStderrSink);
|
|
5489
5932
|
await waitForChildSpawn(spawned);
|
|
5490
5933
|
childSpawned = true;
|
|
5491
5934
|
const pid = spawned.pid;
|
|
@@ -5499,6 +5942,7 @@ async function executeLifecycleResponse(
|
|
|
5499
5942
|
await writeEffectMarker(launch.root, launch.id, spawnedAuthority);
|
|
5500
5943
|
spawned.unref();
|
|
5501
5944
|
} catch (error) {
|
|
5945
|
+
closeChildStderrSink(childStderrSink);
|
|
5502
5946
|
const terminated =
|
|
5503
5947
|
child && childSpawned
|
|
5504
5948
|
? await terminateSpawnedChild(
|
|
@@ -5513,139 +5957,231 @@ async function executeLifecycleResponse(
|
|
|
5513
5957
|
)
|
|
5514
5958
|
: true;
|
|
5515
5959
|
|
|
5516
|
-
|
|
5517
|
-
? fail(
|
|
5960
|
+
const response = terminated
|
|
5961
|
+
? fail(
|
|
5962
|
+
"spawn_failed",
|
|
5963
|
+
lifecycleFailureMessage(
|
|
5964
|
+
`Unable to spawn session: ${sanitizeSdkStartupMessage(error, knownSecrets)}`,
|
|
5965
|
+
child,
|
|
5966
|
+
),
|
|
5967
|
+
)
|
|
5518
5968
|
: fail(
|
|
5519
5969
|
"terminal_uncertain",
|
|
5520
|
-
|
|
5970
|
+
lifecycleFailureMessage(
|
|
5971
|
+
`Unable to establish spawned-session ownership and could not prove the child dead: ${sanitizeSdkStartupMessage(error, knownSecrets)}`,
|
|
5972
|
+
child,
|
|
5973
|
+
),
|
|
5521
5974
|
);
|
|
5975
|
+
await cleanupChildStderr();
|
|
5976
|
+
return response;
|
|
5522
5977
|
}
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
launch.id,
|
|
5530
|
-
launch.root,
|
|
5531
|
-
readinessDeadline,
|
|
5532
|
-
spawnedAuthority,
|
|
5533
|
-
timing,
|
|
5534
|
-
launch.readiness === "deferred" ? SESSION_PREPARED_EVENT : "session_ready",
|
|
5535
|
-
);
|
|
5536
|
-
|
|
5537
|
-
if (readiness.kind !== "ready") {
|
|
5538
|
-
const terminated = await terminateSpawnedChild(
|
|
5539
|
-
child,
|
|
5978
|
+
try {
|
|
5979
|
+
if (!child || !spawnedAuthority) {
|
|
5980
|
+
return fail("spawn_failed", "Unable to retain the spawned session process identity.");
|
|
5981
|
+
}
|
|
5982
|
+
await broker.ledger.transition(identity, "awaiting_ready", { intendedSessionId: launch.id, effectMarker });
|
|
5983
|
+
const readiness = await waitForReady(
|
|
5540
5984
|
broker,
|
|
5541
5985
|
launch.id,
|
|
5542
5986
|
launch.root,
|
|
5543
|
-
|
|
5544
|
-
terminationStartDeadline,
|
|
5987
|
+
readinessDeadline,
|
|
5545
5988
|
spawnedAuthority,
|
|
5546
5989
|
timing,
|
|
5990
|
+
child,
|
|
5991
|
+
launch.readiness === "deferred" ? SESSION_PREPARED_EVENT : "session_ready",
|
|
5547
5992
|
);
|
|
5548
5993
|
|
|
5549
|
-
if (
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5994
|
+
if (readiness.kind !== "ready") {
|
|
5995
|
+
if (readiness.kind !== "timeout") await settleChildExitObservation(child, childStderr);
|
|
5996
|
+
if (readiness.kind !== "timeout") await childStderr?.flush();
|
|
5997
|
+
const statusAtFailure = childLifecycleStatus(broker, child, spawnedAuthority, childStderr);
|
|
5998
|
+
const diagnostic =
|
|
5999
|
+
readiness.kind === "startup_failed"
|
|
6000
|
+
? lifecycleChildDiagnostic(
|
|
6001
|
+
readiness.failure.phase,
|
|
6002
|
+
"startup completion",
|
|
6003
|
+
statusAtFailure,
|
|
6004
|
+
childStderr,
|
|
6005
|
+
readiness.failure.message,
|
|
6006
|
+
knownSecrets,
|
|
6007
|
+
)
|
|
6008
|
+
: readiness.kind === "timeout"
|
|
6009
|
+
? lifecycleChildDiagnostic(
|
|
6010
|
+
readiness.stage,
|
|
6011
|
+
readiness.waitingFor,
|
|
6012
|
+
statusAtFailure,
|
|
6013
|
+
childStderr,
|
|
6014
|
+
undefined,
|
|
6015
|
+
knownSecrets,
|
|
6016
|
+
)
|
|
6017
|
+
: readiness.kind === "ready_probe_failed"
|
|
6018
|
+
? lifecycleChildDiagnostic(
|
|
6019
|
+
"readiness",
|
|
6020
|
+
"session_ready",
|
|
6021
|
+
statusAtFailure,
|
|
6022
|
+
childStderr,
|
|
6023
|
+
undefined,
|
|
6024
|
+
knownSecrets,
|
|
6025
|
+
)
|
|
6026
|
+
: readiness.kind === "child_exited"
|
|
6027
|
+
? lifecycleChildDiagnostic(
|
|
6028
|
+
"readiness",
|
|
6029
|
+
"session_ready",
|
|
6030
|
+
statusAtFailure,
|
|
6031
|
+
childStderr,
|
|
6032
|
+
undefined,
|
|
6033
|
+
knownSecrets,
|
|
6034
|
+
)
|
|
6035
|
+
: undefined;
|
|
6036
|
+
const terminated = await terminateSpawnedChild(
|
|
6037
|
+
child,
|
|
6038
|
+
broker,
|
|
6039
|
+
launch.id,
|
|
6040
|
+
launch.root,
|
|
6041
|
+
lifecycleDeadline,
|
|
6042
|
+
terminationStartDeadline,
|
|
6043
|
+
spawnedAuthority,
|
|
6044
|
+
timing,
|
|
6045
|
+
);
|
|
6046
|
+
|
|
6047
|
+
if (!terminated)
|
|
6048
|
+
return readiness.kind === "ready_probe_failed"
|
|
5573
6049
|
? fail(
|
|
5574
6050
|
"endpoint_unreadable",
|
|
5575
|
-
`Session ${launch.id} exited, and its readiness could not be determined: ${describeReadyProbeFailure(readiness.probe)}
|
|
6051
|
+
`Session ${launch.id} exited, and its readiness could not be determined: ${describeReadyProbeFailure(readiness.probe)}; cleanup could not be proven. ${diagnostic ?? "stage=readiness waiting_for=session_ready child=uncertain exit=not_observed signal=not_observed stderr=none"}`,
|
|
5576
6052
|
)
|
|
5577
|
-
:
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
6053
|
+
: fail(
|
|
6054
|
+
"terminal_uncertain",
|
|
6055
|
+
`Session ${launch.id} did not become ready and its spawned process could not be verified dead. ${diagnostic ?? "stage=readiness waiting_for=session_ready child=uncertain exit=not_observed signal=not_observed stderr=none"}`,
|
|
6056
|
+
);
|
|
6057
|
+
return readiness.kind === "startup_failed"
|
|
6058
|
+
? fail(
|
|
6059
|
+
readiness.failure.code ?? "spawn_failed",
|
|
6060
|
+
// The child-side normalizer only knows process-scoped secrets; launch-scoped
|
|
6061
|
+
// ones (sidecar key, MCP headers) must be redacted here before the message
|
|
6062
|
+
// leaves the broker.
|
|
6063
|
+
`${sanitizeSdkStartupMessage(readiness.failure.message, knownSecrets)} ${diagnostic ?? "stage=startup waiting_for=startup completion child=uncertain exit=not_observed signal=not_observed stderr=none"}`,
|
|
6064
|
+
undefined,
|
|
6065
|
+
readiness.failure.details,
|
|
6066
|
+
)
|
|
6067
|
+
: readiness.kind === "ready_then_exited"
|
|
6068
|
+
? readyThenExitedResponse(launch.id, child)
|
|
6069
|
+
: readiness.kind === "ready_probe_failed"
|
|
6070
|
+
? fail(
|
|
6071
|
+
"endpoint_unreadable",
|
|
6072
|
+
`Session ${launch.id} exited, and its readiness could not be determined: ${describeReadyProbeFailure(readiness.probe)} ${diagnostic}.`,
|
|
6073
|
+
)
|
|
6074
|
+
: readiness.kind === "child_exited"
|
|
6075
|
+
? fail(
|
|
6076
|
+
"spawn_failed",
|
|
6077
|
+
`Session ${launch.id} exited before registering readiness. ${diagnostic}.`,
|
|
6078
|
+
)
|
|
6079
|
+
: fail(
|
|
6080
|
+
"readiness_timeout",
|
|
6081
|
+
`Session ${launch.id} did not register an endpoint before the readiness timeout. ${diagnostic}.`,
|
|
6082
|
+
);
|
|
6083
|
+
}
|
|
6084
|
+
await reconcileReadyScope(broker, launch.id, launch.cwd, launch.root, spawnedAuthority);
|
|
6085
|
+
const verified = await currentReadyAuthority(broker, launch.id, launch.root, spawnedAuthority);
|
|
6086
|
+
if (!verified || !sameReadyAuthority(readiness.authority, verified)) {
|
|
6087
|
+
const exited =
|
|
6088
|
+
child.exitCode !== null ||
|
|
6089
|
+
observeProcess(spawnedAuthority.pid, spawnedAuthority.incarnation, value =>
|
|
6090
|
+
processIncarnationForBroker(broker, value),
|
|
6091
|
+
) === "exited";
|
|
6092
|
+
const terminated = await terminateSpawnedChild(
|
|
6093
|
+
child,
|
|
6094
|
+
broker,
|
|
6095
|
+
launch.id,
|
|
6096
|
+
launch.root,
|
|
6097
|
+
lifecycleDeadline,
|
|
6098
|
+
terminationStartDeadline,
|
|
6099
|
+
spawnedAuthority,
|
|
6100
|
+
timing,
|
|
6101
|
+
);
|
|
6102
|
+
if (exited && readyThenExitToleranceEnabled())
|
|
6103
|
+
return terminated
|
|
6104
|
+
? readyThenExitedResponse(launch.id, child)
|
|
6105
|
+
: fail(
|
|
6106
|
+
"terminal_uncertain",
|
|
6107
|
+
`Session ${launch.id} ${READY_THEN_EXIT_MESSAGE} and cleanup could not be proven.`,
|
|
6108
|
+
);
|
|
5603
6109
|
return terminated
|
|
5604
|
-
?
|
|
6110
|
+
? fail("endpoint_stale", "Session endpoint changed while lifecycle readiness was being verified.")
|
|
5605
6111
|
: fail(
|
|
5606
6112
|
"terminal_uncertain",
|
|
5607
|
-
|
|
6113
|
+
"Session readiness authority changed and its spawned process could not be verified dead.",
|
|
5608
6114
|
);
|
|
6115
|
+
}
|
|
6116
|
+
if (!spawnedAuthority) {
|
|
6117
|
+
return fail("endpoint_stale", "Session endpoint authority is unavailable.");
|
|
6118
|
+
}
|
|
6119
|
+
const replayIncarnation = endpointIncarnation(
|
|
6120
|
+
{
|
|
6121
|
+
endpointGeneration: verified.endpointGeneration,
|
|
6122
|
+
endpointMtimeMs: verified.endpointMtimeMs,
|
|
6123
|
+
pid: spawnedAuthority.pid,
|
|
6124
|
+
...(verified.endpointFileId === undefined ? {} : { endpointFileId: verified.endpointFileId }),
|
|
6125
|
+
},
|
|
6126
|
+
launch.id,
|
|
6127
|
+
);
|
|
6128
|
+
if (!replayIncarnation) {
|
|
6129
|
+
return fail("endpoint_stale", "Session endpoint incarnation is unavailable.");
|
|
6130
|
+
}
|
|
6131
|
+
return {
|
|
6132
|
+
ok: true,
|
|
6133
|
+
result: {
|
|
6134
|
+
sessionId: launch.id,
|
|
6135
|
+
cwd: launch.cwd,
|
|
6136
|
+
endpointGeneration: verified.endpointGeneration,
|
|
6137
|
+
endpointIncarnation: replayIncarnation,
|
|
6138
|
+
pid: verified.endpoint.pid,
|
|
6139
|
+
endpointMtimeMs: verified.endpointMtimeMs,
|
|
6140
|
+
endpoint: verified.endpoint,
|
|
6141
|
+
// Public, ledger-replayable evidence of the bootstrap authority that
|
|
6142
|
+
// reached this runtime. The private key never enters the response.
|
|
6143
|
+
...(launch.coordinatorSidecarKeyId ? { coordinatorSidecarKeyId: launch.coordinatorSidecarKeyId } : {}),
|
|
6144
|
+
...(launch.readiness === "deferred" ? { readiness: "prepared" as const } : {}),
|
|
6145
|
+
...(worktreeReceipt ? { worktree: worktreeReceipt } : {}),
|
|
6146
|
+
},
|
|
6147
|
+
};
|
|
6148
|
+
} catch (error) {
|
|
6149
|
+
let terminated = false;
|
|
6150
|
+
if (child && childSpawned) {
|
|
6151
|
+
try {
|
|
6152
|
+
terminated = await terminateSpawnedChild(
|
|
6153
|
+
child,
|
|
6154
|
+
broker,
|
|
6155
|
+
launch.id,
|
|
6156
|
+
launch.root,
|
|
6157
|
+
lifecycleDeadline,
|
|
6158
|
+
terminationStartDeadline,
|
|
6159
|
+
spawnedAuthority,
|
|
6160
|
+
timing,
|
|
6161
|
+
);
|
|
6162
|
+
} catch {
|
|
6163
|
+
terminated = false;
|
|
6164
|
+
}
|
|
6165
|
+
}
|
|
5609
6166
|
return terminated
|
|
5610
|
-
? fail(
|
|
6167
|
+
? fail(
|
|
6168
|
+
"spawn_failed",
|
|
6169
|
+
`Unable to complete lifecycle startup: ${error instanceof Error ? error.message : String(error)}`,
|
|
6170
|
+
)
|
|
5611
6171
|
: fail(
|
|
5612
6172
|
"terminal_uncertain",
|
|
5613
|
-
|
|
6173
|
+
`Unable to complete lifecycle startup and could not prove the child dead: ${error instanceof Error ? error.message : String(error)}`,
|
|
5614
6174
|
);
|
|
6175
|
+
} finally {
|
|
6176
|
+
await cleanupChildStderr();
|
|
5615
6177
|
}
|
|
5616
|
-
if (!spawnedAuthority) return fail("endpoint_stale", "Session endpoint authority is unavailable.");
|
|
5617
|
-
const replayIncarnation = endpointIncarnation(
|
|
5618
|
-
{
|
|
5619
|
-
endpointGeneration: verified.endpointGeneration,
|
|
5620
|
-
endpointMtimeMs: verified.endpointMtimeMs,
|
|
5621
|
-
pid: spawnedAuthority.pid,
|
|
5622
|
-
...(verified.endpointFileId === undefined ? {} : { endpointFileId: verified.endpointFileId }),
|
|
5623
|
-
},
|
|
5624
|
-
launch.id,
|
|
5625
|
-
);
|
|
5626
|
-
if (!replayIncarnation) return fail("endpoint_stale", "Session endpoint incarnation is unavailable.");
|
|
5627
|
-
return {
|
|
5628
|
-
ok: true,
|
|
5629
|
-
result: {
|
|
5630
|
-
sessionId: launch.id,
|
|
5631
|
-
cwd: launch.cwd,
|
|
5632
|
-
endpointGeneration: verified.endpointGeneration,
|
|
5633
|
-
endpointIncarnation: replayIncarnation,
|
|
5634
|
-
pid: verified.endpoint.pid,
|
|
5635
|
-
endpointMtimeMs: verified.endpointMtimeMs,
|
|
5636
|
-
endpoint: verified.endpoint,
|
|
5637
|
-
// Public, ledger-replayable evidence of the bootstrap authority that
|
|
5638
|
-
// reached this runtime. The private key never enters the response.
|
|
5639
|
-
...(launch.coordinatorSidecarKeyId ? { coordinatorSidecarKeyId: launch.coordinatorSidecarKeyId } : {}),
|
|
5640
|
-
...(launch.readiness === "deferred" ? { readiness: "prepared" as const } : {}),
|
|
5641
|
-
...(worktreeReceipt ? { worktree: worktreeReceipt } : {}),
|
|
5642
|
-
},
|
|
5643
|
-
};
|
|
5644
6178
|
}
|
|
5645
6179
|
|
|
5646
6180
|
const id = cleanup && operation === "session.delete" ? cleanup.sessionId : sessionId(input);
|
|
5647
6181
|
if (!id) return fail("invalid_input", "sessionId is required.");
|
|
5648
6182
|
if (!isCanonicalSessionId(id)) return fail("invalid_input", "sessionId must be a canonical safe identifier.");
|
|
6183
|
+
if (operation === "session.close" && input.forceRetireStale === true)
|
|
6184
|
+
return await executeStaleSessionRetirement(broker, input);
|
|
5649
6185
|
await broker.index.refresh();
|
|
5650
6186
|
let record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
5651
6187
|
if (operation === "session.close") {
|
|
@@ -6854,8 +7390,9 @@ export async function executeLifecycle(
|
|
|
6854
7390
|
}
|
|
6855
7391
|
|
|
6856
7392
|
/**
|
|
6857
|
-
* The live
|
|
6858
|
-
*
|
|
7393
|
+
* The live SDK demand count of the session endpoint this process serves, or
|
|
7394
|
+
* `undefined` while this process serves no endpoint. Observer-only sockets are
|
|
7395
|
+
* excluded by {@link SessionHostRuntimeEvidence.observerClients}.
|
|
6859
7396
|
*
|
|
6860
7397
|
* Only the SDK session runtime owns the real socket table, so it publishes a
|
|
6861
7398
|
* reader here instead of every consumer re-deriving attachment from the OS.
|
|
@@ -6867,8 +7404,10 @@ export type SessionHostAttachmentReader = () => number;
|
|
|
6867
7404
|
export interface SessionHostRuntimeEvidence {
|
|
6868
7405
|
/** This runtime's own live SDK client/socket subscription count. */
|
|
6869
7406
|
attachedClients: SessionHostAttachmentReader;
|
|
6870
|
-
/**
|
|
6871
|
-
|
|
7407
|
+
/** Optional subset of attached sockets that only observes the host. */
|
|
7408
|
+
observerClients?: SessionHostAttachmentReader;
|
|
7409
|
+
/** One authoritative lease held by every admitted or continuing work state. */
|
|
7410
|
+
workLease: SessionWorkLease;
|
|
6872
7411
|
}
|
|
6873
7412
|
|
|
6874
7413
|
/** One runtime's live publication, retractable only by its owner. */
|
|
@@ -6907,14 +7446,14 @@ export function publishSessionHostRuntimeEvidence(evidence: SessionHostRuntimeEv
|
|
|
6907
7446
|
}
|
|
6908
7447
|
|
|
6909
7448
|
/**
|
|
6910
|
-
* This process's currently
|
|
7449
|
+
* This process's currently demanding SDK client count, summed across every
|
|
6911
7450
|
* serving runtime, or `undefined` when no runtime publishes a readable count
|
|
6912
7451
|
* (before startup, after teardown, or when every reader itself fails). Never
|
|
6913
7452
|
* guesses: absence of a reader is absence of evidence.
|
|
6914
7453
|
*/
|
|
6915
7454
|
export function sessionHostAttachedClients(): number | undefined {
|
|
6916
7455
|
let total: number | undefined;
|
|
6917
|
-
for (const { attachedClients } of sessionHostRuntimes) {
|
|
7456
|
+
for (const { attachedClients, observerClients } of sessionHostRuntimes) {
|
|
6918
7457
|
let count: number;
|
|
6919
7458
|
try {
|
|
6920
7459
|
count = attachedClients();
|
|
@@ -6922,21 +7461,31 @@ export function sessionHostAttachedClients(): number | undefined {
|
|
|
6922
7461
|
continue;
|
|
6923
7462
|
}
|
|
6924
7463
|
if (!Number.isSafeInteger(count) || count < 0) continue;
|
|
6925
|
-
|
|
7464
|
+
let observers = 0;
|
|
7465
|
+
if (observerClients) {
|
|
7466
|
+
try {
|
|
7467
|
+
const observed = observerClients();
|
|
7468
|
+
if (Number.isSafeInteger(observed) && observed >= 0) observers = Math.min(count, observed);
|
|
7469
|
+
} catch {
|
|
7470
|
+
// If the observer subset is unavailable, retain the full attachment
|
|
7471
|
+
// count. Losing positive demand evidence is fail-closed.
|
|
7472
|
+
}
|
|
7473
|
+
}
|
|
7474
|
+
total = (total ?? 0) + count - observers;
|
|
6926
7475
|
}
|
|
6927
7476
|
return total;
|
|
6928
7477
|
}
|
|
6929
7478
|
|
|
6930
7479
|
/**
|
|
6931
|
-
* Whether any runtime in this process
|
|
7480
|
+
* Whether any runtime in this process still holds its authoritative work lease.
|
|
6932
7481
|
*
|
|
6933
7482
|
* Positive evidence only: a runtime that publishes nothing, or whose reader
|
|
6934
7483
|
* fails, reports no work, so this can never keep an abandoned host alive.
|
|
6935
7484
|
*/
|
|
6936
|
-
export function
|
|
6937
|
-
for (const {
|
|
7485
|
+
export function sessionHostWorkLeaseActive(): boolean {
|
|
7486
|
+
for (const { workLease } of sessionHostRuntimes) {
|
|
6938
7487
|
try {
|
|
6939
|
-
if (
|
|
7488
|
+
if (workLease.isHeld()) return true;
|
|
6940
7489
|
} catch {}
|
|
6941
7490
|
}
|
|
6942
7491
|
return false;
|
|
@@ -7001,7 +7550,7 @@ export async function reapDeadSessionRegistrations(
|
|
|
7001
7550
|
.slice(0, Math.max(0, limit));
|
|
7002
7551
|
const reaped: ReapedSessionRegistration[] = [];
|
|
7003
7552
|
for (const session of dead) {
|
|
7004
|
-
if (!(await broker.index.unregisterIfCurrent(session))) continue;
|
|
7553
|
+
if (!(await broker.index.unregisterIfCurrent(session, "process_exited"))) continue;
|
|
7005
7554
|
const record = {
|
|
7006
7555
|
sessionId: session.sessionId,
|
|
7007
7556
|
pid: session.pid,
|