@gajae-code/coding-agent 0.17.1 → 0.17.4
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 +266 -0
- package/dist/types/async/job-manager.d.ts +7 -6
- 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 +24 -0
- package/dist/types/config/file-lock.d.ts +10 -1
- package/dist/types/config/model-profile-activation.d.ts +63 -4
- package/dist/types/config/model-registry.d.ts +20 -2
- package/dist/types/config/settings-schema.d.ts +11 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
- package/dist/types/coordinator-mcp/server.d.ts +3 -0
- package/dist/types/daemon/operator-contract.d.ts +5 -1
- package/dist/types/eval/js/worker-protocol.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/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/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 +22 -0
- package/dist/types/modes/acp/acp-agent.d.ts +94 -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/thinking-selector.d.ts +2 -1
- 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/manager.d.ts +56 -0
- 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/acp/adapter.d.ts +27 -0
- package/dist/types/sdk/acp/final-text.d.ts +8 -0
- package/dist/types/sdk/broker/broker.d.ts +13 -2
- package/dist/types/sdk/broker/ensure.d.ts +46 -1
- package/dist/types/sdk/broker/identity.d.ts +3 -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/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 +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 +3 -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 +83 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +38 -1
- package/dist/types/sdk/prompt-failure.d.ts +30 -1
- package/dist/types/sdk/prompt-status.d.ts +8 -0
- package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
- package/dist/types/sdk/router/replay-retention-gap-cache.d.ts +13 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- 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 +73 -2
- package/dist/types/sdk/transport/stdio.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +168 -21
- 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/thinking.d.ts +3 -1
- package/dist/types/tools/bash.d.ts +37 -11
- package/dist/types/tools/job.d.ts +1 -1
- package/dist/types/tools/steer-fold.d.ts +31 -0
- package/dist/types/utils/git.d.ts +28 -2
- package/dist/types/utils/mupdf-embedded.d.ts +2 -0
- package/dist/types/utils/mupdf-wasm-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 +60 -9
- 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 +535 -0
- package/src/cli/session-picker.ts +2 -1
- package/src/cli/setup-cli.ts +9 -1
- package/src/cli/skills-cli.ts +46 -2
- package/src/cli.ts +61 -1
- package/src/commands/daemon.ts +65 -69
- package/src/commands/sdk.ts +232 -100
- package/src/commands/setup.ts +2 -0
- package/src/commands/skills.ts +11 -0
- package/src/config/autorouting-tier-map.ts +16 -0
- package/src/config/file-lock.ts +199 -68
- package/src/config/model-profile-activation.ts +406 -53
- package/src/config/model-registry.ts +204 -25
- 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 +175 -8
- package/src/coordinator-mcp/server.ts +528 -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/debug/report-bundle.ts +114 -24
- 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/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/runtime-skill-discovery.ts +61 -3
- package/src/extensibility/shared-events.ts +3 -1
- package/src/gjc-runtime/managed-owner-admission.ts +67 -50
- package/src/gjc-runtime/session-state-lock.ts +165 -24
- package/src/gjc-runtime/ultragoal-succession.ts +31 -3
- package/src/internal-urls/docs-index.generated.ts +21 -19
- 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 +123 -55
- package/src/modes/acp/acp-agent.ts +593 -47
- package/src/modes/acp/acp-event-mapper.ts +9 -0
- package/src/modes/acp/startup-provenance.ts +59 -0
- package/src/modes/components/custom-provider-wizard.ts +308 -7
- package/src/modes/components/model-selector.ts +31 -15
- package/src/modes/components/oauth-selector.ts +9 -0
- package/src/modes/components/session-selector.ts +47 -4
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/components/thinking-selector.ts +7 -3
- 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/selector-controller.ts +177 -18
- 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/manager.ts +689 -55
- 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 +90 -9
- package/src/sdk/acp/final-text.ts +19 -2
- package/src/sdk/broker/broker.ts +378 -52
- package/src/sdk/broker/ensure.ts +97 -19
- package/src/sdk/broker/identity.ts +9 -1
- package/src/sdk/broker/lifecycle-ledger.ts +95 -31
- package/src/sdk/broker/lifecycle.ts +690 -141
- package/src/sdk/broker/runtime.ts +1 -1
- package/src/sdk/broker/session-index.ts +97 -4
- package/src/sdk/broker/spawn-authority.ts +35 -7
- package/src/sdk/broker/stderr-drainer.ts +59 -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 +721 -84
- package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -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 +687 -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 +636 -9
- package/src/sdk/host/session-runtime.ts +499 -118
- 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/prompt-deadline-flush.ts +334 -0
- package/src/sdk/prompt-deadline-manager.ts +136 -4
- package/src/sdk/prompt-failure.ts +43 -1
- package/src/sdk/prompt-status.ts +8 -0
- package/src/sdk/prompt-tool-boundary.ts +121 -0
- package/src/sdk/protocol/operation-inventory.generated.json +106 -0
- package/src/sdk/router/replay-retention-gap-cache.ts +27 -0
- package/src/sdk/router/session-router.ts +31 -4
- package/src/sdk/session.ts +194 -16
- package/src/sdk/startup-capability.ts +1 -1
- package/src/sdk/transport/relay.ts +71 -6
- package/src/sdk/transport/serve-cli.ts +494 -49
- package/src/sdk/transport/socket.ts +12 -11
- package/src/sdk/transport/stdio.ts +7 -3
- package/src/session/agent-session.ts +2437 -400
- 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 +58 -18
- package/src/session/internal/managed-session-storage.ts +304 -11
- package/src/session/session-manager.ts +382 -44
- package/src/session/session-work-lease.ts +34 -0
- package/src/session-import/codex.ts +12 -3
- package/src/setup/model-onboarding-guidance.ts +1 -1
- package/src/setup/provider-onboarding.ts +397 -24
- package/src/slash-commands/builtin-registry.ts +258 -14
- package/src/task/index.ts +14 -2
- package/src/thinking.ts +21 -2
- package/src/tools/bash.ts +98 -78
- package/src/tools/fetch.ts +119 -20
- 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/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-wasm-embedded.ts +3 -0
- package/src/utils/mupdf-wasm.ts +24 -2
- package/src/utils/mupdf.ts +129 -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/bus/index.ts
CHANGED
|
@@ -50,7 +50,12 @@ type NotificationServer = NativeNotificationServer;
|
|
|
50
50
|
import { $credentialEnv, logger, postmortem, VERSION } from "@gajae-code/utils";
|
|
51
51
|
|
|
52
52
|
import { AsyncJobManager } from "../../async";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
resolveSdkPromptDeadlineMs,
|
|
55
|
+
resolveSdkPromptMaxRuntimeMs,
|
|
56
|
+
Settings,
|
|
57
|
+
validateSettingPatch,
|
|
58
|
+
} from "../../config/settings";
|
|
54
59
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
|
|
55
60
|
import { INTERACTIVE_SELECTOR_RESUME_ORIGIN } from "../../extensibility/shared-events";
|
|
56
61
|
import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
|
|
@@ -62,6 +67,11 @@ import {
|
|
|
62
67
|
} from "../../modes/shared/agent-wire/workflow-gate-broker";
|
|
63
68
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
64
69
|
import type { ClientBridge } from "../../session/client-bridge";
|
|
70
|
+
import {
|
|
71
|
+
createSessionWorkLease,
|
|
72
|
+
type SessionWorkLease,
|
|
73
|
+
type SessionWorkLeaseHandle,
|
|
74
|
+
} from "../../session/session-work-lease";
|
|
65
75
|
import {
|
|
66
76
|
boundTerminalRetentionState,
|
|
67
77
|
findOwnedRegistrationsForTurn,
|
|
@@ -92,14 +102,14 @@ import { publishSessionHostRuntimeEvidence, type SessionHostRuntimePublication }
|
|
|
92
102
|
import { processIncarnation } from "../broker/process-incarnation";
|
|
93
103
|
import { resolveSessionLocator, SessionIndex, type SessionIndexEvent } from "../broker/session-index";
|
|
94
104
|
import {
|
|
95
|
-
|
|
105
|
+
canDeliverSdkEvent,
|
|
96
106
|
createSdkSurfaceFactory,
|
|
97
107
|
masterAttestationForEffectiveHost,
|
|
98
108
|
reattestMasterSessionIdentity,
|
|
109
|
+
SESSION_HOST_OBSERVER_CAPABILITY,
|
|
99
110
|
type SessionSdkHost,
|
|
100
111
|
SessionSdkSessionRuntime,
|
|
101
112
|
shouldHostSdk,
|
|
102
|
-
TOOL_ACTIVITY_CAPABILITY,
|
|
103
113
|
verifyMasterCapabilityFrame,
|
|
104
114
|
} from "../host";
|
|
105
115
|
import { type AbortScope, type ControlSurface, dispatchControl, TypedControlError } from "../host/control";
|
|
@@ -114,6 +124,7 @@ import {
|
|
|
114
124
|
syntheticModelInputError,
|
|
115
125
|
syntheticNamespaceCollision,
|
|
116
126
|
} from "../model-profile-model";
|
|
127
|
+
import { flushWorktreeOnPromptDeadline } from "../prompt-deadline-flush";
|
|
117
128
|
import {
|
|
118
129
|
createPromptDeadlineLease,
|
|
119
130
|
isAttributableProgressEventType,
|
|
@@ -121,13 +132,21 @@ import {
|
|
|
121
132
|
promptDeadlineAt,
|
|
122
133
|
recordAttributableProgress,
|
|
123
134
|
} from "../prompt-deadline-lease";
|
|
135
|
+
import { runBoundedDeadlineFlush } from "../prompt-deadline-manager";
|
|
124
136
|
import {
|
|
125
137
|
assistantFailureCode,
|
|
126
138
|
failedPromptOutcome,
|
|
127
139
|
formatPromptFailureForLocalLog,
|
|
140
|
+
PROMPT_FAILURE_MESSAGE_DEADLINE,
|
|
128
141
|
sanitizePromptFailure,
|
|
129
142
|
} from "../prompt-failure";
|
|
130
143
|
import { PROMPT_CLIENT_REF_MAX_LENGTH, type SdkPromptTerminalOutcome } from "../prompt-status";
|
|
144
|
+
import {
|
|
145
|
+
IDLE_TOOL_BOUNDARY_RESULT,
|
|
146
|
+
TOOL_CALL_BOUNDARY_GRACE_MS,
|
|
147
|
+
type ToolBoundaryWaitResult,
|
|
148
|
+
waitForToolCallBoundary,
|
|
149
|
+
} from "../prompt-tool-boundary";
|
|
131
150
|
import { OPERATIONS } from "../protocol/operation-registry";
|
|
132
151
|
import {
|
|
133
152
|
lifecycleStartupCapabilityForApi,
|
|
@@ -274,6 +293,20 @@ type PromptTerminalExtra = {
|
|
|
274
293
|
diagnosticAlreadyLogged?: boolean;
|
|
275
294
|
};
|
|
276
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Diagnostic reason for a deadline terminal. The pre-#5637 string is preserved
|
|
298
|
+
* byte-identically whenever the abort landed at a tool boundary (or with no tool
|
|
299
|
+
* running); only a force-termination with tool calls still executing gets its
|
|
300
|
+
* own reason, so the "we killed a tool mid-flight" case stops being
|
|
301
|
+
* indistinguishable from a clean expiry. The failure code, provenance, category
|
|
302
|
+
* and message are untouched either way — downstream retry classification must
|
|
303
|
+
* not move.
|
|
304
|
+
*/
|
|
305
|
+
function promptDeadlineDiagnosticReason(boundary: ToolBoundaryWaitResult): string {
|
|
306
|
+
if (boundary.outcome !== "forced") return PROMPT_FAILURE_MESSAGE_DEADLINE;
|
|
307
|
+
return `${PROMPT_FAILURE_MESSAGE_DEADLINE} Forced after ${TOOL_CALL_BOUNDARY_GRACE_MS}ms with ${boundary.pendingToolCallIds.length} tool call(s) still executing.`;
|
|
308
|
+
}
|
|
309
|
+
|
|
277
310
|
function formatPromptTerminalFailureReason(reason: unknown): string {
|
|
278
311
|
let rawReason: string;
|
|
279
312
|
if (reason instanceof Error) rawReason = reason.message;
|
|
@@ -1266,6 +1299,17 @@ interface SessionRuntime {
|
|
|
1266
1299
|
serverStopped: boolean;
|
|
1267
1300
|
/** This runtime's own host-liveness publication; only its teardown may retract it. */
|
|
1268
1301
|
evidencePublication?: SessionHostRuntimePublication;
|
|
1302
|
+
/** Authoritative lease held by admitted input, execution, and recovery work. */
|
|
1303
|
+
workLease: SessionWorkLease;
|
|
1304
|
+
/** Lease retained while an uncorrelated agent loop is active. */
|
|
1305
|
+
activeWorkLease?: SessionWorkLeaseHandle;
|
|
1306
|
+
/** Lease retained while an agent loop is paused awaiting resumption. */
|
|
1307
|
+
pausedWorkLease?: SessionWorkLeaseHandle;
|
|
1308
|
+
/**
|
|
1309
|
+
* Handles of queued submissions promoted to their own run, handed over at
|
|
1310
|
+
* the promotion boundary and retired by that run's agent_start.
|
|
1311
|
+
*/
|
|
1312
|
+
promotedWorkLeases: SessionWorkLeaseHandle[];
|
|
1269
1313
|
brokerRegistrationReleased: boolean;
|
|
1270
1314
|
verbosity: "lean" | "verbose";
|
|
1271
1315
|
/** Whether the agent loop is currently running (drives the typing indicator). */
|
|
@@ -2474,6 +2518,82 @@ function deepStructuralEqual(left: unknown, right: unknown): boolean {
|
|
|
2474
2518
|
);
|
|
2475
2519
|
}
|
|
2476
2520
|
|
|
2521
|
+
/**
|
|
2522
|
+
* Holds one session work lease handle for a dispatched submission from dispatch
|
|
2523
|
+
* until the submission's own settlement boundary:
|
|
2524
|
+
*
|
|
2525
|
+
* - a queued message (steer, follow-up, or a prompt diverted into steering)
|
|
2526
|
+
* releases when it leaves the session queue: consumed inside the running turn
|
|
2527
|
+
* (`startsOwnRun: false`), removed (`removed: true`), or promoted to its own
|
|
2528
|
+
* run (`startsOwnRun: true`), where the handle is handed to the runtime and
|
|
2529
|
+
* released by that run's `agent_start`;
|
|
2530
|
+
* - an inline prompt releases when its dispatch resolves, which is the end of
|
|
2531
|
+
* its run; the runtime's active-run handle and the accepted submission's
|
|
2532
|
+
* own handle cover the execution in between;
|
|
2533
|
+
* - a failed or never-admitted dispatch releases immediately.
|
|
2534
|
+
*
|
|
2535
|
+
* Every transfer is bound to THIS message's hooks, never to queue position, so
|
|
2536
|
+
* batched or reordered promotions cannot release a different submission's
|
|
2537
|
+
* handle. A message that stays queued keeps its handle: it is admitted work
|
|
2538
|
+
* the host must outlive, and only the session's own removal hook ends it.
|
|
2539
|
+
*/
|
|
2540
|
+
interface AdmissionWorkLease {
|
|
2541
|
+
hooks: {
|
|
2542
|
+
onQueuedPromoted: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
|
|
2543
|
+
onDispatchDisposition: (promotion: { startsOwnRun: boolean }) => void;
|
|
2544
|
+
};
|
|
2545
|
+
/** The session admitted the message (a preflight hook fired). */
|
|
2546
|
+
accepted(): void;
|
|
2547
|
+
/** The dispatch resolves at queue time; the message waits for its promotion hook. */
|
|
2548
|
+
queued(): void;
|
|
2549
|
+
/** The dispatch resolved; releases unless the message is still queued. */
|
|
2550
|
+
dispatched(): void;
|
|
2551
|
+
/** The dispatch failed or was never admitted; nothing is queued. */
|
|
2552
|
+
abandon(): void;
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
function createAdmissionWorkLease(
|
|
2556
|
+
lease: SessionWorkLease | undefined,
|
|
2557
|
+
queuedAtDispatch: boolean,
|
|
2558
|
+
promote: ((handle: SessionWorkLeaseHandle) => void) | undefined,
|
|
2559
|
+
): AdmissionWorkLease {
|
|
2560
|
+
let handle = lease?.acquire();
|
|
2561
|
+
let accepted = false;
|
|
2562
|
+
let queued = queuedAtDispatch;
|
|
2563
|
+
const release = (): void => {
|
|
2564
|
+
handle?.release();
|
|
2565
|
+
handle = undefined;
|
|
2566
|
+
};
|
|
2567
|
+
return {
|
|
2568
|
+
hooks: {
|
|
2569
|
+
onQueuedPromoted: promotion => {
|
|
2570
|
+
const current = handle;
|
|
2571
|
+
if (!current) return;
|
|
2572
|
+
handle = undefined;
|
|
2573
|
+
if (promotion.removed || promotion.startsOwnRun !== true || !promote) current.release();
|
|
2574
|
+
else promote(current);
|
|
2575
|
+
},
|
|
2576
|
+
onDispatchDisposition: promotion => {
|
|
2577
|
+
// The dispatch resolved at queue time, not at run end: the message sits
|
|
2578
|
+
// in the steering queue until its promotion hook fires.
|
|
2579
|
+
if (promotion.startsOwnRun === false) queued = true;
|
|
2580
|
+
},
|
|
2581
|
+
},
|
|
2582
|
+
accepted: () => {
|
|
2583
|
+
accepted = true;
|
|
2584
|
+
},
|
|
2585
|
+
queued: () => {
|
|
2586
|
+
queued = true;
|
|
2587
|
+
},
|
|
2588
|
+
dispatched: () => {
|
|
2589
|
+
// The session send contract invokes a preflight hook before resolving;
|
|
2590
|
+
// an adapter that resolves without admitting anything queued no work.
|
|
2591
|
+
if (!accepted || !queued) release();
|
|
2592
|
+
},
|
|
2593
|
+
abandon: release,
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2477
2597
|
function sdkControlSurface(
|
|
2478
2598
|
ctx: ExtensionContext,
|
|
2479
2599
|
pendingInteractive: Map<string, PendingInteractiveAsk>,
|
|
@@ -2567,6 +2687,10 @@ function sdkControlSurface(
|
|
|
2567
2687
|
// executions and post-response publications) so session teardown can join
|
|
2568
2688
|
// them before reporting durable quiescence.
|
|
2569
2689
|
trackReconciliationProducer?: (producer: Promise<unknown>) => void,
|
|
2690
|
+
sessionWorkLease?: SessionWorkLease,
|
|
2691
|
+
// Hands a queued submission's lease handle to the runtime when that
|
|
2692
|
+
// submission is promoted to its own run; the run's agent_start adopts it.
|
|
2693
|
+
promoteWorkLease?: (handle: SessionWorkLeaseHandle) => void,
|
|
2570
2694
|
): ControlSurface & {
|
|
2571
2695
|
cancelPendingPreflights(): Promise<void>;
|
|
2572
2696
|
cancelPendingPreflightsForConnection(connectionId: string): Promise<void>;
|
|
@@ -2574,6 +2698,7 @@ function sdkControlSurface(
|
|
|
2574
2698
|
const unavailable = (operation: string, reason: string) => () => {
|
|
2575
2699
|
throw Object.assign(new Error(`${operation} is unavailable: ${reason}`), { code: "unavailable" });
|
|
2576
2700
|
};
|
|
2701
|
+
const hostWorkLease = sessionWorkLease ?? ctx.getSessionWorkLease?.();
|
|
2577
2702
|
const bindings = new Set(ctx.sdkBindings?.() ?? []);
|
|
2578
2703
|
const surfacePolicy = createSdkSurfaceFactory({
|
|
2579
2704
|
ctx,
|
|
@@ -2611,23 +2736,58 @@ function sdkControlSurface(
|
|
|
2611
2736
|
return "unknown";
|
|
2612
2737
|
};
|
|
2613
2738
|
const sendSteer = async (text: string, clientRef?: string) => {
|
|
2739
|
+
// A steer is always queued: its handle is held from dispatch until the
|
|
2740
|
+
// message leaves the session queue (consumed in-run, promoted to its own
|
|
2741
|
+
// run, or removed), which is the only boundary bound to THIS message.
|
|
2742
|
+
const admission = createAdmissionWorkLease(hostWorkLease, true, promoteWorkLease);
|
|
2743
|
+
const sendOptions = {
|
|
2744
|
+
deliverAs: "steer" as const,
|
|
2745
|
+
onPreflightAcceptCommit: admission.accepted,
|
|
2746
|
+
onPreflightAccepted: admission.accepted,
|
|
2747
|
+
...admission.hooks,
|
|
2748
|
+
};
|
|
2614
2749
|
if (clientRef === undefined) {
|
|
2615
2750
|
const correlation = { commandId: crypto.randomUUID(), turnId: crypto.randomUUID() };
|
|
2616
|
-
|
|
2751
|
+
try {
|
|
2752
|
+
await api.sendUserMessage(text, sendOptions);
|
|
2753
|
+
admission.dispatched();
|
|
2754
|
+
} catch (error) {
|
|
2755
|
+
admission.abandon();
|
|
2756
|
+
throw error;
|
|
2757
|
+
}
|
|
2617
2758
|
return { ...correlation, accepted: true };
|
|
2618
2759
|
}
|
|
2619
2760
|
const normalizedClientRef = clientRef.trim();
|
|
2620
|
-
if (!skillRecon?.reserveSteer || !skillRecon.settleSteer)
|
|
2761
|
+
if (!skillRecon?.reserveSteer || !skillRecon.settleSteer) {
|
|
2762
|
+
admission.abandon();
|
|
2621
2763
|
throw Object.assign(new Error("Steer reconciliation is unavailable."), { code: "unavailable" });
|
|
2622
|
-
|
|
2623
|
-
|
|
2764
|
+
}
|
|
2765
|
+
let reservation: Awaited<ReturnType<NonNullable<typeof skillRecon.reserveSteer>>>;
|
|
2624
2766
|
try {
|
|
2625
|
-
await
|
|
2767
|
+
reservation = await skillRecon.reserveSteer(normalizedClientRef, text);
|
|
2768
|
+
} catch (error) {
|
|
2769
|
+
admission.abandon();
|
|
2770
|
+
throw error;
|
|
2771
|
+
}
|
|
2772
|
+
if (reservation.replay) {
|
|
2773
|
+
admission.abandon();
|
|
2774
|
+
return { sessionId: ctx.sessionManager.getSessionId(), ...reservation.result };
|
|
2775
|
+
}
|
|
2776
|
+
let dispatched = false;
|
|
2777
|
+
try {
|
|
2778
|
+
await api.sendUserMessage(text, sendOptions);
|
|
2779
|
+
dispatched = true;
|
|
2780
|
+
admission.dispatched();
|
|
2626
2781
|
return {
|
|
2627
2782
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
2628
2783
|
...(await skillRecon.settleSteer(normalizedClientRef, "accepted")),
|
|
2629
2784
|
};
|
|
2630
2785
|
} catch (error) {
|
|
2786
|
+
// Only a failed dispatch leaves no queued message behind. When the
|
|
2787
|
+
// dispatch resolved and the durable settlement failed afterwards, the
|
|
2788
|
+
// steer is still queued in the session, so its handle stays held until
|
|
2789
|
+
// the promotion or removal hook fires.
|
|
2790
|
+
if (!dispatched) admission.abandon();
|
|
2631
2791
|
return {
|
|
2632
2792
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
2633
2793
|
...(await skillRecon.settleSteer(normalizedClientRef, "rejected", error)),
|
|
@@ -2735,17 +2895,28 @@ function sdkControlSurface(
|
|
|
2735
2895
|
throw Object.assign(new Error("clientRef must be a non-empty string of at most 128 characters."), {
|
|
2736
2896
|
code: "invalid_input",
|
|
2737
2897
|
});
|
|
2898
|
+
// An explicit steer/follow-up is queued and settles at its promotion
|
|
2899
|
+
// boundary; a fresh prompt settles when its run resolves. A plain prompt
|
|
2900
|
+
// that finds the loop busy at dispatch is queued as steering (below), and
|
|
2901
|
+
// one the session itself diverts reports that through onDispatchDisposition.
|
|
2902
|
+
const admission = createAdmissionWorkLease(hostWorkLease, deliverAs !== undefined, promoteWorkLease);
|
|
2738
2903
|
if (trackReconciliation) {
|
|
2739
2904
|
// Restart recovery must be committed before a tracked prompt reserves capacity,
|
|
2740
2905
|
// otherwise it can admit against pre-hydration state or a lost clientRef.
|
|
2741
2906
|
try {
|
|
2742
2907
|
await awaitReconciliationReady();
|
|
2743
2908
|
} catch {
|
|
2909
|
+
admission.abandon();
|
|
2744
2910
|
throw Object.assign(new Error("Prompt reconciliation state is unavailable; retry after restart."), {
|
|
2745
2911
|
code: "unavailable",
|
|
2746
2912
|
});
|
|
2747
2913
|
}
|
|
2748
|
-
|
|
2914
|
+
try {
|
|
2915
|
+
admitPrompt(trimmedClientRef);
|
|
2916
|
+
} catch (error) {
|
|
2917
|
+
admission.abandon();
|
|
2918
|
+
throw error;
|
|
2919
|
+
}
|
|
2749
2920
|
}
|
|
2750
2921
|
try {
|
|
2751
2922
|
if (forceFresh && isSessionBusy()) {
|
|
@@ -2764,6 +2935,7 @@ function sdkControlSurface(
|
|
|
2764
2935
|
},
|
|
2765
2936
|
);
|
|
2766
2937
|
} catch (error) {
|
|
2938
|
+
admission.abandon();
|
|
2767
2939
|
if (trackReconciliation) releasePromptAdmission(trimmedClientRef);
|
|
2768
2940
|
throw error;
|
|
2769
2941
|
}
|
|
@@ -2833,12 +3005,14 @@ function sdkControlSurface(
|
|
|
2833
3005
|
);
|
|
2834
3006
|
} catch (error) {
|
|
2835
3007
|
accepting = false;
|
|
3008
|
+
admission.abandon();
|
|
2836
3009
|
// Durable acceptance failed, so the prompt was never accepted: reject the
|
|
2837
3010
|
// control preflight and rethrow so the awaiting session does not execute it.
|
|
2838
3011
|
onPromptAcceptFailed(correlation);
|
|
2839
3012
|
settlePreflight({ status: "rejected", error });
|
|
2840
3013
|
throw error;
|
|
2841
3014
|
}
|
|
3015
|
+
admission.accepted();
|
|
2842
3016
|
accepting = false;
|
|
2843
3017
|
accepted = true;
|
|
2844
3018
|
// #4743: an accepted run owns a durable terminal publication when it
|
|
@@ -2858,18 +3032,22 @@ function sdkControlSurface(
|
|
|
2858
3032
|
};
|
|
2859
3033
|
// Do not acknowledge the prompt until AgentSession's async preflight
|
|
2860
3034
|
// succeeds. The terminal result records correlation before agent_start can fire.
|
|
3035
|
+
const effectiveDeliverAs = deliverAs ?? (!forceFresh && isBusy() ? ("steer" as const) : undefined);
|
|
3036
|
+
if (effectiveDeliverAs !== undefined) admission.queued();
|
|
2861
3037
|
try {
|
|
2862
3038
|
submission = Promise.resolve(
|
|
2863
3039
|
api.sendUserMessage(content, {
|
|
2864
|
-
...(
|
|
3040
|
+
...(effectiveDeliverAs ? { deliverAs: effectiveDeliverAs } : {}),
|
|
2865
3041
|
onPreflightAcceptCommit,
|
|
2866
3042
|
onPreflightAccepted,
|
|
3043
|
+
...admission.hooks,
|
|
2867
3044
|
preflightSignal: preflightController.signal,
|
|
2868
3045
|
...(sdkRunToken ? { sdkRunToken } : {}),
|
|
2869
3046
|
}),
|
|
2870
3047
|
);
|
|
2871
3048
|
} catch (error) {
|
|
2872
3049
|
submissionSettled.resolve();
|
|
3050
|
+
admission.abandon();
|
|
2873
3051
|
if (accepted && !preflightController.signal.aborted)
|
|
2874
3052
|
trackReconciliationProducer?.(Promise.resolve(onPromptFailed(correlation, error)));
|
|
2875
3053
|
else settlePreflight({ status: "rejected", error });
|
|
@@ -2880,6 +3058,7 @@ function sdkControlSurface(
|
|
|
2880
3058
|
// joined — only its reconciliation publications are tracked below.
|
|
2881
3059
|
void submission.then(
|
|
2882
3060
|
() => {
|
|
3061
|
+
admission.dispatched();
|
|
2883
3062
|
if (!accepted)
|
|
2884
3063
|
settlePreflight({
|
|
2885
3064
|
status: "rejected",
|
|
@@ -2890,6 +3069,7 @@ function sdkControlSurface(
|
|
|
2890
3069
|
submissionSettled.resolve();
|
|
2891
3070
|
},
|
|
2892
3071
|
error => {
|
|
3072
|
+
admission.abandon();
|
|
2893
3073
|
if (accepted && !preflightController.signal.aborted)
|
|
2894
3074
|
trackReconciliationProducer?.(Promise.resolve(onPromptFailed(correlation, error)));
|
|
2895
3075
|
else settlePreflight({ status: "rejected", error });
|
|
@@ -2902,6 +3082,7 @@ function sdkControlSurface(
|
|
|
2902
3082
|
if (result.status === "rejected") throw result.error;
|
|
2903
3083
|
return { commandId, turnId, accepted: true, ...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}) };
|
|
2904
3084
|
} catch (error) {
|
|
3085
|
+
admission.abandon();
|
|
2905
3086
|
if (trackReconciliation) releasePromptAdmission(trimmedClientRef);
|
|
2906
3087
|
throw error;
|
|
2907
3088
|
} finally {
|
|
@@ -3425,7 +3606,15 @@ function sdkControlSurface(
|
|
|
3425
3606
|
...(typeof result === "object" && result ? (result as object) : {}),
|
|
3426
3607
|
...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}),
|
|
3427
3608
|
});
|
|
3428
|
-
} else if (durableSkillAccepted && skillRecon) {
|
|
3609
|
+
} else if (durableSkillAccepted && skillRecon && !promptOwned) {
|
|
3610
|
+
// A lifecycle-owned skill (requester connection present, submission
|
|
3611
|
+
// registered by onPromptAccepted) is terminalized solely by the
|
|
3612
|
+
// correlated agent_end handler (terminalizePrompt -> claim ->
|
|
3613
|
+
// finalize with the final text). This promise-settlement transition
|
|
3614
|
+
// carries no text and would pre-empt that finalization, dropping the
|
|
3615
|
+
// durable content; it remains only for ownerless acceptance, where
|
|
3616
|
+
// no lifecycle handler will ever fire — and there it is the sole
|
|
3617
|
+
// terminal publication, so it must carry the returned text itself.
|
|
3429
3618
|
trackReconciliationProducer?.(
|
|
3430
3619
|
skillRecon.noteTransition(correlation, {
|
|
3431
3620
|
type: "agent_end",
|
|
@@ -4096,6 +4285,13 @@ export function createNotificationsExtension(
|
|
|
4096
4285
|
handle: string,
|
|
4097
4286
|
options: { graceMs: number; terminal?: { scope: "turn" | "owned"; expectedEpoch?: number } },
|
|
4098
4287
|
) => Promise<RunSettlementProof>;
|
|
4288
|
+
/**
|
|
4289
|
+
* Live, SYNCHRONOUS view of the dispatched tool calls the run resource
|
|
4290
|
+
* ledger holds for an execution handle (#5637). Strictly read-only: it
|
|
4291
|
+
* never claims, seals or otherwise mutates ledger state. Optional so an
|
|
4292
|
+
* older host that does not thread it degrades to the event-derived set.
|
|
4293
|
+
*/
|
|
4294
|
+
pendingToolExecutions?: (handle: string) => readonly string[];
|
|
4099
4295
|
};
|
|
4100
4296
|
} = {},
|
|
4101
4297
|
): void {
|
|
@@ -4323,6 +4519,37 @@ export function createNotificationsExtension(
|
|
|
4323
4519
|
"session.branch",
|
|
4324
4520
|
]);
|
|
4325
4521
|
const sessionId = (ctx: ExtensionContext): string => ctx.sessionManager.getSessionId();
|
|
4522
|
+
const releaseActiveWorkLease = (rt: SessionRuntime): void => {
|
|
4523
|
+
rt.activeWorkLease?.release();
|
|
4524
|
+
rt.activeWorkLease = undefined;
|
|
4525
|
+
};
|
|
4526
|
+
const releasePausedWorkLease = (rt: SessionRuntime): void => {
|
|
4527
|
+
rt.pausedWorkLease?.release();
|
|
4528
|
+
rt.pausedWorkLease = undefined;
|
|
4529
|
+
};
|
|
4530
|
+
const releasePromotedWorkLeases = (rt: SessionRuntime): void => {
|
|
4531
|
+
for (const handle of rt.promotedWorkLeases.splice(0)) handle.release();
|
|
4532
|
+
};
|
|
4533
|
+
const beginAgentWork = (rt: SessionRuntime): void => {
|
|
4534
|
+
releasePausedWorkLease(rt);
|
|
4535
|
+
// Acquire the run's handle before retiring the promoted ones so the lease
|
|
4536
|
+
// never reads as released between a queued submission and its run.
|
|
4537
|
+
if (!rt.activeWorkLease) rt.activeWorkLease = rt.workLease.acquire();
|
|
4538
|
+
// Every submission promoted into this run handed its handle over at the
|
|
4539
|
+
// promotion boundary; the run's own handle covers them from here.
|
|
4540
|
+
releasePromotedWorkLeases(rt);
|
|
4541
|
+
};
|
|
4542
|
+
const finishAgentWork = (rt: SessionRuntime, paused: boolean): void => {
|
|
4543
|
+
if (paused) {
|
|
4544
|
+
if (!rt.pausedWorkLease) rt.pausedWorkLease = rt.activeWorkLease ?? rt.workLease.acquire();
|
|
4545
|
+
rt.activeWorkLease = undefined;
|
|
4546
|
+
return;
|
|
4547
|
+
}
|
|
4548
|
+
// A run that failed before its agent_start still reaches agent_end; the
|
|
4549
|
+
// handles promoted into it have no later agent_start to retire them.
|
|
4550
|
+
if (!rt.activeWorkLease) releasePromotedWorkLeases(rt);
|
|
4551
|
+
releaseActiveWorkLease(rt);
|
|
4552
|
+
};
|
|
4326
4553
|
|
|
4327
4554
|
async function stopSession(
|
|
4328
4555
|
id: string,
|
|
@@ -4466,6 +4693,9 @@ export function createNotificationsExtension(
|
|
|
4466
4693
|
// runs while an identity successor is already serving, and clearing
|
|
4467
4694
|
// that live runtime's reader would manufacture "no evidence" for a
|
|
4468
4695
|
// host whose clients are still attached.
|
|
4696
|
+
releaseActiveWorkLease(rt);
|
|
4697
|
+
releasePausedWorkLease(rt);
|
|
4698
|
+
releasePromotedWorkLeases(rt);
|
|
4469
4699
|
rt.evidencePublication?.retract();
|
|
4470
4700
|
rt.evidencePublication = undefined;
|
|
4471
4701
|
} catch (e) {
|
|
@@ -4603,7 +4833,25 @@ export function createNotificationsExtension(
|
|
|
4603
4833
|
const pendingInteractive = new Map<string, PendingInteractiveAsk>();
|
|
4604
4834
|
const pendingPromptCorrelations: Array<{ commandId: string; turnId: string }> = [];
|
|
4605
4835
|
const pendingPromptCorrelationsBySdkRunToken = new Map<string, { commandId: string; turnId: string }>();
|
|
4836
|
+
const workLease = ctx.getSessionWorkLease?.() ?? createSessionWorkLease();
|
|
4606
4837
|
let runtime: SessionRuntime | undefined;
|
|
4838
|
+
// A promoted submission's handle must either enter the runtime-owned list
|
|
4839
|
+
// before teardown fences it, or be released immediately: stopSession drains
|
|
4840
|
+
// the list after fencing, and a predecessor runtime that already stopped
|
|
4841
|
+
// (mid session_switch) would otherwise hold the handle forever.
|
|
4842
|
+
const promoteWorkLease = (handle: SessionWorkLeaseHandle): void => {
|
|
4843
|
+
const currentRuntime = runtime;
|
|
4844
|
+
if (
|
|
4845
|
+
!currentRuntime ||
|
|
4846
|
+
currentRuntime.stopping ||
|
|
4847
|
+
currentRuntime.serverStopped ||
|
|
4848
|
+
runtimes.get(id) !== currentRuntime
|
|
4849
|
+
) {
|
|
4850
|
+
handle.release();
|
|
4851
|
+
return;
|
|
4852
|
+
}
|
|
4853
|
+
currentRuntime.promotedWorkLeases.push(handle);
|
|
4854
|
+
};
|
|
4607
4855
|
|
|
4608
4856
|
// The SDK can always answer now (interactive via the answer source, or the
|
|
4609
4857
|
// workflow gate), so the endpoint advertises a resolver. Validate the native
|
|
@@ -4750,12 +4998,55 @@ export function createNotificationsExtension(
|
|
|
4750
4998
|
}
|
|
4751
4999
|
};
|
|
4752
5000
|
|
|
4753
|
-
|
|
5001
|
+
type HostConnectionIncarnation = { generation: number; closed: boolean };
|
|
5002
|
+
type HostCapabilityCacheEntry = { generation: number; capabilities: ReadonlySet<string> };
|
|
5003
|
+
const hostCapCache = new Map<string, HostCapabilityCacheEntry>();
|
|
5004
|
+
const hostConnectionIncarnations = new Map<string, HostConnectionIncarnation>();
|
|
5005
|
+
const hostAttachedConnections = new Set<string>();
|
|
5006
|
+
let nextHostConnectionGeneration = 0;
|
|
5007
|
+
const liveHostConnection = (connectionId: string): HostConnectionIncarnation | undefined => {
|
|
5008
|
+
if (!connectionId) return undefined;
|
|
5009
|
+
const current = hostConnectionIncarnations.get(connectionId);
|
|
5010
|
+
if (current?.closed) return undefined;
|
|
5011
|
+
if (current) return current;
|
|
5012
|
+
const incarnation = { generation: ++nextHostConnectionGeneration, closed: false };
|
|
5013
|
+
hostConnectionIncarnations.set(connectionId, incarnation);
|
|
5014
|
+
return incarnation;
|
|
5015
|
+
};
|
|
5016
|
+
const rememberHostCapabilities = (connectionId: string, capabilities: readonly string[]): void => {
|
|
5017
|
+
const incarnation = liveHostConnection(connectionId);
|
|
5018
|
+
if (!incarnation) return;
|
|
5019
|
+
hostCapCache.set(connectionId, {
|
|
5020
|
+
generation: incarnation.generation,
|
|
5021
|
+
capabilities: new Set(capabilities),
|
|
5022
|
+
});
|
|
5023
|
+
};
|
|
5024
|
+
const liveHostCapabilities = (connectionId: string): ReadonlySet<string> | undefined => {
|
|
5025
|
+
const incarnation = hostConnectionIncarnations.get(connectionId);
|
|
5026
|
+
const entry = hostCapCache.get(connectionId);
|
|
5027
|
+
return incarnation && !incarnation.closed && entry?.generation === incarnation.generation
|
|
5028
|
+
? entry.capabilities
|
|
5029
|
+
: undefined;
|
|
5030
|
+
};
|
|
5031
|
+
const closeHostConnection = (connectionId: string): void => {
|
|
5032
|
+
const current = hostConnectionIncarnations.get(connectionId);
|
|
5033
|
+
if (current) current.closed = true;
|
|
5034
|
+
else {
|
|
5035
|
+
hostConnectionIncarnations.set(connectionId, {
|
|
5036
|
+
generation: ++nextHostConnectionGeneration,
|
|
5037
|
+
closed: true,
|
|
5038
|
+
});
|
|
5039
|
+
}
|
|
5040
|
+
hostCapCache.delete(connectionId);
|
|
5041
|
+
hostAttachedConnections.delete(connectionId);
|
|
5042
|
+
};
|
|
4754
5043
|
|
|
4755
5044
|
const configOverrides = new Map<string, unknown>();
|
|
4756
5045
|
const configRevision = { current: 0 };
|
|
4757
5046
|
const PROMPT_SUBMISSION_CAPACITY = 128;
|
|
4758
5047
|
const PROMPT_SUBMISSION_TTL_MS = 5 * 60_000;
|
|
5048
|
+
/** Bound on remembered `tool_execution_end`s that outran their own start. */
|
|
5049
|
+
const UNMATCHED_TOOL_END_CAPACITY = 64;
|
|
4759
5050
|
const PROMPT_TERMINAL_TOMBSTONE_CAPACITY = 256;
|
|
4760
5051
|
const PROMPT_TERMINAL_TOMBSTONE_TTL_MS = 15 * 60_000;
|
|
4761
5052
|
// SDK-owned terminalization grace; injectable in tests, never a user setting.
|
|
@@ -4803,18 +5094,53 @@ export function createNotificationsExtension(
|
|
|
4803
5094
|
* by the firing deadline timer, cleared when the attempt completes or
|
|
4804
5095
|
* is superseded). Fresh attributable progress recorded while the
|
|
4805
5096
|
* attempt awaits durable reconciliation advances the lease generation;
|
|
4806
|
-
* the attempt must then back off instead of
|
|
4807
|
-
* (mirrors PromptDeadlineManager).
|
|
5097
|
+
* before self-abort starts, the attempt must then back off instead of
|
|
5098
|
+
* terminalizing a live prompt (mirrors PromptDeadlineManager).
|
|
4808
5099
|
*/
|
|
4809
|
-
deadlineAttempt?: {
|
|
5100
|
+
deadlineAttempt?: {
|
|
5101
|
+
lease: PromptDeadlineLease;
|
|
5102
|
+
generation: number;
|
|
5103
|
+
/**
|
|
5104
|
+
* Set synchronously immediately before this expiry attempt aborts
|
|
5105
|
+
* its own run. Progress from that abort belongs to this attempt,
|
|
5106
|
+
* not to an independently live prompt.
|
|
5107
|
+
*/
|
|
5108
|
+
selfAbortStarted?: true;
|
|
5109
|
+
};
|
|
5110
|
+
/**
|
|
5111
|
+
* A due deadline expiry is already awaiting the tool-call boundary for
|
|
5112
|
+
* this submission. Progress that cannot renew past an already-due hard
|
|
5113
|
+
* cap re-arms a zero-delay timer on every delivery; without this fence
|
|
5114
|
+
* each one would open ANOTHER grace wait and they would pile up through
|
|
5115
|
+
* the whole grace (review thread P2).
|
|
5116
|
+
*/
|
|
5117
|
+
deadlineExpiryInFlight?: boolean;
|
|
4810
5118
|
phase: "active" | "outcome_claimed" | "terminalizing" | "publication_closed" | "delivered";
|
|
4811
5119
|
outcome?: SdkPromptTerminalOutcome;
|
|
4812
5120
|
/** Agent-owned resource run captured at acceptance; cleanup targets only this handle. */
|
|
4813
5121
|
executionHandle?: string;
|
|
4814
5122
|
/** Cancels accepted skill preparation before an execution handle exists. */
|
|
4815
5123
|
preflightAbort?: () => void | Promise<void>;
|
|
5124
|
+
/** Keeps the host alive from durable admission through terminal publication. */
|
|
5125
|
+
workLease?: SessionWorkLeaseHandle;
|
|
4816
5126
|
reconciliationKind: ReconciliationKind;
|
|
4817
5127
|
bufferedFrames: Array<PromptLifecycleFrame | Record<string, unknown>>;
|
|
5128
|
+
/**
|
|
5129
|
+
* Dispatched tool calls currently executing for this correlation. A SET of
|
|
5130
|
+
* ids, not a counter: a duplicate or unmatched `tool_execution_end` must
|
|
5131
|
+
* not drive it negative and falsely report the prompt idle (#5637).
|
|
5132
|
+
*/
|
|
5133
|
+
runningToolCallIds: Set<string>;
|
|
5134
|
+
/**
|
|
5135
|
+
* Ids whose `tool_execution_end` was delivered with no matching start yet.
|
|
5136
|
+
* The fanout is asynchronous and unordered across listeners, so the end of
|
|
5137
|
+
* a finished call can land first; without this the late start would add a
|
|
5138
|
+
* call that is already over and leave it running forever. Bounded, and
|
|
5139
|
+
* only ever written on that out-of-order path.
|
|
5140
|
+
*/
|
|
5141
|
+
endedToolCallIds: Set<string>;
|
|
5142
|
+
/** Drained when `runningToolCallIds` becomes empty; one entry per waiting expiry attempt. */
|
|
5143
|
+
toolIdleWaiters: Array<() => void>;
|
|
4818
5144
|
};
|
|
4819
5145
|
const promptSubmissions = new Map<string, PromptSubmission>();
|
|
4820
5146
|
/** Connections fenced by a fatal prompt closure; their later frames are refused. */
|
|
@@ -4832,15 +5158,17 @@ export function createNotificationsExtension(
|
|
|
4832
5158
|
* ordinary direct SDK subscribers retain both public surfaces from #4570.
|
|
4833
5159
|
*/
|
|
4834
5160
|
const broadcastEventFrame = (event: SdkFrame): string[] => {
|
|
4835
|
-
const gated = CAP_GATED_FRAME_KINDS.has(String(event.kind));
|
|
4836
5161
|
const json = JSON.stringify(event);
|
|
4837
5162
|
const recipients: string[] = [];
|
|
4838
|
-
for (const
|
|
5163
|
+
for (const connectionId of hostAttachedConnections) {
|
|
5164
|
+
const incarnation = hostConnectionIncarnations.get(connectionId);
|
|
5165
|
+
if (!incarnation || incarnation.closed) continue;
|
|
5166
|
+
const capabilities = liveHostCapabilities(connectionId);
|
|
4839
5167
|
if (fencedConnections.has(connectionId)) continue;
|
|
4840
|
-
if (
|
|
5168
|
+
if (!canDeliverSdkEvent(String(event.kind), capabilities)) continue;
|
|
4841
5169
|
try {
|
|
4842
5170
|
server.sendTo(connectionId, json);
|
|
4843
|
-
if (capabilities
|
|
5171
|
+
if (capabilities?.has(POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY)) recipients.push(connectionId);
|
|
4844
5172
|
} catch {
|
|
4845
5173
|
// Broadcasts are best effort; directed responses surface send failures.
|
|
4846
5174
|
}
|
|
@@ -4848,15 +5176,17 @@ export function createNotificationsExtension(
|
|
|
4848
5176
|
return recipients;
|
|
4849
5177
|
};
|
|
4850
5178
|
const broadcastEventFrameWithReceipts = (event: SdkFrame): string[] => {
|
|
4851
|
-
const gated = CAP_GATED_FRAME_KINDS.has(String(event.kind));
|
|
4852
5179
|
const json = JSON.stringify(event);
|
|
4853
5180
|
const receipts: string[] = [];
|
|
4854
|
-
for (const
|
|
5181
|
+
for (const connectionId of hostAttachedConnections) {
|
|
5182
|
+
const incarnation = hostConnectionIncarnations.get(connectionId);
|
|
5183
|
+
if (!incarnation || incarnation.closed) continue;
|
|
5184
|
+
const capabilities = liveHostCapabilities(connectionId);
|
|
4855
5185
|
if (fencedConnections.has(connectionId)) continue;
|
|
4856
|
-
if (
|
|
5186
|
+
if (!canDeliverSdkEvent(String(event.kind), capabilities)) continue;
|
|
4857
5187
|
try {
|
|
4858
5188
|
const receipt = server.sendToWithReceipt(connectionId, json);
|
|
4859
|
-
if (capabilities
|
|
5189
|
+
if (capabilities?.has(POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY)) receipts.push(receipt);
|
|
4860
5190
|
} catch {
|
|
4861
5191
|
// Rejected positioned sends remain eligible for the atomic raw fallback.
|
|
4862
5192
|
}
|
|
@@ -4940,6 +5270,7 @@ export function createNotificationsExtension(
|
|
|
4940
5270
|
pendingPromptCorrelationsBySdkRunToken.delete(sdkRunToken);
|
|
4941
5271
|
}
|
|
4942
5272
|
};
|
|
5273
|
+
|
|
4943
5274
|
const addTerminalTombstone = (key: string, connectionId: string, now = Date.now()) => {
|
|
4944
5275
|
promptTerminalTombstones.delete(key);
|
|
4945
5276
|
promptTerminalTombstones.set(key, { connectionId, expiresAt: now + PROMPT_TERMINAL_TOMBSTONE_TTL_MS });
|
|
@@ -4949,6 +5280,10 @@ export function createNotificationsExtension(
|
|
|
4949
5280
|
const finalizePrompt = (key: string, correlation: { commandId: string; turnId: string }) => {
|
|
4950
5281
|
const submission = promptSubmissions.get(key);
|
|
4951
5282
|
if (submission?.deadlineTimer) clearTimeout(submission.deadlineTimer);
|
|
5283
|
+
if (submission?.workLease) {
|
|
5284
|
+
submission.workLease.release();
|
|
5285
|
+
submission.workLease = undefined;
|
|
5286
|
+
}
|
|
4952
5287
|
promptSubmissions.delete(key);
|
|
4953
5288
|
removePendingPromptCorrelation(correlation);
|
|
4954
5289
|
if (submission) addTerminalTombstone(key, submission.connectionId);
|
|
@@ -4956,6 +5291,10 @@ export function createNotificationsExtension(
|
|
|
4956
5291
|
const expirePromptDelivery = (key: string, submission: PromptSubmission) => {
|
|
4957
5292
|
if (!submission.terminal) return;
|
|
4958
5293
|
if (submission.deadlineTimer) clearTimeout(submission.deadlineTimer);
|
|
5294
|
+
if (submission.workLease) {
|
|
5295
|
+
submission.workLease.release();
|
|
5296
|
+
submission.workLease = undefined;
|
|
5297
|
+
}
|
|
4959
5298
|
promptSubmissions.delete(key);
|
|
4960
5299
|
// A fatal closure is transport-level, not a committed semantic terminal: the
|
|
4961
5300
|
// durable record stays authoritative, so it must never leave a tombstone.
|
|
@@ -5021,10 +5360,6 @@ export function createNotificationsExtension(
|
|
|
5021
5360
|
finalizePrompt(key, correlation);
|
|
5022
5361
|
}
|
|
5023
5362
|
};
|
|
5024
|
-
const readFiniteSetting = (key: "sdk.promptDeadlineMs" | "sdk.promptMaxRuntimeMs", fallback: number): number => {
|
|
5025
|
-
const value = settings?.get(key);
|
|
5026
|
-
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
5027
|
-
};
|
|
5028
5363
|
/**
|
|
5029
5364
|
* (Re)arm the accepted prompt's terminal deadline from its current lease.
|
|
5030
5365
|
* `terminalizePrompt` stays the single authoritative terminal owner; this
|
|
@@ -5037,33 +5372,104 @@ export function createNotificationsExtension(
|
|
|
5037
5372
|
if (submission.deadlineTimer) clearTimeout(submission.deadlineTimer);
|
|
5038
5373
|
submission.deadlineTimer = setTimeout(
|
|
5039
5374
|
() => {
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5375
|
+
void (async () => {
|
|
5376
|
+
const current = promptSubmissions.get(key);
|
|
5377
|
+
// Identity fencing: a discarded, evicted or re-accepted submission is
|
|
5378
|
+
// never terminalized by a predecessor's timer.
|
|
5379
|
+
if (!current || current.deadlineLease !== lease) return;
|
|
5380
|
+
// Progress delivered while this timer was pending renews the lease, so
|
|
5381
|
+
// re-arm instead of terminalizing a demonstrably live prompt.
|
|
5382
|
+
if (Date.now() < promptDeadlineAt(lease)) {
|
|
5383
|
+
armPromptDeadline(key, correlation);
|
|
5384
|
+
return;
|
|
5385
|
+
}
|
|
5386
|
+
// Nothing to terminalize behind the terminal owner — and nothing to
|
|
5387
|
+
// wait for on its behalf either. `terminalizePrompt` refuses this
|
|
5388
|
+
// state anyway; checking it here keeps a superseded timer from
|
|
5389
|
+
// opening a boundary wait it can never use.
|
|
5390
|
+
if (current.terminal || current.phase !== "active") return;
|
|
5391
|
+
// One boundary wait per submission (review thread P2). At an already
|
|
5392
|
+
// due HARD CAP `promptDeadlineAt` is pinned to `acceptedAt + maxMs`,
|
|
5393
|
+
// so every streamed `tool_execution_update` — attributable progress
|
|
5394
|
+
// by design, so a multi-minute compile does not trip the inactivity
|
|
5395
|
+
// lease — re-arms a ZERO-delay timer that walks straight past the
|
|
5396
|
+
// re-arm check above. `deadlineAttempt` cannot serve as the fence: it
|
|
5397
|
+
// is registered only AFTER the wait, precisely so the boundary event
|
|
5398
|
+
// that ends the wait is not read as superseding it.
|
|
5399
|
+
if (current.deadlineExpiryInFlight) return;
|
|
5400
|
+
current.deadlineExpiryInFlight = true;
|
|
5401
|
+
try {
|
|
5402
|
+
// Let an in-flight dispatched tool call reach its boundary first
|
|
5403
|
+
// (#5637): terminal fencing aborts the run before it waits for
|
|
5404
|
+
// settlement, so a tool aborted mid-write leaves a torn artifact.
|
|
5405
|
+
// The wait sits BEFORE the attempt is registered on purpose — inside
|
|
5406
|
+
// `terminalizePrompt` the very `tool_execution_end` that ends the
|
|
5407
|
+
// wait would bump the lease generation and the post-claim fence
|
|
5408
|
+
// would read this attempt as "superseded", so the deadline would
|
|
5409
|
+
// effectively never fire while tools run. Here the existing
|
|
5410
|
+
// renewal/re-arm logic handles that case unchanged. Nothing has been
|
|
5411
|
+
// aborted yet, so this wait is composed with NO abort signal: an
|
|
5412
|
+
// already-aborted one would make the path unreachable.
|
|
5413
|
+
const boundary: ToolBoundaryWaitResult =
|
|
5414
|
+
pendingToolCallIdsFor(current).length === 0
|
|
5415
|
+
? IDLE_TOOL_BOUNDARY_RESULT
|
|
5416
|
+
: await waitForToolCallBoundary({
|
|
5417
|
+
pending: () => pendingToolCallIdsFor(current),
|
|
5418
|
+
whenIdle: () => whenPromptToolsIdle(current),
|
|
5419
|
+
graceMs: TOOL_CALL_BOUNDARY_GRACE_MS,
|
|
5420
|
+
});
|
|
5421
|
+
// Every pre-await fence is re-validated after the await, in order.
|
|
5422
|
+
const after = promptSubmissions.get(key);
|
|
5423
|
+
if (!after || after.deadlineLease !== lease) return;
|
|
5424
|
+
if (after.terminal || after.phase !== "active") return;
|
|
5425
|
+
if (Date.now() < promptDeadlineAt(lease)) {
|
|
5426
|
+
// The boundary event renewed the lease: the prompt is demonstrably
|
|
5427
|
+
// alive, so re-arm instead of terminalizing it.
|
|
5428
|
+
armPromptDeadline(key, correlation);
|
|
5429
|
+
return;
|
|
5430
|
+
}
|
|
5431
|
+
if (boundary.outcome === "forced") {
|
|
5432
|
+
// Ids only: never tool arguments, output, paths or anything
|
|
5433
|
+
// credential-shaped.
|
|
5434
|
+
logger.warn("sdk_prompt_deadline_forced_mid_tool", {
|
|
5435
|
+
commandId: correlation.commandId,
|
|
5436
|
+
turnId: correlation.turnId,
|
|
5437
|
+
graceMs: TOOL_CALL_BOUNDARY_GRACE_MS,
|
|
5438
|
+
pendingToolCallIds: boundary.pendingToolCallIds,
|
|
5439
|
+
});
|
|
5440
|
+
}
|
|
5441
|
+
// Register the expiry attempt BEFORE the awaited durable claim:
|
|
5442
|
+
// progress arriving while the claim is in flight must supersede
|
|
5443
|
+
// this attempt (see the post-claim fence in terminalizePrompt).
|
|
5444
|
+
// The generation is read AFTER the boundary wait because the
|
|
5445
|
+
// boundary event may have bumped it and this attempt is
|
|
5446
|
+
// deliberately the current one.
|
|
5447
|
+
after.deadlineAttempt = { lease, generation: lease.generation };
|
|
5448
|
+
void terminalizePrompt(
|
|
5449
|
+
correlation,
|
|
5450
|
+
{
|
|
5451
|
+
kind: "failed",
|
|
5452
|
+
code: "prompt_deadline_exceeded",
|
|
5453
|
+
message: "Prompt deadline exceeded.",
|
|
5454
|
+
provenance: "deadline",
|
|
5455
|
+
phase: "submission",
|
|
5456
|
+
category: "deadline",
|
|
5457
|
+
},
|
|
5458
|
+
{ fence: true },
|
|
5459
|
+
{ diagnostic: { reason: promptDeadlineDiagnosticReason(boundary) } },
|
|
5460
|
+
);
|
|
5461
|
+
} finally {
|
|
5462
|
+
// `finally` is safe here, and a bare clear before each `return` is
|
|
5463
|
+
// not: a leaked `true` disables the deadline for the rest of the
|
|
5464
|
+
// submission's life, which is strictly worse than the pile-up it
|
|
5465
|
+
// guards. It can only ever clear the flag THIS attempt set on THIS
|
|
5466
|
+
// object — a competing attempt would have returned at the guard
|
|
5467
|
+
// above without setting it — and an attempt that threw is no longer
|
|
5468
|
+
// waiting, so re-admitting the next one is correct. The wait itself
|
|
5469
|
+
// is bounded by the grace, so this always runs.
|
|
5470
|
+
current.deadlineExpiryInFlight = false;
|
|
5471
|
+
}
|
|
5472
|
+
})();
|
|
5067
5473
|
},
|
|
5068
5474
|
Math.max(0, promptDeadlineAt(lease) - Date.now()),
|
|
5069
5475
|
);
|
|
@@ -5074,6 +5480,7 @@ export function createNotificationsExtension(
|
|
|
5074
5480
|
const correlation = runtime.activePromptCorrelation;
|
|
5075
5481
|
// Renewal is attributed exactly like the correlated delivery below.
|
|
5076
5482
|
renewPromptDeadline(correlation, event);
|
|
5483
|
+
noteToolDispatchBoundary(correlation, event);
|
|
5077
5484
|
const submission = promptSubmissions.get(promptSubmissionKey(correlation));
|
|
5078
5485
|
if (!submission || submission.abandoned) return;
|
|
5079
5486
|
const frame = {
|
|
@@ -5108,10 +5515,24 @@ export function createNotificationsExtension(
|
|
|
5108
5515
|
const deadlineAttemptStatus = (
|
|
5109
5516
|
key: string,
|
|
5110
5517
|
submission: PromptSubmission,
|
|
5111
|
-
attempt: { lease: PromptDeadlineLease; generation: number },
|
|
5518
|
+
attempt: { lease: PromptDeadlineLease; generation: number; selfAbortStarted?: true },
|
|
5112
5519
|
): DeadlineAttemptStatus => {
|
|
5113
5520
|
if (promptSubmissions.get(key) !== submission || submission.deadlineLease !== attempt.lease) return "stale";
|
|
5114
5521
|
const lease = submission.deadlineLease;
|
|
5522
|
+
// A deadline expiry attempt must not be superseded by progress that its
|
|
5523
|
+
// OWN abort caused. Terminal fencing aborts the running tool, and the
|
|
5524
|
+
// agent loop pairs that tool off with a `tool_execution_end` which is
|
|
5525
|
+
// NOT marked non-dispatched — the marker is reserved for calls that
|
|
5526
|
+
// never started, and this one did (`dispatched = record.started`,
|
|
5527
|
+
// agent-loop.ts). So the abort's teardown reaches renewPromptDeadline as
|
|
5528
|
+
// ordinary attributable progress and advances the generation of the very
|
|
5529
|
+
// lease this attempt is expiring. Reading that as supersession makes the
|
|
5530
|
+
// attempt back off, re-arm, and walk into the identical trap forever: the
|
|
5531
|
+
// prompt never gets a terminal frame and its durable claim stays pending
|
|
5532
|
+
// (#5583). Progress observed BEFORE the self-abort is still authoritative
|
|
5533
|
+
// — that is the post-claim fence, and it keeps a genuinely live prompt
|
|
5534
|
+
// from being killed.
|
|
5535
|
+
if (attempt.selfAbortStarted) return "current";
|
|
5115
5536
|
if (lease.generation !== attempt.generation && Date.now() < promptDeadlineAt(lease)) return "superseded";
|
|
5116
5537
|
return "current";
|
|
5117
5538
|
};
|
|
@@ -5146,6 +5567,99 @@ export function createNotificationsExtension(
|
|
|
5146
5567
|
if (submission.deadlineLease.generation === generation) return;
|
|
5147
5568
|
armPromptDeadline(key, correlation);
|
|
5148
5569
|
};
|
|
5570
|
+
/**
|
|
5571
|
+
* Track which dispatched tool calls are executing for the accepted prompt,
|
|
5572
|
+
* so an expired deadline can abort at a tool boundary instead of mid-write
|
|
5573
|
+
* (#5637). Attribution is deliberately identical to `renewPromptDeadline`'s
|
|
5574
|
+
* — same funnel, same allowlist, same pairing-only exclusion — because a
|
|
5575
|
+
* call that cannot prove progress cannot prove it is running either.
|
|
5576
|
+
* `tool_execution_update` proves neither start nor end, so it never changes
|
|
5577
|
+
* membership.
|
|
5578
|
+
*/
|
|
5579
|
+
const noteToolDispatchBoundary = (
|
|
5580
|
+
correlation: { commandId: string; turnId: string },
|
|
5581
|
+
event: { type: string; toolCallId?: unknown },
|
|
5582
|
+
) => {
|
|
5583
|
+
if (event.type !== "tool_execution_start" && event.type !== "tool_execution_end") return;
|
|
5584
|
+
if (isNonDispatchedToolEvent(event) || typeof event.toolCallId !== "string") return;
|
|
5585
|
+
const submission = promptSubmissions.get(promptSubmissionKey(correlation));
|
|
5586
|
+
if (!submission) return;
|
|
5587
|
+
if (event.type === "tool_execution_start") {
|
|
5588
|
+
// A start whose own end already arrived describes a call that is over:
|
|
5589
|
+
// re-adding it would strand an id that can never be removed again, and
|
|
5590
|
+
// the deadline would then burn the whole grace and report a forced
|
|
5591
|
+
// mid-tool kill for a tool that is not running (review thread P1,
|
|
5592
|
+
// mirror case). The id is consumed, so the pair closes exactly once.
|
|
5593
|
+
if (submission.endedToolCallIds.delete(event.toolCallId)) return;
|
|
5594
|
+
submission.runningToolCallIds.add(event.toolCallId);
|
|
5595
|
+
return;
|
|
5596
|
+
}
|
|
5597
|
+
if (!submission.runningToolCallIds.delete(event.toolCallId)) {
|
|
5598
|
+
// End with no known start: remember it only for this out-of-order case.
|
|
5599
|
+
submission.endedToolCallIds.add(event.toolCallId);
|
|
5600
|
+
// Oldest-first eviction; a Set iterates in insertion order.
|
|
5601
|
+
while (submission.endedToolCallIds.size > UNMATCHED_TOOL_END_CAPACITY) {
|
|
5602
|
+
const oldest = submission.endedToolCallIds.values().next().value;
|
|
5603
|
+
if (oldest === undefined) break;
|
|
5604
|
+
submission.endedToolCallIds.delete(oldest);
|
|
5605
|
+
}
|
|
5606
|
+
return;
|
|
5607
|
+
}
|
|
5608
|
+
if (submission.runningToolCallIds.size > 0) return;
|
|
5609
|
+
for (const resolve of submission.toolIdleWaiters.splice(0)) resolve();
|
|
5610
|
+
};
|
|
5611
|
+
/**
|
|
5612
|
+
* Authoritative set of tool calls executing for this submission (#5637).
|
|
5613
|
+
*
|
|
5614
|
+
* The run resource ledger is the source of truth, not the extension fanout.
|
|
5615
|
+
* AgentLoop reserves a `kind: "tool"` lease SYNCHRONOUSLY inside its
|
|
5616
|
+
* dispatch loop, before the tool's `execute` is invoked, and the lease
|
|
5617
|
+
* settles at the call's real end. `tool_execution_start` instead travels an
|
|
5618
|
+
* ASYNCHRONOUS path — enqueued on the agent event stream, drained a turn
|
|
5619
|
+
* later, then handed to listeners whose returned promises are deliberately
|
|
5620
|
+
* not awaited, behind any user extension registered ahead of this one. So
|
|
5621
|
+
* `runningToolCallIds` can still be EMPTY while a file-mutating tool is
|
|
5622
|
+
* already writing, which is exactly the mid-`apply_patch` kill this fix
|
|
5623
|
+
* exists to prevent.
|
|
5624
|
+
*
|
|
5625
|
+
* So whenever the ledger can be read it is the SOLE authority, and a read
|
|
5626
|
+
* that comes back empty means empty. The fanout is wrong in both
|
|
5627
|
+
* directions, not one: ledger-running/event-idle must not report idle (the
|
|
5628
|
+
* mid-`apply_patch` kill above), and ledger-empty/event-running must not
|
|
5629
|
+
* report pending — the same asynchrony that delays a start also delays an
|
|
5630
|
+
* end, so a `tool_execution_end` stuck behind another extension would
|
|
5631
|
+
* otherwise burn the whole grace and record a forced mid-tool kill for a
|
|
5632
|
+
* call that had already returned.
|
|
5633
|
+
*
|
|
5634
|
+
* The fanout stays for observability and still wakes `toolIdleWaiters`, but
|
|
5635
|
+
* it is the pending authority only when the ledger cannot be consulted: an
|
|
5636
|
+
* older host without the seam, no execution handle bound yet, or a read
|
|
5637
|
+
* that threw. That fallback is exactly the pre-ledger behaviour and never
|
|
5638
|
+
* worse.
|
|
5639
|
+
*/
|
|
5640
|
+
const pendingToolCallIdsFor = (submission: PromptSubmission): string[] => {
|
|
5641
|
+
const handle = submission.executionHandle;
|
|
5642
|
+
const readLedger = terminalAbortSeams?.pendingToolExecutions;
|
|
5643
|
+
if (handle && readLedger)
|
|
5644
|
+
try {
|
|
5645
|
+
// Materialized inside the `try` so only a COMPLETED read wins; a read
|
|
5646
|
+
// that succeeds with no entries still answers "nothing is running",
|
|
5647
|
+
// which is the whole point of preferring it over the stale fanout.
|
|
5648
|
+
return [...readLedger(handle)];
|
|
5649
|
+
} catch (error) {
|
|
5650
|
+
// A seam failure must never disable the deadline or extend it
|
|
5651
|
+
// unboundedly; fall back to the event-derived set for this attempt.
|
|
5652
|
+
logger.warn(`sdk: pending tool execution seam failed: ${String(error)}`);
|
|
5653
|
+
}
|
|
5654
|
+
return [...submission.runningToolCallIds];
|
|
5655
|
+
};
|
|
5656
|
+
/** Resolves once no dispatched tool call is executing for this submission. */
|
|
5657
|
+
const whenPromptToolsIdle = (submission: PromptSubmission): Promise<void> => {
|
|
5658
|
+
if (submission.runningToolCallIds.size === 0) return Promise.resolve();
|
|
5659
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
5660
|
+
submission.toolIdleWaiters.push(resolve);
|
|
5661
|
+
return promise;
|
|
5662
|
+
};
|
|
5149
5663
|
const flushPromptLifecycle = (key: string, submission: PromptSubmission) => {
|
|
5150
5664
|
for (const frame of submission.bufferedFrames.splice(0)) {
|
|
5151
5665
|
try {
|
|
@@ -5177,6 +5691,7 @@ export function createNotificationsExtension(
|
|
|
5177
5691
|
if (trackReconciliation) releasePromptAdmission(clientRef);
|
|
5178
5692
|
return;
|
|
5179
5693
|
}
|
|
5694
|
+
const workLeaseHandle = runtime?.workLease.acquire();
|
|
5180
5695
|
// Register delivery ownership synchronously before any await so post-accept
|
|
5181
5696
|
// failures that race the durable write still emit correlated terminals.
|
|
5182
5697
|
cleanupPromptRecords();
|
|
@@ -5188,11 +5703,13 @@ export function createNotificationsExtension(
|
|
|
5188
5703
|
([, submission]) =>
|
|
5189
5704
|
submission.terminal && (submission.phase === "delivered" || submission.fatal === true),
|
|
5190
5705
|
);
|
|
5191
|
-
if (!oldestTerminal)
|
|
5706
|
+
if (!oldestTerminal) {
|
|
5707
|
+
workLeaseHandle?.release();
|
|
5192
5708
|
throw Object.assign(
|
|
5193
5709
|
new Error("Too many active prompt submissions; reconcile or await terminal state."),
|
|
5194
5710
|
{ code: "reconciliation_capacity" },
|
|
5195
5711
|
);
|
|
5712
|
+
}
|
|
5196
5713
|
expirePromptDelivery(oldestTerminal[0], oldestTerminal[1]);
|
|
5197
5714
|
}
|
|
5198
5715
|
if (sdkRunToken) pendingPromptCorrelationsBySdkRunToken.set(sdkRunToken, correlation);
|
|
@@ -5207,15 +5724,19 @@ export function createNotificationsExtension(
|
|
|
5207
5724
|
createdAt: Date.now(),
|
|
5208
5725
|
deadlineLease: createPromptDeadlineLease({
|
|
5209
5726
|
now: Date.now(),
|
|
5210
|
-
leaseMs:
|
|
5211
|
-
maxMs:
|
|
5727
|
+
leaseMs: resolveSdkPromptDeadlineMs(settings?.get("sdk.promptDeadlineMs")),
|
|
5728
|
+
maxMs: resolveSdkPromptMaxRuntimeMs(settings?.get("sdk.promptMaxRuntimeMs")),
|
|
5212
5729
|
}),
|
|
5213
5730
|
phase: "active",
|
|
5731
|
+
...(workLeaseHandle ? { workLease: workLeaseHandle } : {}),
|
|
5214
5732
|
// Bound to the Agent run at `agent_start`; acceptance precedes execution.
|
|
5215
5733
|
executionHandle: undefined,
|
|
5216
5734
|
...(preflightAbort ? { preflightAbort } : {}),
|
|
5217
5735
|
reconciliationKind,
|
|
5218
5736
|
bufferedFrames: [],
|
|
5737
|
+
runningToolCallIds: new Set<string>(),
|
|
5738
|
+
endedToolCallIds: new Set<string>(),
|
|
5739
|
+
toolIdleWaiters: [],
|
|
5219
5740
|
};
|
|
5220
5741
|
const key = promptSubmissionKey(correlation);
|
|
5221
5742
|
promptSubmissions.set(key, submission);
|
|
@@ -5226,9 +5747,14 @@ export function createNotificationsExtension(
|
|
|
5226
5747
|
};
|
|
5227
5748
|
/** Roll back process-local registration when durable acceptance failed. */
|
|
5228
5749
|
const discardPromptAcceptance = (correlation: { commandId: string; turnId: string }) => {
|
|
5229
|
-
const
|
|
5750
|
+
const key = promptSubmissionKey(correlation);
|
|
5751
|
+
const submission = promptSubmissions.get(key);
|
|
5230
5752
|
if (submission?.deadlineTimer) clearTimeout(submission.deadlineTimer);
|
|
5231
|
-
|
|
5753
|
+
if (submission?.workLease) {
|
|
5754
|
+
submission.workLease.release();
|
|
5755
|
+
submission.workLease = undefined;
|
|
5756
|
+
}
|
|
5757
|
+
promptSubmissions.delete(key);
|
|
5232
5758
|
removePendingPromptCorrelation(correlation);
|
|
5233
5759
|
};
|
|
5234
5760
|
const recordPromptTerminal = (correlation: { commandId: string; turnId: string } | undefined) => {
|
|
@@ -5252,6 +5778,10 @@ export function createNotificationsExtension(
|
|
|
5252
5778
|
) => {
|
|
5253
5779
|
submission.deadlineAttempt = undefined;
|
|
5254
5780
|
if (submission.deadlineTimer) clearTimeout(submission.deadlineTimer);
|
|
5781
|
+
if (submission.workLease) {
|
|
5782
|
+
submission.workLease.release();
|
|
5783
|
+
submission.workLease = undefined;
|
|
5784
|
+
}
|
|
5255
5785
|
if (!submission.terminal) {
|
|
5256
5786
|
submission.terminal = true;
|
|
5257
5787
|
submission.fatal = true;
|
|
@@ -5403,6 +5933,10 @@ export function createNotificationsExtension(
|
|
|
5403
5933
|
}
|
|
5404
5934
|
let proof: RunSettlementProof;
|
|
5405
5935
|
try {
|
|
5936
|
+
// Mark immediately before the abort seam, with no await between:
|
|
5937
|
+
// abort-time attributable completion is caused by this deadline
|
|
5938
|
+
// attempt and must not supersede its own expiry terminal.
|
|
5939
|
+
if (deadlineAttempt) deadlineAttempt.selfAbortStarted = true;
|
|
5406
5940
|
proof = await terminalAbortSeams.abortPromptAndWaitWithTerminal(submission.executionHandle, {
|
|
5407
5941
|
graceMs: PROMPT_TERMINALIZATION_GRACE_MS,
|
|
5408
5942
|
// Terminal abort registers the continuation fence for the
|
|
@@ -5439,10 +5973,56 @@ export function createNotificationsExtension(
|
|
|
5439
5973
|
}
|
|
5440
5974
|
if (capture) capture.proof = proof;
|
|
5441
5975
|
}
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5976
|
+
if (deadlineAttempt) {
|
|
5977
|
+
// Durability BEFORE the durable terminal (#5583, #5623 review round 4).
|
|
5978
|
+
// The bus owns an independent deadline timer and terminalization path,
|
|
5979
|
+
// so the host wiring in `session-runtime` never sees this expiry —
|
|
5980
|
+
// without this call an active notification-bus session loses its dirty
|
|
5981
|
+
// edits on a deadline, which is the exact work-loss this change exists
|
|
5982
|
+
// to stop. It runs between the durable pending claim above and the
|
|
5983
|
+
// finalize below on purpose: a process death anywhere in this window
|
|
5984
|
+
// leaves the durable record PENDING, so restart recovery retries the
|
|
5985
|
+
// prompt instead of treating work that was never autosaved as finished.
|
|
5986
|
+
// Ordering it after the finalize made the autosave the one step a crash
|
|
5987
|
+
// could silently skip. The cost is that the durable terminal is delayed
|
|
5988
|
+
// by at most the flush's 10s bound.
|
|
5989
|
+
//
|
|
5990
|
+
// Deadline path only: reaching here inside `if (deadlineAttempt)` proves
|
|
5991
|
+
// the attempt was registered, and `winner` is the authoritative claimed
|
|
5992
|
+
// outcome, so a cancel, an `agent_failed`, or a normal `agent_end` never
|
|
5993
|
+
// autosaves. Bounded by the shared race and fully swallowed: the
|
|
5994
|
+
// terminal recorded and published below is identical whether the flush
|
|
5995
|
+
// succeeds, fails, or is abandoned.
|
|
5996
|
+
if (
|
|
5997
|
+
winner.kind === "failed" &&
|
|
5998
|
+
winner.code === "prompt_deadline_exceeded" &&
|
|
5999
|
+
settings?.get("sdk.flushWorktreeOnDeadline") !== false
|
|
6000
|
+
) {
|
|
6001
|
+
// `has` is true only for a value the user actually wrote, so this
|
|
6002
|
+
// separates an explicit opt-in from the schema default. The flush only
|
|
6003
|
+
// honours the default inside a linked worktree the session owns.
|
|
6004
|
+
const configured = settings?.get("sdk.flushWorktreeOnDeadline");
|
|
6005
|
+
const hasExplicitSetting =
|
|
6006
|
+
typeof settings?.has === "function"
|
|
6007
|
+
? settings.has("sdk.flushWorktreeOnDeadline")
|
|
6008
|
+
: configured === true;
|
|
6009
|
+
const explicitOptIn = hasExplicitSetting === true && configured === true;
|
|
6010
|
+
await runBoundedDeadlineFlush(signal =>
|
|
6011
|
+
flushWorktreeOnPromptDeadline(ctx.cwd, {
|
|
6012
|
+
explicitOptIn,
|
|
6013
|
+
isCurrent: () =>
|
|
6014
|
+
deadlineAttemptStatus(promptSubmissionKey(correlation), submission, deadlineAttempt) ===
|
|
6015
|
+
"current",
|
|
6016
|
+
signal,
|
|
6017
|
+
}),
|
|
6018
|
+
);
|
|
6019
|
+
}
|
|
6020
|
+
}
|
|
6021
|
+
// Second deadline-attempt fence, across both the awaited fencing
|
|
6022
|
+
// transition and the autosave above: progress recorded while either was in
|
|
6023
|
+
// flight supersedes exactly like progress during the claim, and the
|
|
6024
|
+
// autosave alone can hold this path for ten seconds. Non-deadline attempts
|
|
6025
|
+
// skip this — their attempt identity was never registered.
|
|
5446
6026
|
if (deadlineAttempt) {
|
|
5447
6027
|
const key = promptSubmissionKey(correlation);
|
|
5448
6028
|
const status = deadlineAttemptStatus(key, submission, deadlineAttempt);
|
|
@@ -6693,6 +7273,8 @@ export function createNotificationsExtension(
|
|
|
6693
7273
|
terminalAbortSeams,
|
|
6694
7274
|
// #4743: join fire-and-forget reconciliation producers at teardown.
|
|
6695
7275
|
trackReconciliationProducer,
|
|
7276
|
+
workLease,
|
|
7277
|
+
promoteWorkLease,
|
|
6696
7278
|
);
|
|
6697
7279
|
cancelPreflightsForConnection = controlSurface.cancelPendingPreflightsForConnection;
|
|
6698
7280
|
const abandonPromptResponse = (connectionId: string, frame: Record<string, unknown>) => {
|
|
@@ -6832,7 +7414,7 @@ export function createNotificationsExtension(
|
|
|
6832
7414
|
expectedEpoch: options.masterAttestationEpoch,
|
|
6833
7415
|
replay: consumedMasterNonces,
|
|
6834
7416
|
}),
|
|
6835
|
-
connectionCapabilities:
|
|
7417
|
+
connectionCapabilities: liveHostCapabilities,
|
|
6836
7418
|
installProviderDefinitions,
|
|
6837
7419
|
onProviderDefinitionsRemoved: removeProviderDefinitions,
|
|
6838
7420
|
onRequest: options.onSdkRequest,
|
|
@@ -7215,6 +7797,8 @@ export function createNotificationsExtension(
|
|
|
7215
7797
|
policyGeneration: 0,
|
|
7216
7798
|
workflowGatePublicationEpoch: 0,
|
|
7217
7799
|
busy: false,
|
|
7800
|
+
workLease,
|
|
7801
|
+
promotedWorkLeases: [],
|
|
7218
7802
|
pendingPromptCorrelations,
|
|
7219
7803
|
pendingPromptCorrelationsBySdkRunToken,
|
|
7220
7804
|
activePromptCorrelation: undefined,
|
|
@@ -7358,14 +7942,10 @@ export function createNotificationsExtension(
|
|
|
7358
7942
|
sendEndpointStale(inbound.connectionId, typedFrame);
|
|
7359
7943
|
return;
|
|
7360
7944
|
}
|
|
7361
|
-
if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
|
|
7362
7945
|
if (typedFrame.type === "event_replay") {
|
|
7363
|
-
|
|
7364
|
-
hostCapCache.set(
|
|
7365
|
-
inbound.connectionId,
|
|
7366
|
-
new Set(capabilities.filter((capability): capability is string => typeof capability === "string")),
|
|
7367
|
-
);
|
|
7946
|
+
if (liveHostConnection(inbound.connectionId)) hostAttachedConnections.add(inbound.connectionId);
|
|
7368
7947
|
}
|
|
7948
|
+
if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
|
|
7369
7949
|
inboundSdkFrame?.(inbound.connectionId, typedFrame);
|
|
7370
7950
|
} catch (error) {
|
|
7371
7951
|
sendMalformed(
|
|
@@ -7398,15 +7978,26 @@ export function createNotificationsExtension(
|
|
|
7398
7978
|
);
|
|
7399
7979
|
}
|
|
7400
7980
|
server.onNegotiatedCapabilities((_err, connectionId, capabilities) => {
|
|
7401
|
-
|
|
7981
|
+
// ThreadsafeFunction currently delivers its tuple payload as the second
|
|
7982
|
+
// callback argument at runtime, while older generated declarations expose
|
|
7983
|
+
// the tuple members as separate parameters. Accept both shapes without
|
|
7984
|
+
// allowing an unvalidated payload to become authority.
|
|
7985
|
+
const tuple = Array.isArray(connectionId) ? (connectionId as unknown[]) : undefined;
|
|
7986
|
+
const id = tuple?.[0] ?? connectionId;
|
|
7987
|
+
const negotiated = tuple?.[1] ?? capabilities;
|
|
7988
|
+
if (typeof id === "string" && Array.isArray(negotiated))
|
|
7989
|
+
rememberHostCapabilities(
|
|
7990
|
+
id,
|
|
7991
|
+
negotiated.filter((capability): capability is string => typeof capability === "string"),
|
|
7992
|
+
);
|
|
7402
7993
|
});
|
|
7403
7994
|
server.onConnectionClose((_err, connectionId) => {
|
|
7404
7995
|
if (!connectionId) return;
|
|
7996
|
+
closeHostConnection(connectionId);
|
|
7405
7997
|
void controlSurface
|
|
7406
7998
|
.cancelPendingPreflightsForConnection(connectionId)
|
|
7407
7999
|
.catch(error => logger.warn(`sdk: failed to cancel disconnected preflight: ${String(error)}`));
|
|
7408
8000
|
host.handleDisconnect(connectionId);
|
|
7409
|
-
hostCapCache.delete(connectionId);
|
|
7410
8001
|
// The socket is gone, so its fence has nothing left to refuse. Dropping the
|
|
7411
8002
|
// entry keeps the set bounded by live connections instead of growing forever.
|
|
7412
8003
|
fencedConnections.delete(connectionId);
|
|
@@ -7672,7 +8263,7 @@ export function createNotificationsExtension(
|
|
|
7672
8263
|
// thread (forwarded by the daemon over the WS, fail-closed at the daemon).
|
|
7673
8264
|
server.onInbound(async (err, inbound) => {
|
|
7674
8265
|
if (err || !inbound) return;
|
|
7675
|
-
const notificationOrigin =
|
|
8266
|
+
const notificationOrigin = liveHostCapabilities(inbound.connectionId)?.has(ASK_SELECTED_ACK_CAPABILITY);
|
|
7676
8267
|
const admission = notificationInboundAdmission({
|
|
7677
8268
|
inboundFenced: initializedRuntime.inboundFenced,
|
|
7678
8269
|
policySuspended: runtime?.policySuspended ?? false,
|
|
@@ -7777,6 +8368,8 @@ export function createNotificationsExtension(
|
|
|
7777
8368
|
]
|
|
7778
8369
|
: text;
|
|
7779
8370
|
let acceptedSent = false;
|
|
8371
|
+
const deliverAsSteer = runtime?.busy === true;
|
|
8372
|
+
const admission = createAdmissionWorkLease(workLease, deliverAsSteer, promoteWorkLease);
|
|
7780
8373
|
const acceptAdmission = (): void => {
|
|
7781
8374
|
// Fired by AgentSession's preflight acceptance: after admission has
|
|
7782
8375
|
// committed but before the turn starts. Registering the update id and
|
|
@@ -7784,6 +8377,7 @@ export function createNotificationsExtension(
|
|
|
7784
8377
|
// pendingInbound registration it consumes.
|
|
7785
8378
|
if (acceptedSent) return;
|
|
7786
8379
|
acceptedSent = true;
|
|
8380
|
+
admission.accepted();
|
|
7787
8381
|
if (runtime && typeof inbound.updateId === "number") runtime.pendingInbound.add(inbound.updateId);
|
|
7788
8382
|
sendInboundAck(authenticatedInbound.connectionId, authenticatedInbound, "accepted");
|
|
7789
8383
|
};
|
|
@@ -7793,12 +8387,15 @@ export function createNotificationsExtension(
|
|
|
7793
8387
|
// of after the await; a rejection (preflight, admission, or later)
|
|
7794
8388
|
// still reaches the catch and maps to a rejected ack.
|
|
7795
8389
|
await api.sendUserMessage(content, {
|
|
7796
|
-
...(
|
|
8390
|
+
...(deliverAsSteer ? { deliverAs: "steer" as const } : {}),
|
|
7797
8391
|
onPreflightAcceptCommit: acceptAdmission,
|
|
7798
8392
|
onPreflightAccepted: acceptAdmission,
|
|
8393
|
+
...admission.hooks,
|
|
7799
8394
|
});
|
|
7800
8395
|
acceptAdmission();
|
|
8396
|
+
admission.dispatched();
|
|
7801
8397
|
} catch (e) {
|
|
8398
|
+
admission.abandon();
|
|
7802
8399
|
sendInboundAck(
|
|
7803
8400
|
authenticatedInbound.connectionId,
|
|
7804
8401
|
authenticatedInbound,
|
|
@@ -7923,15 +8520,21 @@ export function createNotificationsExtension(
|
|
|
7923
8520
|
}
|
|
7924
8521
|
|
|
7925
8522
|
// The native server owns the only authoritative view of this host's live
|
|
7926
|
-
// SDK client sockets; publish it
|
|
7927
|
-
// own lifetime without probing the OS
|
|
7928
|
-
//
|
|
8523
|
+
// SDK client sockets; publish it with observer-only sockets separated so a
|
|
8524
|
+
// detached session host can bound its own lifetime without probing the OS
|
|
8525
|
+
// (#4010). Work is represented by the session's single lease, not by
|
|
8526
|
+
// reconstructed busy/correlation fields. The handle is this runtime's alone,
|
|
8527
|
+
// so only this runtime's teardown can retract it.
|
|
7929
8528
|
initializedRuntime.evidencePublication = publishSessionHostRuntimeEvidence({
|
|
7930
8529
|
attachedClients: () => server.clientCount(),
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
8530
|
+
observerClients: () => {
|
|
8531
|
+
if (workLease.isHeld()) return 0;
|
|
8532
|
+
const observers = [...hostCapCache.keys()].filter(connectionId =>
|
|
8533
|
+
liveHostCapabilities(connectionId)?.has(SESSION_HOST_OBSERVER_CAPABILITY),
|
|
8534
|
+
).length;
|
|
8535
|
+
return Math.min(server.clientCount(), observers);
|
|
8536
|
+
},
|
|
8537
|
+
workLease,
|
|
7935
8538
|
});
|
|
7936
8539
|
ephemeralTurns.configureAuthority({
|
|
7937
8540
|
sessionId: id,
|
|
@@ -8033,6 +8636,32 @@ export function createNotificationsExtension(
|
|
|
8033
8636
|
...(lifecycleRequestId ? { lifecycleRequestId } : {}),
|
|
8034
8637
|
});
|
|
8035
8638
|
},
|
|
8639
|
+
heartbeat: async input => {
|
|
8640
|
+
const expected = registration;
|
|
8641
|
+
if (
|
|
8642
|
+
!expected ||
|
|
8643
|
+
expected.sessionId !== input.sessionId ||
|
|
8644
|
+
expected.endpointGeneration !== input.endpointGeneration ||
|
|
8645
|
+
path.resolve(expected.locator.stateRoot) !== path.resolve(input.stateRoot)
|
|
8646
|
+
)
|
|
8647
|
+
return;
|
|
8648
|
+
await index.append({
|
|
8649
|
+
type: "host_heartbeat",
|
|
8650
|
+
sessionId: expected.sessionId,
|
|
8651
|
+
locator: expected.locator,
|
|
8652
|
+
endpointGeneration: expected.endpointGeneration,
|
|
8653
|
+
pid: expected.pid,
|
|
8654
|
+
...(expected.processIncarnation === undefined
|
|
8655
|
+
? {}
|
|
8656
|
+
: { processIncarnation: expected.processIncarnation }),
|
|
8657
|
+
...(expected.hostIncarnation === undefined
|
|
8658
|
+
? {}
|
|
8659
|
+
: { hostIncarnation: expected.hostIncarnation }),
|
|
8660
|
+
...(expected.masterRole === undefined ? {} : { masterRole: expected.masterRole }),
|
|
8661
|
+
activity: input.activity,
|
|
8662
|
+
ts: input.activity.at,
|
|
8663
|
+
});
|
|
8664
|
+
},
|
|
8036
8665
|
unregister: async input => {
|
|
8037
8666
|
const expected = registration;
|
|
8038
8667
|
if (
|
|
@@ -8048,8 +8677,8 @@ export function createNotificationsExtension(
|
|
|
8048
8677
|
});
|
|
8049
8678
|
throwIfLifecycleStopped();
|
|
8050
8679
|
initializedRuntime.brokerRegistrationActive = true;
|
|
8051
|
-
// Host liveness is derived from alive(pid)
|
|
8052
|
-
//
|
|
8680
|
+
// Host liveness is derived from alive(pid) and the coalesced heartbeats;
|
|
8681
|
+
// activity transitions are appended by the host through this registration.
|
|
8053
8682
|
} catch (brokerError) {
|
|
8054
8683
|
if (lifecycleRequired) throw brokerError;
|
|
8055
8684
|
logger.warn(`sdk broker registration skipped: ${String(brokerError)}`);
|
|
@@ -8869,6 +9498,9 @@ export function createNotificationsExtension(
|
|
|
8869
9498
|
const id = sessionId(ctx);
|
|
8870
9499
|
const rt = runtimes.get(id);
|
|
8871
9500
|
if (!rt) return;
|
|
9501
|
+
void rt.host
|
|
9502
|
+
.reportActivity("active")
|
|
9503
|
+
.catch(error => logger.warn(`notifications: active activity checkpoint failed: ${String(error)}`));
|
|
8872
9504
|
// Streaming state is SDK-visible session truth (context.get isStreaming);
|
|
8873
9505
|
// it is tracked regardless of whether notifications are active.
|
|
8874
9506
|
rt.busy = true;
|
|
@@ -8891,6 +9523,7 @@ export function createNotificationsExtension(
|
|
|
8891
9523
|
if (sdkRunToken && correlation) rt.pendingPromptCorrelationsBySdkRunToken.delete(sdkRunToken);
|
|
8892
9524
|
const continuation = rt.activePromptCorrelation !== undefined;
|
|
8893
9525
|
rt.activePromptCorrelation = correlation;
|
|
9526
|
+
beginAgentWork(rt);
|
|
8894
9527
|
if (correlation && !continuation) rt.bindPromptExecutionHandle(correlation, ctx.getActivePromptHandle());
|
|
8895
9528
|
if (continuation) return;
|
|
8896
9529
|
await rt.notePromptReconciliation(correlation, { type: "agent_start" });
|
|
@@ -8956,6 +9589,9 @@ export function createNotificationsExtension(
|
|
|
8956
9589
|
const id = sessionId(ctx);
|
|
8957
9590
|
const rt = runtimes.get(id);
|
|
8958
9591
|
if (!rt) return;
|
|
9592
|
+
void rt.host
|
|
9593
|
+
.reportActivity("idle")
|
|
9594
|
+
.catch(error => logger.warn(`notifications: idle activity checkpoint failed: ${String(error)}`));
|
|
8959
9595
|
// Clear the streaming flag for SDK consumers even when notifications are off.
|
|
8960
9596
|
rt.busy = false;
|
|
8961
9597
|
const correlation = rt.activePromptCorrelation;
|
|
@@ -9043,6 +9679,7 @@ export function createNotificationsExtension(
|
|
|
9043
9679
|
} else {
|
|
9044
9680
|
rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
|
|
9045
9681
|
}
|
|
9682
|
+
finishAgentWork(rt, event.stopReason === "paused");
|
|
9046
9683
|
rt.activePromptCorrelation = undefined;
|
|
9047
9684
|
terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "failed");
|
|
9048
9685
|
try {
|