@gajae-code/coding-agent 0.17.2 → 0.17.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +267 -0
- package/dist/types/cli/daemon-cli.d.ts +3 -1
- package/dist/types/cli/github-release.d.ts +17 -0
- package/dist/types/cli/public-command-entry.d.ts +56 -0
- package/dist/types/cli/public-command-errors.d.ts +126 -0
- package/dist/types/cli/public-command-evidence.d.ts +99 -0
- package/dist/types/cli/public-command-help.d.ts +65 -0
- package/dist/types/cli/public-command-registry.d.ts +69 -0
- package/dist/types/cli/session-picker.d.ts +1 -1
- package/dist/types/cli/setup-cli.d.ts +1 -0
- package/dist/types/cli/skills-cli.d.ts +5 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +4 -50
- package/dist/types/commands/sdk.d.ts +20 -37
- package/dist/types/commands/setup.d.ts +3 -0
- package/dist/types/commands/skills.d.ts +9 -0
- package/dist/types/config/autorouting-tier-map.d.ts +1049 -3
- package/dist/types/config/file-lock.d.ts +13 -3
- package/dist/types/config/model-profile-activation.d.ts +45 -1
- package/dist/types/config/model-registry.d.ts +14 -3
- package/dist/types/config/model-resolver.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +11 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
- package/dist/types/daemon/operator-contract.d.ts +5 -1
- package/dist/types/eval/js/worker-protocol.d.ts +2 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +12 -0
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +4 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +11 -2
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +18 -0
- package/dist/types/extensibility/shared-events.d.ts +3 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
- package/dist/types/harness-control-plane/control-endpoint.d.ts +4 -1
- package/dist/types/harness-control-plane/finalize.d.ts +14 -3
- package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/parse.d.ts +9 -1
- package/dist/types/main.d.ts +23 -1
- package/dist/types/modes/acp/acp-agent.d.ts +55 -1
- package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
- package/dist/types/modes/components/session-selector.d.ts +5 -2
- package/dist/types/modes/components/user-message-selector.d.ts +5 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/runtime/process-lifecycle.d.ts +2 -0
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +69 -6
- package/dist/types/runtime-mcp/pool.d.ts +17 -0
- package/dist/types/runtime-mcp/redaction.d.ts +20 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
- package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
- package/dist/types/sdk/broker/broker.d.ts +17 -4
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/identity.d.ts +5 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
- package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
- package/dist/types/sdk/broker/managed-task-dag.d.ts +1335 -0
- package/dist/types/sdk/broker/managed-task-verification.d.ts +37 -0
- package/dist/types/sdk/broker/runtime.d.ts +1 -1
- package/dist/types/sdk/broker/session-index.d.ts +22 -3
- package/dist/types/sdk/broker/spawn-authority.d.ts +12 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -0
- package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
- package/dist/types/sdk/bus/index.d.ts +19 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
- package/dist/types/sdk/cli/master-cli.d.ts +6 -10
- package/dist/types/sdk/cli/rows.d.ts +19 -1
- package/dist/types/sdk/cli/session-cli.d.ts +22 -5
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/guides/cli.d.ts +3 -4
- package/dist/types/sdk/guides/verify.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +9 -0
- package/dist/types/sdk/host/session-runtime.d.ts +31 -0
- package/dist/types/sdk/host/types.d.ts +10 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +85 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +37 -0
- package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +3 -3
- package/dist/types/sdk/startup-capability.d.ts +2 -0
- package/dist/types/sdk/transport/relay.d.ts +1 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +48 -4
- package/dist/types/sdk/transport/stdio.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +175 -23
- package/dist/types/session/fallback-chain-controller.d.ts +6 -6
- package/dist/types/session/fold-coordinator.d.ts +17 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
- package/dist/types/session/session-manager.d.ts +64 -1
- package/dist/types/session/session-work-lease.d.ts +14 -0
- package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
- package/dist/types/setup/provider-onboarding.d.ts +81 -1
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tools/bash-interactive.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +37 -11
- package/dist/types/tools/fetch.d.ts +10 -1
- package/dist/types/tools/job.d.ts +1 -1
- package/dist/types/tools/steer-fold.d.ts +31 -0
- package/dist/types/tools/subagent.d.ts +8 -1
- package/dist/types/utils/git.d.ts +28 -2
- package/dist/types/utils/mupdf-embedded.d.ts +2 -0
- package/dist/types/utils/mupdf.d.ts +4 -0
- package/dist/types/utils/session-title-generation.d.ts +6 -0
- package/dist/types/utils/title-generator.d.ts +11 -2
- package/dist/types/web/scrapers/types.d.ts +3 -0
- package/package.json +18 -10
- package/scripts/build-binary.ts +9 -3
- package/scripts/compile-args.ts +0 -9
- package/scripts/embed-mupdf.ts +52 -0
- package/scripts/generate-sdk-operation-inventory.ts +31 -1
- package/scripts/vendor-markit.ts +196 -0
- package/src/async/job-manager.ts +22 -1
- package/src/cli/daemon-cli.ts +53 -18
- package/src/cli/github-release.ts +87 -5
- package/src/cli/mcp-cli.ts +38 -4
- package/src/cli/public-command-entry.ts +450 -0
- package/src/cli/public-command-errors.ts +581 -0
- package/src/cli/public-command-evidence.ts +717 -0
- package/src/cli/public-command-help.ts +360 -0
- package/src/cli/public-command-registry.ts +541 -0
- package/src/cli/session-picker.ts +2 -1
- package/src/cli/setup-cli.ts +10 -1
- package/src/cli/skills-cli.ts +46 -2
- package/src/cli/stats-cli.ts +11 -1
- package/src/cli.ts +61 -1
- package/src/commands/daemon.ts +65 -69
- package/src/commands/sdk.ts +175 -117
- package/src/commands/setup.ts +2 -0
- package/src/commands/skills.ts +11 -0
- package/src/config/autorouting-tier-map.ts +543 -3
- package/src/config/file-lock.ts +206 -65
- package/src/config/model-discovery-manager.ts +4 -4
- package/src/config/model-profile-activation.ts +417 -66
- package/src/config/model-registry.ts +254 -36
- package/src/config/model-resolver.ts +6 -1
- package/src/config/settings-schema.ts +26 -2
- package/src/coordinator-mcp/policy.ts +10 -5
- package/src/coordinator-mcp/question-gate-codec.ts +17 -0
- package/src/coordinator-mcp/question-state.ts +179 -9
- package/src/coordinator-mcp/server.ts +521 -87
- package/src/coordinator-mcp/session-reaper.ts +6 -4
- package/src/crash/record-loader.ts +6 -2
- package/src/daemon/operator-contract.ts +26 -12
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
- package/src/eval/js/context-manager.ts +8 -8
- package/src/eval/js/worker-core.ts +9 -4
- package/src/eval/js/worker-protocol.ts +2 -0
- package/src/exec/bash-executor.ts +3 -0
- package/src/extensibility/extensions/runner.ts +52 -4
- package/src/extensibility/extensions/types.ts +10 -0
- package/src/extensibility/gjc-plugins/registry.ts +196 -37
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +46 -8
- package/src/extensibility/runtime-skill-discovery.ts +61 -3
- package/src/extensibility/shared-events.ts +3 -1
- package/src/gjc-runtime/session-state-lock.ts +165 -24
- package/src/gjc-runtime/state-writer.ts +317 -6
- package/src/gjc-runtime/ultragoal-succession.ts +31 -3
- package/src/harness-control-plane/control-endpoint.ts +24 -6
- package/src/harness-control-plane/finalize.ts +312 -25
- package/src/harness-control-plane/owner.ts +323 -89
- package/src/internal-urls/docs-index.generated.ts +23 -20
- package/src/internal-urls/embedded-protocol.ts +83 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/parse.ts +21 -1
- package/src/internal-urls/router.ts +4 -2
- package/src/main.ts +192 -74
- package/src/modes/acp/acp-agent.ts +973 -56
- package/src/modes/acp/startup-provenance.ts +59 -0
- package/src/modes/components/custom-provider-wizard.ts +308 -7
- package/src/modes/components/gjc-bundle-settings.ts +21 -1
- package/src/modes/components/oauth-selector.ts +9 -0
- package/src/modes/components/session-selector.ts +47 -4
- package/src/modes/components/tool-status-header.ts +72 -6
- package/src/modes/components/user-message-selector.ts +57 -15
- package/src/modes/controllers/command-controller.ts +58 -32
- package/src/modes/controllers/extension-ui-controller.ts +33 -7
- package/src/modes/controllers/input-controller.ts +4 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +161 -15
- package/src/modes/interactive-mode.ts +21 -7
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
- package/src/modes/types.ts +2 -2
- package/src/prompts/agent-fragments/browser-aside.md +1 -0
- package/src/prompts/system/title-system.md +1 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/read.md +2 -0
- package/src/runtime/network-prewarm-service.test.ts +36 -1
- package/src/runtime/network-prewarm-service.ts +28 -3
- package/src/runtime/process-lifecycle.ts +22 -11
- package/src/runtime-mcp/config.ts +16 -1
- package/src/runtime-mcp/loader.ts +6 -3
- package/src/runtime-mcp/manager.ts +845 -109
- package/src/runtime-mcp/pool.test.ts +31 -0
- package/src/runtime-mcp/pool.ts +304 -28
- package/src/runtime-mcp/redaction.ts +42 -0
- package/src/runtime-mcp/startup-policy.ts +7 -0
- package/src/runtime-mcp/tool-bridge.ts +71 -0
- package/src/runtime-mcp/transports/http.ts +108 -25
- package/src/sdk/acp/adapter.ts +27 -8
- package/src/sdk/broker/broker.ts +1431 -198
- package/src/sdk/broker/ensure.ts +21 -3
- package/src/sdk/broker/identity.ts +22 -1
- package/src/sdk/broker/lifecycle-ledger.ts +95 -31
- package/src/sdk/broker/lifecycle.ts +690 -141
- package/src/sdk/broker/managed-task-dag.ts +1927 -0
- package/src/sdk/broker/managed-task-verification.ts +642 -0
- package/src/sdk/broker/runtime.ts +1 -1
- package/src/sdk/broker/session-index.ts +97 -4
- package/src/sdk/broker/spawn-authority.ts +175 -14
- package/src/sdk/broker/startup-failure.ts +80 -1
- package/src/sdk/broker/stderr-drainer.ts +59 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/index.ts +707 -81
- package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -3
- package/src/sdk/cli/master-cli.ts +47 -36
- package/src/sdk/cli/rows.ts +30 -7
- package/src/sdk/cli/session-cli.ts +705 -184
- package/src/sdk/client/client.ts +18 -0
- package/src/sdk/guides/catalog.ts +2 -2
- package/src/sdk/guides/cli.ts +20 -62
- package/src/sdk/guides/verify.ts +14 -0
- package/src/sdk/host/host.ts +49 -5
- package/src/sdk/host/session-runtime.test.ts +184 -2
- package/src/sdk/host/session-runtime.ts +322 -101
- package/src/sdk/host/types.ts +12 -1
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
- package/src/sdk/host/websocket-transport.ts +40 -6
- package/src/sdk/index.ts +11 -0
- package/src/sdk/lifecycle/service.ts +2 -0
- package/src/sdk/prompt-deadline-flush.ts +356 -0
- package/src/sdk/prompt-deadline-manager.ts +133 -1
- package/src/sdk/prompt-tool-boundary.ts +121 -0
- package/src/sdk/protocol/operation-inventory.generated.json +106 -0
- package/src/sdk/router/session-router.ts +9 -1
- package/src/sdk/session.ts +763 -116
- package/src/sdk/startup-capability.ts +1 -1
- package/src/sdk/transport/relay.ts +71 -6
- package/src/sdk/transport/serve-cli.ts +397 -42
- package/src/sdk/transport/socket.ts +12 -11
- package/src/sdk/transport/stdio.ts +7 -3
- package/src/session/agent-session.ts +2472 -407
- package/src/session/fallback-chain-controller.ts +6 -7
- package/src/session/fold-coordinator.ts +24 -5
- package/src/session/internal/managed-session-scope.ts +68 -18
- package/src/session/internal/managed-session-storage.ts +334 -12
- package/src/session/session-manager.ts +382 -44
- package/src/session/session-work-lease.ts +34 -0
- package/src/setup/model-onboarding-guidance.ts +1 -1
- package/src/setup/provider-onboarding.ts +397 -24
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +259 -15
- package/src/task/executor.ts +46 -17
- package/src/task/index.ts +14 -2
- package/src/task/types.ts +2 -0
- package/src/tools/bash-interactive.ts +2 -0
- package/src/tools/bash.ts +118 -78
- package/src/tools/fetch.ts +140 -25
- package/src/tools/job.ts +151 -25
- package/src/tools/output-meta.ts +15 -2
- package/src/tools/path-utils.ts +27 -3
- package/src/tools/read.ts +3 -3
- package/src/tools/steer-fold.ts +107 -0
- package/src/tools/subagent-render.ts +24 -1
- package/src/tools/subagent.ts +27 -1
- package/src/tools/tool-catalog.generated.ts +29 -2
- package/src/tools/tool-errors.ts +4 -0
- package/src/utils/git.ts +53 -3
- package/src/utils/markit.ts +55 -41
- package/src/utils/mupdf-assets.d.ts +4 -0
- package/src/utils/mupdf-embedded.ts +4 -0
- package/src/utils/mupdf.ts +136 -0
- package/src/utils/session-title-generation.ts +29 -0
- package/src/utils/title-generator.ts +46 -2
- package/src/web/scrapers/types.ts +24 -3
- package/vendor/markit-ai/LICENSE +21 -0
- package/vendor/markit-ai/MANIFEST.json +412 -0
- package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
- package/vendor/markit-ai/dist/commands/config.js +139 -0
- package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
- package/vendor/markit-ai/dist/commands/convert.js +113 -0
- package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/formats.js +92 -0
- package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/init.js +29 -0
- package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/onboard.js +61 -0
- package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
- package/vendor/markit-ai/dist/commands/plugin.js +58 -0
- package/vendor/markit-ai/dist/config.d.ts +26 -0
- package/vendor/markit-ai/dist/config.js +42 -0
- package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
- package/vendor/markit-ai/dist/converters/audio.js +104 -0
- package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
- package/vendor/markit-ai/dist/converters/csv.js +83 -0
- package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/docx.js +57 -0
- package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/epub.js +128 -0
- package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
- package/vendor/markit-ai/dist/converters/github.js +148 -0
- package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/html.js +30 -0
- package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/image.js +124 -0
- package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
- package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
- package/vendor/markit-ai/dist/converters/iwork.js +391 -0
- package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/json.js +21 -0
- package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
- package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
- package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
- package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
- package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
- package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
- package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
- package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
- package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
- package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
- package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
- package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
- package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
- package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
- package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
- package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
- package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/pptx.js +289 -0
- package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
- package/vendor/markit-ai/dist/converters/rss.js +138 -0
- package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
- package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
- package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/xml.js +18 -0
- package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/yaml.js +17 -0
- package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/zip.js +57 -0
- package/vendor/markit-ai/dist/index.d.ts +30 -0
- package/vendor/markit-ai/dist/index.js +26 -0
- package/vendor/markit-ai/dist/main.d.ts +2 -0
- package/vendor/markit-ai/dist/main.js +191 -0
- package/vendor/markit-ai/dist/markit.d.ts +45 -0
- package/vendor/markit-ai/dist/markit.js +267 -0
- package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
- package/vendor/markit-ai/dist/plugins/api.js +42 -0
- package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
- package/vendor/markit-ai/dist/plugins/index.js +3 -0
- package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
- package/vendor/markit-ai/dist/plugins/installer.js +176 -0
- package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
- package/vendor/markit-ai/dist/plugins/loader.js +61 -0
- package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
- package/vendor/markit-ai/dist/plugins/types.js +1 -0
- package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
- package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
- package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
- package/vendor/markit-ai/dist/providers/index.js +56 -0
- package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
- package/vendor/markit-ai/dist/providers/openai.js +76 -0
- package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
- package/vendor/markit-ai/dist/providers/types.js +1 -0
- package/vendor/markit-ai/dist/types.d.ts +36 -0
- package/vendor/markit-ai/dist/types.js +1 -0
- package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
- package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
- package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
- package/vendor/markit-ai/dist/utils/output.js +31 -0
- package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
- package/vendor/markit-ai/dist/utils/turndown.js +70 -0
- package/vendor/markit-ai/package.json +74 -0
- package/vendor/markit-ai.patch +49 -0
|
@@ -60,7 +60,11 @@ import {
|
|
|
60
60
|
promptFailureRetryability,
|
|
61
61
|
rephaseFailedOutcome,
|
|
62
62
|
} from "../../sdk/prompt-failure";
|
|
63
|
-
import type {
|
|
63
|
+
import type {
|
|
64
|
+
SdkPromptFailureCategory,
|
|
65
|
+
SdkPromptFailurePhase,
|
|
66
|
+
SdkPromptTerminalOutcome,
|
|
67
|
+
} from "../../sdk/prompt-status";
|
|
64
68
|
import { PromptActivity, type PromptWatchdogClock, systemPromptWatchdogClock } from "../../sdk/prompt-watchdog";
|
|
65
69
|
import { validateRequiredPromptText } from "../../sdk/protocol/adapter-validation";
|
|
66
70
|
import { type SessionAttachment, SessionRouter, type SessionRouterFrame } from "../../sdk/router";
|
|
@@ -74,6 +78,7 @@ import {
|
|
|
74
78
|
} from "./acp-event-mapper";
|
|
75
79
|
import { resolveAcpPermissionMode } from "./permission-mode";
|
|
76
80
|
import type { AcpStartupOptions } from "./startup-options";
|
|
81
|
+
import { resolveStartupProvenance } from "./startup-provenance";
|
|
77
82
|
import { ACP_TERMINAL_AUTH_FLAG } from "./terminal-auth";
|
|
78
83
|
|
|
79
84
|
const ACP_DEFAULT_MODE_ID = "default";
|
|
@@ -87,6 +92,8 @@ const SESSION_PAGE_SIZE = 50;
|
|
|
87
92
|
const MAX_ACP_REPLAY_PAGES = 10_000;
|
|
88
93
|
/** Bounded retention of settled prompt correlations so late duplicates stay closed. */
|
|
89
94
|
const SETTLED_PROMPT_CORRELATION_RETENTION = 16;
|
|
95
|
+
/** Bounded valid terminal candidates retained while an uncertain prompt has only clientRef identity. */
|
|
96
|
+
const PROVISIONAL_ABORT_TERMINAL_CANDIDATE_RETENTION = 16;
|
|
90
97
|
/**
|
|
91
98
|
* A cancelled prompt must still settle. The SDK acknowledges `turn.abort` before the
|
|
92
99
|
* aborted run publishes its normalized terminal, and agent-owned async work that
|
|
@@ -159,6 +166,9 @@ const ACP_FIRST_PROMPT_RETRY_BASE_DELAY_MS = 250;
|
|
|
159
166
|
type JsonObject = Record<string, unknown>;
|
|
160
167
|
interface PromptWaiter {
|
|
161
168
|
acknowledged: boolean;
|
|
169
|
+
invocationKind: "prompt" | "skill";
|
|
170
|
+
/** ACP-owned identity, never inherited from caller metadata or reused for replay. */
|
|
171
|
+
clientRef: string;
|
|
162
172
|
/** True once turn.prompt / skill.invoke has been sent; cancel must not fake-settle after this. */
|
|
163
173
|
dispatched: boolean;
|
|
164
174
|
/** True only while the dispatched control request can still reveal its correlation. */
|
|
@@ -217,6 +227,15 @@ interface PromptWaiter {
|
|
|
217
227
|
}
|
|
218
228
|
|
|
219
229
|
type PromptCorrelation = { commandId?: string; turnId?: string };
|
|
230
|
+
type UncertainAbortOwner = {
|
|
231
|
+
kind: PromptWaiter["invocationKind"];
|
|
232
|
+
clientRef: string;
|
|
233
|
+
correlation?: { commandId: string; turnId: string };
|
|
234
|
+
};
|
|
235
|
+
type UncertainPromptRecoveryResult =
|
|
236
|
+
| { kind: "terminal" }
|
|
237
|
+
| { kind: "uncertain"; error: AcpSdkAdapterError }
|
|
238
|
+
| { kind: "ownership_lost" };
|
|
220
239
|
type PromptPhaseOwner = PromptWaiter | "background" | undefined;
|
|
221
240
|
|
|
222
241
|
type BrokerConnection = { adapter: AcpSdkAdapter; client: SdkClient };
|
|
@@ -253,6 +272,11 @@ type SessionRecord = {
|
|
|
253
272
|
/** Replayable startup notice captured before ACP bootstrap; emitted once after the session id is known. */
|
|
254
273
|
routingInactiveNotice?: string;
|
|
255
274
|
activePrompt?: PromptWaiter;
|
|
275
|
+
/** One bounded read-only recovery owner for this attachment and waiter. */
|
|
276
|
+
statusRecovery?: PromptWaiter;
|
|
277
|
+
statusRecoveryTask?: Promise<UncertainPromptRecoveryResult>;
|
|
278
|
+
/** An uncertain abort's exact late terminal is required before a successor may start. */
|
|
279
|
+
uncertainAbortOwner?: UncertainAbortOwner;
|
|
256
280
|
/** Set by `session/cancel` so an in-flight prompt settles as `cancelled`, never as an error. */
|
|
257
281
|
cancelRequested?: boolean;
|
|
258
282
|
/** True once the session's first logical prompt has settled; gates first-turn readiness retries. */
|
|
@@ -597,6 +621,14 @@ function sdkFrameCorrelation(frame: JsonObject, event?: JsonObject): PromptCorre
|
|
|
597
621
|
return strictCorrelationFrom(frame, event);
|
|
598
622
|
}
|
|
599
623
|
|
|
624
|
+
function sdkFrameBelongsToSession(frame: JsonObject, event: JsonObject | undefined, sessionId: string): boolean {
|
|
625
|
+
return (
|
|
626
|
+
frame.routerSessionId === sessionId &&
|
|
627
|
+
(frame.sessionId === undefined || frame.sessionId === sessionId) &&
|
|
628
|
+
(event?.sessionId === undefined || event.sessionId === sessionId)
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
600
632
|
/**
|
|
601
633
|
* Conflicting envelope/event identities own no prompt and therefore cannot
|
|
602
634
|
* refresh a watchdog or publish into either turn.
|
|
@@ -608,7 +640,7 @@ function watchdogCorrelationFrom(frame: JsonObject, event?: JsonObject): PromptC
|
|
|
608
640
|
function logDroppedPromptTerminal(
|
|
609
641
|
sessionId: string,
|
|
610
642
|
event: JsonObject,
|
|
611
|
-
reason: "incomplete_correlation" | "correlation_mismatch",
|
|
643
|
+
reason: "incomplete_correlation" | "correlation_mismatch" | "session_mismatch",
|
|
612
644
|
actual: PromptCorrelation,
|
|
613
645
|
expected?: PromptCorrelation,
|
|
614
646
|
): void {
|
|
@@ -640,6 +672,46 @@ export class AcpPromptFailureError extends AcpSdkAdapterError {
|
|
|
640
672
|
}
|
|
641
673
|
}
|
|
642
674
|
|
|
675
|
+
/**
|
|
676
|
+
* Recover the shared prompt-failure projection for a direct SDK rejection.
|
|
677
|
+
*
|
|
678
|
+
* A terminal frame normally reaches {@link AcpPromptFailureError} through
|
|
679
|
+
* `#settlePrompt`, but a host may reject the `turn.prompt` control request
|
|
680
|
+
* itself with `code: "prompt_failed"`. That path never has a terminal outcome
|
|
681
|
+
* to retain, so translating the raw adapter error used to bypass
|
|
682
|
+
* `promptFailureWireData` and publish only the bare `code`/`details` pair.
|
|
683
|
+
* Keep the fallback deliberately narrow: only the two prompt terminal codes
|
|
684
|
+
* are upgraded, and any optional provider classifier is re-validated before
|
|
685
|
+
* it enters the existing classifier/projection seam.
|
|
686
|
+
*/
|
|
687
|
+
function promptFailureFromDirectError(error: unknown): SdkPromptFailedOutcome | undefined {
|
|
688
|
+
if (error instanceof AcpPromptFailureError) return error.failure;
|
|
689
|
+
let code: unknown;
|
|
690
|
+
let providerCode: unknown;
|
|
691
|
+
let phase: unknown;
|
|
692
|
+
try {
|
|
693
|
+
const candidate = error as {
|
|
694
|
+
code?: unknown;
|
|
695
|
+
providerCode?: unknown;
|
|
696
|
+
transportFailure?: { providerCode?: unknown };
|
|
697
|
+
phase?: unknown;
|
|
698
|
+
};
|
|
699
|
+
code = candidate?.code;
|
|
700
|
+
providerCode = candidate?.providerCode ?? candidate?.transportFailure?.providerCode;
|
|
701
|
+
phase = candidate?.phase;
|
|
702
|
+
} catch {
|
|
703
|
+
return undefined;
|
|
704
|
+
}
|
|
705
|
+
if (code !== "prompt_failed" && code !== "prompt_deadline_exceeded") return undefined;
|
|
706
|
+
return failedPromptOutcome({
|
|
707
|
+
code,
|
|
708
|
+
provenance: code === "prompt_deadline_exceeded" ? "deadline" : "agent_failed",
|
|
709
|
+
...(isSafePromptFailureCode(providerCode) ? { providerCode } : {}),
|
|
710
|
+
...(isSdkPromptFailurePhase(phase) ? { phase } : {}),
|
|
711
|
+
evidence: {},
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
643
715
|
/**
|
|
644
716
|
* The wire projection of a prompt terminal's classification (issue #5615).
|
|
645
717
|
*
|
|
@@ -655,12 +727,71 @@ export class AcpPromptFailureError extends AcpSdkAdapterError {
|
|
|
655
727
|
* dropped instead of becoming a leak of provider text (the redaction contract in
|
|
656
728
|
* `sanitizePromptFailure`). A dropped or absent code is omitted, never nulled.
|
|
657
729
|
*/
|
|
730
|
+
export const OPERATOR_UPSTREAM_LABEL = "Upstream provider failure";
|
|
731
|
+
export const OPERATOR_UPSTREAM_SUFFIX = ": the model provider ended this turn.";
|
|
732
|
+
/**
|
|
733
|
+
* `post_start` proves execution began, so tools may already have run and had effects before the
|
|
734
|
+
* provider died. The wording therefore qualifies the turn rather than absolving the task: saying
|
|
735
|
+
* the task was unaffected would assert something the bounded classifiers do not establish.
|
|
736
|
+
*/
|
|
737
|
+
export const OPERATOR_PARTIAL_WORK_NOTE =
|
|
738
|
+
" Work this turn had already performed may have taken effect; check before retrying.";
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Operator-facing wording for a post-start terminal, assembled ONLY from bounded safe tokens: the
|
|
742
|
+
* classifier `phase`, the `category`, and a `providerCode` that passes `isSafePromptFailureCode`.
|
|
743
|
+
* Raw provider text never reaches this function and must never reach it.
|
|
744
|
+
*
|
|
745
|
+
* Gated on BOTH `phase === "post_start"` and `category === "provider_transport"`. A submission-phase
|
|
746
|
+
* provider failure is rejected by the phase gate: nothing executed, so wording about a turn the
|
|
747
|
+
* provider ended would describe an event that never occurred.
|
|
748
|
+
*
|
|
749
|
+
* It claims only what those tokens prove — that the upstream provider ended the turn — and says
|
|
750
|
+
* nothing about whether the operator's work survived. `post_start` proves execution began, so the
|
|
751
|
+
* wording qualifies that earlier work may already have taken effect instead of absolving the task.
|
|
752
|
+
*
|
|
753
|
+
* This is additive. The wire `code`/`details` pair and every `PROMPT_FAILURE_MESSAGE_*` constant
|
|
754
|
+
* stay exactly as they are — pinned ACP core-v1 conformance asserts on them — so this wording
|
|
755
|
+
* travels alongside the redacted message instead of repurposing it.
|
|
756
|
+
*
|
|
757
|
+
* It says NOTHING about the worktree, deliberately. Nothing on this path inspects the worktree, so
|
|
758
|
+
* any sentence about uncommitted work would assert a check that never ran — reporting an
|
|
759
|
+
* uninspected tree as clean is the absence-of-evidence-as-evidence-of-absence error, and an
|
|
760
|
+
* operator who believed it would sweep work away. Silence is the only honest option until something
|
|
761
|
+
* actually looks.
|
|
762
|
+
*
|
|
763
|
+
* Returns `undefined` for every other category: `phase` and `category` are already on the wire, so
|
|
764
|
+
* restating them in prose tells an operator nothing they do not already have.
|
|
765
|
+
*/
|
|
766
|
+
export function postStartOperatorMessage(input: {
|
|
767
|
+
phase: SdkPromptFailurePhase;
|
|
768
|
+
category: SdkPromptFailureCategory;
|
|
769
|
+
providerCode?: string;
|
|
770
|
+
}): string | undefined {
|
|
771
|
+
// Phase first: a submission-phase provider failure never started executing, so wording about a
|
|
772
|
+
// turn that ended mid-flight would describe something that did not happen. `phase` is a bounded
|
|
773
|
+
// classifier already on the wire, so gating on it widens nothing.
|
|
774
|
+
if (input.phase !== "post_start") return undefined;
|
|
775
|
+
if (input.category !== "provider_transport") return undefined;
|
|
776
|
+
const code = isSafePromptFailureCode(input.providerCode) ? ` (${input.providerCode})` : "";
|
|
777
|
+
return `${OPERATOR_UPSTREAM_LABEL}${code}${OPERATOR_UPSTREAM_SUFFIX}${OPERATOR_PARTIAL_WORK_NOTE}`;
|
|
778
|
+
}
|
|
779
|
+
|
|
658
780
|
function promptFailureWireData(failure: SdkPromptFailedOutcome): Record<string, string> {
|
|
781
|
+
// `operatorMessage` says, in words, what the classifier tokens already say in codes: that the
|
|
782
|
+
// cause was an upstream provider problem rather than a failure of the operator's task. Built
|
|
783
|
+
// only from those same bounded tokens, so it widens nothing that reaches the wire.
|
|
784
|
+
const operatorMessage = postStartOperatorMessage({
|
|
785
|
+
phase: failure.phase,
|
|
786
|
+
category: failure.category,
|
|
787
|
+
...(failure.providerCode === undefined ? {} : { providerCode: failure.providerCode }),
|
|
788
|
+
});
|
|
659
789
|
return {
|
|
660
790
|
phase: failure.phase,
|
|
661
791
|
category: failure.category,
|
|
662
792
|
retryability: promptFailureRetryability(failure.category),
|
|
663
793
|
...(isSafePromptFailureCode(failure.providerCode) ? { providerCode: failure.providerCode } : {}),
|
|
794
|
+
...(operatorMessage === undefined ? {} : { operatorMessage }),
|
|
664
795
|
};
|
|
665
796
|
}
|
|
666
797
|
|
|
@@ -939,18 +1070,20 @@ const ROUTER_PASSTHROUGH_FRAME_TYPES = new Set([
|
|
|
939
1070
|
"reverse_request_cancelled",
|
|
940
1071
|
]);
|
|
941
1072
|
|
|
942
|
-
function acpFrameFromRouted(frame: SessionRouterFrame): JsonObject {
|
|
1073
|
+
function acpFrameFromRouted(frame: SessionRouterFrame, attachmentSessionId: string): JsonObject {
|
|
943
1074
|
if (
|
|
944
1075
|
frame.body.type === "activity" ||
|
|
945
1076
|
frame.body.type === "agent_start" ||
|
|
946
1077
|
frame.body.type === "agent_end" ||
|
|
947
1078
|
frame.body.type === "agent_failed"
|
|
948
1079
|
)
|
|
949
|
-
return frame.body;
|
|
950
|
-
if (typeof frame.body.type === "string" && ROUTER_PASSTHROUGH_FRAME_TYPES.has(frame.body.type))
|
|
1080
|
+
return { ...frame.body, routerSessionId: attachmentSessionId };
|
|
1081
|
+
if (typeof frame.body.type === "string" && ROUTER_PASSTHROUGH_FRAME_TYPES.has(frame.body.type))
|
|
1082
|
+
return { ...frame.body, routerSessionId: attachmentSessionId };
|
|
951
1083
|
const connectionId = typeof frame.body.connectionId === "string" ? frame.body.connectionId : undefined;
|
|
952
1084
|
return {
|
|
953
1085
|
type: "event",
|
|
1086
|
+
routerSessionId: attachmentSessionId,
|
|
954
1087
|
...(frame.name === undefined ? {} : { kind: frame.name }),
|
|
955
1088
|
...(frame.sessionId === undefined ? {} : { sessionId: frame.sessionId }),
|
|
956
1089
|
...(frame.commandId === undefined ? {} : { commandId: frame.commandId }),
|
|
@@ -1366,9 +1499,10 @@ export function acpRequestFailure(error: unknown): unknown {
|
|
|
1366
1499
|
// spread is empty for every other error, so their payloads are byte-identical to
|
|
1367
1500
|
// before. `code`/`details` and the JSON-RPC code itself are untouched either way:
|
|
1368
1501
|
// this enriches `data`, it does not restate the redacted message.
|
|
1502
|
+
const promptFailure = promptFailureFromDirectError(error);
|
|
1369
1503
|
const data =
|
|
1370
|
-
|
|
1371
|
-
? { code, details: message, ...promptFailureWireData(
|
|
1504
|
+
promptFailure !== undefined
|
|
1505
|
+
? { code, details: message, ...promptFailureWireData(promptFailure) }
|
|
1372
1506
|
: { code, details: message };
|
|
1373
1507
|
// An abandoned prompt additionally publishes the plan it never finished (issue #5669).
|
|
1374
1508
|
// Same rule as above: `code`/`details` and the JSON-RPC code are untouched, and an abandon
|
|
@@ -1507,10 +1641,12 @@ export class AcpAgent implements Agent {
|
|
|
1507
1641
|
readonly #pendingRouterFrames = new Map<string, Record<string, unknown>[]>();
|
|
1508
1642
|
#routerStartPromise: Promise<void> | undefined;
|
|
1509
1643
|
readonly #sessions = new Map<string, SessionRecord>();
|
|
1644
|
+
/** Exact abort owners survive SessionRecord replacement until terminal proof or retirement. */
|
|
1645
|
+
readonly #uncertainAbortOwners = new Map<string, UncertainAbortOwner>();
|
|
1510
1646
|
/** Retain settled prompt identities across automatic transport reattachment. */
|
|
1511
1647
|
readonly #retiredPromptCorrelations = new Map<string, PromptCorrelation[]>();
|
|
1512
1648
|
/** Prevent a successor admission while a retired prompt can still reveal its identity. */
|
|
1513
|
-
readonly #retiredPromptAcknowledgements = new
|
|
1649
|
+
readonly #retiredPromptAcknowledgements = new Map<string, PromptWaiter>();
|
|
1514
1650
|
/** Ready terminal metadata writes retain ownership across same-id record replacement. */
|
|
1515
1651
|
readonly #terminalMetadataTails = new Map<string, Promise<void>>();
|
|
1516
1652
|
/** Unstreamed terminal text retains transcript ownership across same-id replacement. */
|
|
@@ -1579,7 +1715,7 @@ export class AcpAgent implements Agent {
|
|
|
1579
1715
|
await adapter.attachmentReady(attachment);
|
|
1580
1716
|
},
|
|
1581
1717
|
onFrame: (attachment, frame) => {
|
|
1582
|
-
const acpFrame = acpFrameFromRouted(frame);
|
|
1718
|
+
const acpFrame = acpFrameFromRouted(frame, attachment.sessionId);
|
|
1583
1719
|
const adapter =
|
|
1584
1720
|
this.#sessions.get(attachment.sessionId)?.adapter ??
|
|
1585
1721
|
this.#pendingRouterAdapters.get(attachment.sessionId);
|
|
@@ -1824,6 +1960,7 @@ export class AcpAgent implements Agent {
|
|
|
1824
1960
|
{ sessionId: params.sessionId },
|
|
1825
1961
|
this.#lifecycleIdempotencyKey(params.sessionId, "session.delete"),
|
|
1826
1962
|
);
|
|
1963
|
+
this.#uncertainAbortOwners.delete(params.sessionId);
|
|
1827
1964
|
return {};
|
|
1828
1965
|
}
|
|
1829
1966
|
this.#beginTeardown(params.sessionId);
|
|
@@ -1855,6 +1992,7 @@ export class AcpAgent implements Agent {
|
|
|
1855
1992
|
this.#knownSessionMetadata.delete(params.sessionId);
|
|
1856
1993
|
this.#retiredPromptCorrelations.delete(params.sessionId);
|
|
1857
1994
|
this.#retiredPromptAcknowledgements.delete(params.sessionId);
|
|
1995
|
+
this.#uncertainAbortOwners.delete(params.sessionId);
|
|
1858
1996
|
return {};
|
|
1859
1997
|
}
|
|
1860
1998
|
throw error;
|
|
@@ -1866,6 +2004,7 @@ export class AcpAgent implements Agent {
|
|
|
1866
2004
|
{ sessionId: params.sessionId, sessionPath: saved, cwd, target: { path: cwd } },
|
|
1867
2005
|
this.#lifecycleIdempotencyKey(params.sessionId, "session.delete"),
|
|
1868
2006
|
);
|
|
2007
|
+
this.#uncertainAbortOwners.delete(params.sessionId);
|
|
1869
2008
|
this.#knownSessionCwds.delete(params.sessionId);
|
|
1870
2009
|
this.#ownedSessionIds.delete(params.sessionId);
|
|
1871
2010
|
this.#knownSessionMcpServers.delete(params.sessionId);
|
|
@@ -2094,6 +2233,11 @@ export class AcpAgent implements Agent {
|
|
|
2094
2233
|
if (record.pendingFirstPromptRetry && record.pendingFirstPromptRetry !== retryReservation)
|
|
2095
2234
|
throw new AcpSdkAdapterError("conflict", "ACP session is retrying its first prompt.");
|
|
2096
2235
|
if (record.activePrompt) throw new AcpSdkAdapterError("conflict", "ACP session already has an active prompt.");
|
|
2236
|
+
if (record.uncertainAbortOwner)
|
|
2237
|
+
throw new AcpSdkAdapterError(
|
|
2238
|
+
"conflict",
|
|
2239
|
+
"ACP session is still awaiting the uncertainly aborted turn's terminal.",
|
|
2240
|
+
);
|
|
2097
2241
|
if (record.authFailure) throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
|
|
2098
2242
|
if (this.#retiredPromptAcknowledgements.has(params.sessionId))
|
|
2099
2243
|
throw new AcpSdkAdapterError(
|
|
@@ -2112,6 +2256,7 @@ export class AcpAgent implements Agent {
|
|
|
2112
2256
|
|
|
2113
2257
|
const payload = acpPromptPayload(params.prompt);
|
|
2114
2258
|
const skillInvocation = acpSkillInvocation(params.prompt);
|
|
2259
|
+
const clientRef = randomUUID();
|
|
2115
2260
|
if (!skillInvocation) {
|
|
2116
2261
|
const promptError = validateRequiredPromptText("turn.prompt", {
|
|
2117
2262
|
text: payload.text,
|
|
@@ -2146,7 +2291,7 @@ export class AcpAgent implements Agent {
|
|
|
2146
2291
|
JSON.stringify({
|
|
2147
2292
|
type: "control_request",
|
|
2148
2293
|
operation: skillInvocation ? "skill.invoke" : "turn.prompt",
|
|
2149
|
-
input: skillInvocation ?? { text: payload.text, images: payload.images },
|
|
2294
|
+
input: { ...(skillInvocation ?? { text: payload.text, images: payload.images }), clientRef },
|
|
2150
2295
|
// AcpSdkAdapter.control passes `confirm: false` to SdkClient, which serializes
|
|
2151
2296
|
// the field even though it is not part of the skill input payload.
|
|
2152
2297
|
...(skillInvocation ? { confirm: false } : {}),
|
|
@@ -2168,6 +2313,8 @@ export class AcpAgent implements Agent {
|
|
|
2168
2313
|
record.promptObservedAssistantOutput = false;
|
|
2169
2314
|
const { promise: response, resolve, reject } = Promise.withResolvers<PromptResponse>();
|
|
2170
2315
|
const waiter: PromptWaiter = {
|
|
2316
|
+
invocationKind: skillInvocation ? "skill" : "prompt",
|
|
2317
|
+
clientRef,
|
|
2171
2318
|
acknowledged: false,
|
|
2172
2319
|
dispatched: false,
|
|
2173
2320
|
acknowledgementPending: false,
|
|
@@ -2212,11 +2359,11 @@ export class AcpAgent implements Agent {
|
|
|
2212
2359
|
} catch (error) {
|
|
2213
2360
|
waiter.awaitingProviderPreflight = false;
|
|
2214
2361
|
if (waiter.settled || record.activePrompt !== waiter) {
|
|
2215
|
-
this.#
|
|
2362
|
+
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2216
2363
|
return await response;
|
|
2217
2364
|
}
|
|
2218
2365
|
if (record.cancelRequested && waiter.cancelAcknowledged) {
|
|
2219
|
-
this.#
|
|
2366
|
+
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2220
2367
|
await this.#settleCancelledPrompt(params.sessionId, record, waiter);
|
|
2221
2368
|
return await response;
|
|
2222
2369
|
}
|
|
@@ -2224,18 +2371,22 @@ export class AcpAgent implements Agent {
|
|
|
2224
2371
|
record.activePrompt = undefined;
|
|
2225
2372
|
record.busy = record.backgroundBusy;
|
|
2226
2373
|
clearPromptWatchdog(waiter);
|
|
2227
|
-
|
|
2374
|
+
// #5526's guarded release replaces the unguarded delete: it only clears
|
|
2375
|
+
// the entry when it still belongs to THIS waiter, so a successor's
|
|
2376
|
+
// acknowledgement is never dropped. Strictly narrower than the delete
|
|
2377
|
+
// it supersedes; dev's watchdog clear above is unaffected.
|
|
2378
|
+
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2228
2379
|
void this.#publishPromptPhaseIdle(params.sessionId, record.adapter);
|
|
2229
2380
|
}
|
|
2230
2381
|
throw error;
|
|
2231
2382
|
}
|
|
2232
2383
|
waiter.awaitingProviderPreflight = false;
|
|
2233
2384
|
if (waiter.settled || record.activePrompt !== waiter) {
|
|
2234
|
-
this.#
|
|
2385
|
+
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2235
2386
|
return await response;
|
|
2236
2387
|
}
|
|
2237
2388
|
if (record.cancelRequested && waiter.cancelAcknowledged) {
|
|
2238
|
-
this.#
|
|
2389
|
+
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2239
2390
|
await this.#settleCancelledPrompt(params.sessionId, record, waiter);
|
|
2240
2391
|
return await response;
|
|
2241
2392
|
}
|
|
@@ -2278,14 +2429,18 @@ export class AcpAgent implements Agent {
|
|
|
2278
2429
|
}
|
|
2279
2430
|
|
|
2280
2431
|
waiter.acknowledgementPending = true;
|
|
2432
|
+
const promptAdapter = record.adapter;
|
|
2281
2433
|
const acknowledgementTask = (async (): Promise<PromptResponse> => {
|
|
2282
2434
|
if (waiter.settled || record.activePrompt !== waiter) return await response;
|
|
2283
2435
|
const acknowledgement = skillInvocation
|
|
2284
|
-
? await
|
|
2285
|
-
: await
|
|
2436
|
+
? await promptAdapter.control("skill.invoke", { ...skillInvocation, clientRef })
|
|
2437
|
+
: await promptAdapter.prompt({
|
|
2286
2438
|
text: payload.text,
|
|
2439
|
+
clientRef,
|
|
2287
2440
|
...(payload.images.length ? { images: payload.images } : {}),
|
|
2288
2441
|
});
|
|
2442
|
+
// A recovered waiter already owns its exact identity; a late ack cannot rebind it.
|
|
2443
|
+
if (waiter.settled && waiter.acknowledged) return await response;
|
|
2289
2444
|
|
|
2290
2445
|
const acknowledgementCorrelation = promptAcknowledgement(acknowledgement);
|
|
2291
2446
|
if (!acknowledgementCorrelation)
|
|
@@ -2293,6 +2448,74 @@ export class AcpAgent implements Agent {
|
|
|
2293
2448
|
"invalid_prompt_acknowledgement",
|
|
2294
2449
|
"SDK prompt acknowledgement must accept the prompt and include commandId and turnId.",
|
|
2295
2450
|
);
|
|
2451
|
+
if (
|
|
2452
|
+
this.#sessions.get(params.sessionId) !== record ||
|
|
2453
|
+
record.adapter !== promptAdapter ||
|
|
2454
|
+
record.activePrompt !== waiter
|
|
2455
|
+
) {
|
|
2456
|
+
const current = this.#sessions.get(params.sessionId);
|
|
2457
|
+
const provisionalOwner = current?.uncertainAbortOwner;
|
|
2458
|
+
if (
|
|
2459
|
+
provisionalOwner?.kind === waiter.invocationKind &&
|
|
2460
|
+
provisionalOwner.clientRef === waiter.clientRef &&
|
|
2461
|
+
provisionalOwner.correlation === undefined &&
|
|
2462
|
+
this.#hasRetiredPromptCorrelation(params.sessionId, record, acknowledgementCorrelation)
|
|
2463
|
+
) {
|
|
2464
|
+
// A stale ACK cannot bind a provisional abort owner. Keep the fence
|
|
2465
|
+
// until this session is retired or new exact evidence is available.
|
|
2466
|
+
waiter.deferredFrames.length = 0;
|
|
2467
|
+
return await response;
|
|
2468
|
+
}
|
|
2469
|
+
this.#bindUncertainAbortOwner(params.sessionId, waiter, acknowledgementCorrelation);
|
|
2470
|
+
this.#rememberSettledPromptCorrelation(params.sessionId, record, acknowledgementCorrelation);
|
|
2471
|
+
const retainedOwner = current?.uncertainAbortOwner;
|
|
2472
|
+
if (
|
|
2473
|
+
current &&
|
|
2474
|
+
retainedOwner?.kind === waiter.invocationKind &&
|
|
2475
|
+
retainedOwner.clientRef === waiter.clientRef &&
|
|
2476
|
+
retainedOwner.correlation &&
|
|
2477
|
+
correlationsExactlyMatch(retainedOwner.correlation, acknowledgementCorrelation)
|
|
2478
|
+
) {
|
|
2479
|
+
await current.frameTail;
|
|
2480
|
+
const deferredFrames = waiter.deferredFrames.splice(0);
|
|
2481
|
+
let reconciledDeferredTerminal = false;
|
|
2482
|
+
for (const deferred of deferredFrames) {
|
|
2483
|
+
const deferredEvent = receivedSdkEvent(deferred.frame)?.event;
|
|
2484
|
+
if (!deferredEvent) continue;
|
|
2485
|
+
const deferredOutcome = terminalOutcome(deferredEvent);
|
|
2486
|
+
const validDeferredTerminal =
|
|
2487
|
+
(deferredEvent.type === "agent_end" && deferredOutcome !== undefined) ||
|
|
2488
|
+
(deferredEvent.type === "agent_failed" && deferredOutcome?.kind === "failed");
|
|
2489
|
+
const deferredCorrelation = sdkFrameCorrelation(deferred.frame, deferredEvent);
|
|
2490
|
+
if (
|
|
2491
|
+
!validDeferredTerminal ||
|
|
2492
|
+
!sdkFrameBelongsToSession(deferred.frame, deferredEvent, params.sessionId) ||
|
|
2493
|
+
!deferredCorrelation ||
|
|
2494
|
+
!hasCompleteCorrelation(deferredCorrelation) ||
|
|
2495
|
+
!correlationsExactlyMatch(deferredCorrelation, acknowledgementCorrelation)
|
|
2496
|
+
)
|
|
2497
|
+
continue;
|
|
2498
|
+
waiter.terminalReserved = true;
|
|
2499
|
+
await this.#handleSdkFrame(
|
|
2500
|
+
params.sessionId,
|
|
2501
|
+
current.adapter,
|
|
2502
|
+
deferred.frame,
|
|
2503
|
+
current.publicationGeneration,
|
|
2504
|
+
);
|
|
2505
|
+
reconciledDeferredTerminal = true;
|
|
2506
|
+
break;
|
|
2507
|
+
}
|
|
2508
|
+
if (!reconciledDeferredTerminal)
|
|
2509
|
+
void this.#recoverUncertainAbortAfterLateAcknowledgement(
|
|
2510
|
+
params.sessionId,
|
|
2511
|
+
current.adapter,
|
|
2512
|
+
retainedOwner,
|
|
2513
|
+
);
|
|
2514
|
+
} else {
|
|
2515
|
+
waiter.deferredFrames.length = 0;
|
|
2516
|
+
}
|
|
2517
|
+
return await response;
|
|
2518
|
+
}
|
|
2296
2519
|
if (this.#hasRetiredPromptCorrelation(params.sessionId, record, acknowledgementCorrelation)) {
|
|
2297
2520
|
try {
|
|
2298
2521
|
const abortAcknowledgement = await record.adapter.cancel("turn");
|
|
@@ -2318,6 +2541,7 @@ export class AcpAgent implements Agent {
|
|
|
2318
2541
|
waiter.boundary = record.inboundSequence;
|
|
2319
2542
|
waiter.correlation = acknowledgementCorrelation;
|
|
2320
2543
|
waiter.acknowledged = true;
|
|
2544
|
+
this.#bindUncertainAbortOwner(params.sessionId, waiter, acknowledgementCorrelation);
|
|
2321
2545
|
if (promptWaiterRetired(record, waiter)) {
|
|
2322
2546
|
this.#rememberSettledPromptCorrelation(params.sessionId, record, acknowledgementCorrelation);
|
|
2323
2547
|
return await response;
|
|
@@ -2427,10 +2651,20 @@ export class AcpAgent implements Agent {
|
|
|
2427
2651
|
}
|
|
2428
2652
|
this.#settlePrompt(params.sessionId, record, waiter);
|
|
2429
2653
|
return await response;
|
|
2430
|
-
})()
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2654
|
+
})()
|
|
2655
|
+
.catch(async error => {
|
|
2656
|
+
if (
|
|
2657
|
+
!(error instanceof SdkClientError || error instanceof AcpSdkAdapterError) ||
|
|
2658
|
+
error.code !== "uncertain_after_send"
|
|
2659
|
+
)
|
|
2660
|
+
throw error;
|
|
2661
|
+
if (record.adapter === promptAdapter) this.#startUncertainPromptRecovery(params.sessionId, record, waiter);
|
|
2662
|
+
return await response;
|
|
2663
|
+
})
|
|
2664
|
+
.finally(() => {
|
|
2665
|
+
waiter.acknowledgementPending = false;
|
|
2666
|
+
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2667
|
+
});
|
|
2434
2668
|
// Settlement can win before the SDK answers `turn.prompt`; acknowledgement processing
|
|
2435
2669
|
// must remain alive to tombstone its eventual correlation without holding the ACP caller.
|
|
2436
2670
|
void acknowledgementTask.catch(() => undefined);
|
|
@@ -2541,6 +2775,49 @@ export class AcpAgent implements Agent {
|
|
|
2541
2775
|
}
|
|
2542
2776
|
waiter?.cancelAttemptResolve?.(true);
|
|
2543
2777
|
} catch (error) {
|
|
2778
|
+
let cancellationError = error;
|
|
2779
|
+
const abortDetails = error instanceof SdkClientError ? object(error.details) : undefined;
|
|
2780
|
+
if (
|
|
2781
|
+
error instanceof SdkClientError &&
|
|
2782
|
+
error.code === "uncertain_after_send" &&
|
|
2783
|
+
abortDetails?.operation === "turn.abort" &&
|
|
2784
|
+
waiter &&
|
|
2785
|
+
record.activePrompt === waiter &&
|
|
2786
|
+
waiter.dispatched &&
|
|
2787
|
+
!waiter.settled
|
|
2788
|
+
) {
|
|
2789
|
+
if (waiter.terminalReserved) {
|
|
2790
|
+
waiter.cancelAttemptResolve?.(true);
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
this.#retainUncertainAbortOwner(params.sessionId, record, waiter);
|
|
2794
|
+
let recovery = record.statusRecovery === waiter ? record.statusRecoveryTask : undefined;
|
|
2795
|
+
if (!recovery && this.#startUncertainPromptRecovery(params.sessionId, record, waiter))
|
|
2796
|
+
recovery = record.statusRecoveryTask;
|
|
2797
|
+
if (recovery) {
|
|
2798
|
+
const recoveryResult = await recovery;
|
|
2799
|
+
if (
|
|
2800
|
+
recoveryResult.kind === "terminal" ||
|
|
2801
|
+
(recoveryResult.kind === "ownership_lost" && waiter.terminalReserved)
|
|
2802
|
+
) {
|
|
2803
|
+
record.cancelRequested = false;
|
|
2804
|
+
waiter.cancelAttemptResolve?.(true);
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2807
|
+
cancellationError =
|
|
2808
|
+
recoveryResult.kind === "uncertain"
|
|
2809
|
+
? recoveryResult.error
|
|
2810
|
+
: new AcpSdkAdapterError(
|
|
2811
|
+
"terminal_uncertain",
|
|
2812
|
+
"ACP abort recovery lost ownership before exact terminal evidence was resolved. No mutation was replayed.",
|
|
2813
|
+
);
|
|
2814
|
+
} else {
|
|
2815
|
+
cancellationError = new AcpSdkAdapterError(
|
|
2816
|
+
"terminal_uncertain",
|
|
2817
|
+
"ACP abort could not retain recovery ownership. No mutation was replayed.",
|
|
2818
|
+
);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2544
2821
|
// With no active prompt the cancel intent normally clears — except when a first-turn
|
|
2545
2822
|
// retry is reserved across its backoff gap. That reservation owner has not yet observed
|
|
2546
2823
|
// the cancel; clearing it here would let the retry resubmit a turn the client cancelled
|
|
@@ -2565,7 +2842,7 @@ export class AcpAgent implements Agent {
|
|
|
2565
2842
|
waiter.cancelAttempt = undefined;
|
|
2566
2843
|
waiter.cancelAttemptResolve = undefined;
|
|
2567
2844
|
}
|
|
2568
|
-
throw
|
|
2845
|
+
throw cancellationError;
|
|
2569
2846
|
} finally {
|
|
2570
2847
|
if (waiter) {
|
|
2571
2848
|
waiter.pendingCancelAttempts = Math.max(0, (waiter.pendingCancelAttempts ?? 1) - 1);
|
|
@@ -2596,6 +2873,22 @@ export class AcpAgent implements Agent {
|
|
|
2596
2873
|
if (!waiter.cancelAcknowledged) return;
|
|
2597
2874
|
if (this.#sessions.get(id) !== record || record.activePrompt !== waiter || waiter.settled) return;
|
|
2598
2875
|
if (waiter.terminalReserved) return;
|
|
2876
|
+
// An overlapping acknowledged cancel does not supersede the earlier
|
|
2877
|
+
// uncertain abort's bounded terminal lookup. Let that exact read either
|
|
2878
|
+
// prove a terminal or retire the waiter as still uncertain before grace
|
|
2879
|
+
// settlement can discard its foreground recovery owner.
|
|
2880
|
+
if (record.statusRecovery === waiter && this.#ownsUncertainAbort(record, waiter)) return;
|
|
2881
|
+
const uncertainAbortOwner = this.#uncertainAbortOwners.get(id);
|
|
2882
|
+
if (uncertainAbortOwner && this.#ownsUncertainAbort(record, waiter)) {
|
|
2883
|
+
record.uncertainAbortOwner = uncertainAbortOwner;
|
|
2884
|
+
const ownerCorrelation = uncertainAbortOwner.correlation;
|
|
2885
|
+
if (
|
|
2886
|
+
ownerCorrelation &&
|
|
2887
|
+
!record.backgroundCorrelations.some(owner => correlationsExactlyMatch(owner, ownerCorrelation))
|
|
2888
|
+
)
|
|
2889
|
+
record.backgroundCorrelations.push(ownerCorrelation);
|
|
2890
|
+
record.backgroundBusy = true;
|
|
2891
|
+
}
|
|
2599
2892
|
record.activePrompt = undefined;
|
|
2600
2893
|
if (!record.backgroundBusy) record.busy = false;
|
|
2601
2894
|
clearPromptWatchdog(waiter);
|
|
@@ -2862,11 +3155,7 @@ export class AcpAgent implements Agent {
|
|
|
2862
3155
|
this.#pendingRouterFrames.set(id, bufferedFrames);
|
|
2863
3156
|
try {
|
|
2864
3157
|
await this.#ensureRouterReady();
|
|
2865
|
-
|
|
2866
|
-
? await this.#router.adoptLifecycleResult(lifecycleResult, { sessionId: id, cwd })
|
|
2867
|
-
: this.#router.attachment(id);
|
|
2868
|
-
if (!attachment)
|
|
2869
|
-
throw new AcpSdkAdapterError("unavailable", `ACP session ${id} has no current Router attachment.`);
|
|
3158
|
+
if (lifecycleResult) await this.#router.adoptLifecycleResult(lifecycleResult, { sessionId: id, cwd });
|
|
2870
3159
|
let currentAttachment = this.#router.attachment(id);
|
|
2871
3160
|
for (let attempt = 0; !currentAttachment && attempt < 40; attempt++) {
|
|
2872
3161
|
await Bun.sleep(50);
|
|
@@ -2874,7 +3163,7 @@ export class AcpAgent implements Agent {
|
|
|
2874
3163
|
currentAttachment = this.#router.attachment(id);
|
|
2875
3164
|
}
|
|
2876
3165
|
if (!currentAttachment)
|
|
2877
|
-
throw new AcpSdkAdapterError("unavailable", `ACP session ${id}
|
|
3166
|
+
throw new AcpSdkAdapterError("unavailable", `ACP session ${id} has no current Router attachment.`);
|
|
2878
3167
|
adapter = new AcpSdkAdapter({
|
|
2879
3168
|
router: this.#router,
|
|
2880
3169
|
attachment: currentAttachment,
|
|
@@ -2889,24 +3178,23 @@ export class AcpAgent implements Agent {
|
|
|
2889
3178
|
// a broker-launched SDK host keeps ACP ownership across reconnects.
|
|
2890
3179
|
await adapter.start({ activateProviders: false });
|
|
2891
3180
|
let capabilities: JsonObject | undefined;
|
|
3181
|
+
// A query that never answered and a host that answered without provenance are
|
|
3182
|
+
// different operator problems, so the failure is retained instead of discarded.
|
|
3183
|
+
let capabilitiesFailure: unknown;
|
|
2892
3184
|
try {
|
|
2893
3185
|
const response = object(await adapter.query("runtime.capabilities"));
|
|
2894
3186
|
const result = object(response?.result) ?? response;
|
|
2895
3187
|
// Q18 is a paged query surface: the capability object arrives as the single
|
|
2896
3188
|
// page item, so fall back to the envelope only for direct-result hosts.
|
|
2897
3189
|
capabilities = object(pageItems(result)[0]) ?? result;
|
|
2898
|
-
} catch {
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
throw new AcpSdkAdapterError(
|
|
2907
|
-
"unavailable",
|
|
2908
|
-
"This ACP client requires a newer GJC SDK session with startup control provenance; restart the session.",
|
|
2909
|
-
);
|
|
3190
|
+
} catch (error) {
|
|
3191
|
+
capabilitiesFailure = error;
|
|
3192
|
+
}
|
|
3193
|
+
const primaryControlSurface = resolveStartupProvenance({
|
|
3194
|
+
sessionId: id,
|
|
3195
|
+
capabilities,
|
|
3196
|
+
queryFailure: capabilitiesFailure,
|
|
3197
|
+
});
|
|
2910
3198
|
if (primaryControlSurface === "sdk") {
|
|
2911
3199
|
adapter.authorizeProviderActivation();
|
|
2912
3200
|
await adapter.ensureProviders();
|
|
@@ -2916,6 +3204,7 @@ export class AcpAgent implements Agent {
|
|
|
2916
3204
|
const exactAttachment = this.#router.attachment(id) ?? currentAttachment;
|
|
2917
3205
|
if (!exactAttachment.isCurrent())
|
|
2918
3206
|
throw new AcpSdkAdapterError("unavailable", `ACP session ${id} lost exact Router authority.`);
|
|
3207
|
+
const retainedAbortOwner = this.#uncertainAbortOwners.get(id);
|
|
2919
3208
|
const record: SessionRecord = {
|
|
2920
3209
|
cwd,
|
|
2921
3210
|
adapter,
|
|
@@ -2929,11 +3218,12 @@ export class AcpAgent implements Agent {
|
|
|
2929
3218
|
settledPromptCorrelations: this.#retiredPromptCorrelations.get(id) ?? [],
|
|
2930
3219
|
inboundSequence: 0,
|
|
2931
3220
|
connectionId: adapter.connectionId,
|
|
2932
|
-
busy:
|
|
2933
|
-
backgroundBusy:
|
|
3221
|
+
busy: retainedAbortOwner !== undefined,
|
|
3222
|
+
backgroundBusy: retainedAbortOwner !== undefined,
|
|
2934
3223
|
backgroundAnonymousCount: 0,
|
|
2935
|
-
backgroundCorrelations: [],
|
|
3224
|
+
backgroundCorrelations: retainedAbortOwner?.correlation ? [retainedAbortOwner.correlation] : [],
|
|
2936
3225
|
toolArgs: new Map(),
|
|
3226
|
+
uncertainAbortOwner: retainedAbortOwner,
|
|
2937
3227
|
};
|
|
2938
3228
|
// Preserve first-turn state across reattachment (issue #5574). The startup-readiness
|
|
2939
3229
|
// retry only guards a session's very first logical prompt; a fresh record built on
|
|
@@ -2970,9 +3260,11 @@ export class AcpAgent implements Agent {
|
|
|
2970
3260
|
try {
|
|
2971
3261
|
await this.#teardownSession(id, "attachment failed", false);
|
|
2972
3262
|
} finally {
|
|
2973
|
-
this.#
|
|
3263
|
+
if (!this.#uncertainAbortOwners.has(id)) {
|
|
3264
|
+
this.#knownSessionCwds.delete(id);
|
|
3265
|
+
this.#knownSessionMcpServers.delete(id);
|
|
3266
|
+
}
|
|
2974
3267
|
this.#ownedSessionIds.delete(id);
|
|
2975
|
-
this.#knownSessionMcpServers.delete(id);
|
|
2976
3268
|
}
|
|
2977
3269
|
} else if (adapter) {
|
|
2978
3270
|
try {
|
|
@@ -3017,7 +3309,186 @@ export class AcpAgent implements Agent {
|
|
|
3017
3309
|
|
|
3018
3310
|
#fenceRetiredPromptAcknowledgement(id: string, waiter: PromptWaiter): void {
|
|
3019
3311
|
if (waiter.dispatched && waiter.acknowledgementPending && !waiter.acknowledged)
|
|
3020
|
-
this.#retiredPromptAcknowledgements.
|
|
3312
|
+
this.#retiredPromptAcknowledgements.set(id, waiter);
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
#releaseRetiredPromptAcknowledgement(id: string, waiter: PromptWaiter): void {
|
|
3316
|
+
if (this.#retiredPromptAcknowledgements.get(id) === waiter) this.#retiredPromptAcknowledgements.delete(id);
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
#retainUncertainAbortOwner(id: string, record: SessionRecord, waiter: PromptWaiter): void {
|
|
3320
|
+
let owner: UncertainAbortOwner = {
|
|
3321
|
+
kind: waiter.invocationKind,
|
|
3322
|
+
clientRef: waiter.clientRef,
|
|
3323
|
+
};
|
|
3324
|
+
if (hasCompleteCorrelation(waiter.correlation))
|
|
3325
|
+
owner = {
|
|
3326
|
+
...owner,
|
|
3327
|
+
correlation: { commandId: waiter.correlation.commandId, turnId: waiter.correlation.turnId },
|
|
3328
|
+
};
|
|
3329
|
+
const retained = this.#uncertainAbortOwners.get(id);
|
|
3330
|
+
if (
|
|
3331
|
+
retained &&
|
|
3332
|
+
(retained.kind !== owner.kind ||
|
|
3333
|
+
retained.clientRef !== owner.clientRef ||
|
|
3334
|
+
(retained.correlation !== undefined &&
|
|
3335
|
+
owner.correlation !== undefined &&
|
|
3336
|
+
!correlationsExactlyMatch(retained.correlation, owner.correlation)))
|
|
3337
|
+
) {
|
|
3338
|
+
record.uncertainAbortOwner = retained;
|
|
3339
|
+
return;
|
|
3340
|
+
}
|
|
3341
|
+
if (retained?.correlation && !owner.correlation) owner = retained;
|
|
3342
|
+
this.#uncertainAbortOwners.set(id, owner);
|
|
3343
|
+
record.uncertainAbortOwner = owner;
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
#bindUncertainAbortOwner(id: string, waiter: PromptWaiter, correlation: PromptCorrelation): void {
|
|
3347
|
+
if (!hasCompleteCorrelation(correlation)) return;
|
|
3348
|
+
const retained = this.#uncertainAbortOwners.get(id);
|
|
3349
|
+
if (!retained || retained.kind !== waiter.invocationKind || retained.clientRef !== waiter.clientRef) return;
|
|
3350
|
+
if (retained.correlation && !correlationsExactlyMatch(retained.correlation, correlation)) return;
|
|
3351
|
+
const boundCorrelation = { commandId: correlation.commandId, turnId: correlation.turnId };
|
|
3352
|
+
const owner: UncertainAbortOwner = {
|
|
3353
|
+
kind: retained.kind,
|
|
3354
|
+
clientRef: retained.clientRef,
|
|
3355
|
+
correlation: boundCorrelation,
|
|
3356
|
+
};
|
|
3357
|
+
this.#uncertainAbortOwners.set(id, owner);
|
|
3358
|
+
const current = this.#sessions.get(id);
|
|
3359
|
+
if (!current) return;
|
|
3360
|
+
current.uncertainAbortOwner = owner;
|
|
3361
|
+
if (current.activePrompt !== waiter || waiter.settled) {
|
|
3362
|
+
if (!current.backgroundCorrelations.some(background => correlationsExactlyMatch(background, correlation)))
|
|
3363
|
+
current.backgroundCorrelations.push(boundCorrelation);
|
|
3364
|
+
current.backgroundBusy = true;
|
|
3365
|
+
current.busy = true;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
async #recoverUncertainAbortAfterLateAcknowledgement(
|
|
3370
|
+
id: string,
|
|
3371
|
+
adapter: AcpSdkAdapter,
|
|
3372
|
+
owner: UncertainAbortOwner,
|
|
3373
|
+
): Promise<void> {
|
|
3374
|
+
const correlation = owner.correlation;
|
|
3375
|
+
if (!correlation) return;
|
|
3376
|
+
let timer: NodeJS.Timeout | undefined;
|
|
3377
|
+
let timedOut = false;
|
|
3378
|
+
try {
|
|
3379
|
+
const deadline = Promise.withResolvers<never>();
|
|
3380
|
+
timer = setTimeout(() => {
|
|
3381
|
+
timedOut = true;
|
|
3382
|
+
deadline.reject(new Error("status query timeout"));
|
|
3383
|
+
}, 5_000);
|
|
3384
|
+
const response = object(
|
|
3385
|
+
await Promise.race([
|
|
3386
|
+
adapter.query("turn.result", {
|
|
3387
|
+
kind: owner.kind,
|
|
3388
|
+
commandId: correlation.commandId,
|
|
3389
|
+
turnId: correlation.turnId,
|
|
3390
|
+
}),
|
|
3391
|
+
deadline.promise,
|
|
3392
|
+
]),
|
|
3393
|
+
);
|
|
3394
|
+
const status = object(response?.result) ?? response;
|
|
3395
|
+
const reportedCorrelation = strictCorrelationFrom(response, status);
|
|
3396
|
+
const outcome = status ? terminalOutcome(status) : undefined;
|
|
3397
|
+
const failure = object(status?.error);
|
|
3398
|
+
const conflictingIdentity = [response, status].some(
|
|
3399
|
+
value =>
|
|
3400
|
+
(value?.kind !== undefined && value.kind !== owner.kind) ||
|
|
3401
|
+
(value?.sessionId !== undefined && value.sessionId !== id) ||
|
|
3402
|
+
(value?.clientRef !== undefined && value.clientRef !== owner.clientRef),
|
|
3403
|
+
);
|
|
3404
|
+
const failedReceipt =
|
|
3405
|
+
status?.status === "failed" &&
|
|
3406
|
+
((outcome?.kind === "failed" &&
|
|
3407
|
+
(status.receiptState === "present" ||
|
|
3408
|
+
status.receiptState === "missing" ||
|
|
3409
|
+
status.receiptState === "unknown")) ||
|
|
3410
|
+
(outcome === undefined &&
|
|
3411
|
+
(status.receiptState === "present" ||
|
|
3412
|
+
status.receiptState === "missing" ||
|
|
3413
|
+
status.receiptState === "unknown") &&
|
|
3414
|
+
typeof failure?.code === "string" &&
|
|
3415
|
+
/^[a-zA-Z0-9_.-]{1,64}$/.test(failure.code) &&
|
|
3416
|
+
typeof failure.message === "string" &&
|
|
3417
|
+
failure.message.trim().length > 0 &&
|
|
3418
|
+
failure.message.length <= 512));
|
|
3419
|
+
const terminal =
|
|
3420
|
+
(status?.status === "terminal_ok" &&
|
|
3421
|
+
outcome?.kind === "stopped" &&
|
|
3422
|
+
(status.receiptState === "present" || status.receiptState === "missing")) ||
|
|
3423
|
+
failedReceipt;
|
|
3424
|
+
// The original prompt has already settled as terminal_uncertain on this
|
|
3425
|
+
// detached follow-up path. Missing receipt remains a failure classification;
|
|
3426
|
+
// this proof retires only the session ownership fence, never the old result
|
|
3427
|
+
// as a successful stop reason.
|
|
3428
|
+
if (
|
|
3429
|
+
!terminal ||
|
|
3430
|
+
conflictingIdentity ||
|
|
3431
|
+
status?.kind !== owner.kind ||
|
|
3432
|
+
!reportedCorrelation ||
|
|
3433
|
+
!hasCompleteCorrelation(reportedCorrelation) ||
|
|
3434
|
+
!correlationsExactlyMatch(correlation, reportedCorrelation)
|
|
3435
|
+
)
|
|
3436
|
+
return;
|
|
3437
|
+
const record = this.#sessions.get(id);
|
|
3438
|
+
const retained = this.#uncertainAbortOwners.get(id);
|
|
3439
|
+
if (
|
|
3440
|
+
!record ||
|
|
3441
|
+
record.adapter !== adapter ||
|
|
3442
|
+
retained?.kind !== owner.kind ||
|
|
3443
|
+
retained.clientRef !== owner.clientRef ||
|
|
3444
|
+
!retained.correlation ||
|
|
3445
|
+
!correlationsExactlyMatch(retained.correlation, correlation)
|
|
3446
|
+
)
|
|
3447
|
+
return;
|
|
3448
|
+
this.#clearUncertainAbortOwner(id, record, owner.kind, correlation);
|
|
3449
|
+
record.backgroundCorrelations = record.backgroundCorrelations.filter(
|
|
3450
|
+
background => !correlationsExactlyMatch(background, correlation),
|
|
3451
|
+
);
|
|
3452
|
+
record.backgroundBusy = record.backgroundAnonymousCount > 0 || record.backgroundCorrelations.length > 0;
|
|
3453
|
+
record.busy = record.backgroundBusy;
|
|
3454
|
+
void this.#publishPromptPhaseIdle(id, adapter);
|
|
3455
|
+
} catch {
|
|
3456
|
+
logger.warn("acp_uncertain_abort_late_ack_lookup_unresolved", {
|
|
3457
|
+
sessionId: id,
|
|
3458
|
+
reason: timedOut ? "timeout" : "unavailable",
|
|
3459
|
+
});
|
|
3460
|
+
} finally {
|
|
3461
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
#clearUncertainAbortOwner(
|
|
3466
|
+
id: string,
|
|
3467
|
+
record: SessionRecord,
|
|
3468
|
+
kind: PromptWaiter["invocationKind"],
|
|
3469
|
+
correlation: PromptCorrelation,
|
|
3470
|
+
): void {
|
|
3471
|
+
const owner = this.#uncertainAbortOwners.get(id);
|
|
3472
|
+
if (owner?.kind === kind && owner.correlation && correlationsExactlyMatch(owner.correlation, correlation))
|
|
3473
|
+
this.#uncertainAbortOwners.delete(id);
|
|
3474
|
+
const recordOwner = record.uncertainAbortOwner;
|
|
3475
|
+
if (
|
|
3476
|
+
recordOwner?.kind === kind &&
|
|
3477
|
+
recordOwner.correlation &&
|
|
3478
|
+
correlationsExactlyMatch(recordOwner.correlation, correlation)
|
|
3479
|
+
)
|
|
3480
|
+
record.uncertainAbortOwner = undefined;
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
#ownsUncertainAbort(record: SessionRecord, waiter: PromptWaiter): boolean {
|
|
3484
|
+
const owner = record.uncertainAbortOwner;
|
|
3485
|
+
return (
|
|
3486
|
+
owner?.kind === waiter.invocationKind &&
|
|
3487
|
+
owner.clientRef === waiter.clientRef &&
|
|
3488
|
+
(!owner.correlation ||
|
|
3489
|
+
!hasCompleteCorrelation(waiter.correlation) ||
|
|
3490
|
+
correlationsExactlyMatch(owner.correlation, waiter.correlation))
|
|
3491
|
+
);
|
|
3021
3492
|
}
|
|
3022
3493
|
|
|
3023
3494
|
#advanceTerminalGeneration(record: SessionRecord): void {
|
|
@@ -3027,6 +3498,23 @@ export class AcpAgent implements Agent {
|
|
|
3027
3498
|
#recoverSessionAfterTransportFailure(id: string, adapter: AcpSdkAdapter, error: Error): void {
|
|
3028
3499
|
const record = this.#sessions.get(id);
|
|
3029
3500
|
if (!record || record.adapter !== adapter) return;
|
|
3501
|
+
if (error instanceof SdkClientError && error.code === "uncertain_after_send") {
|
|
3502
|
+
const details = object(error.details);
|
|
3503
|
+
const abortWaiter = record.activePrompt;
|
|
3504
|
+
if (details?.operation === "turn.abort" && abortWaiter?.dispatched && !abortWaiter.settled) {
|
|
3505
|
+
if (abortWaiter.terminalReserved) return;
|
|
3506
|
+
this.#retainUncertainAbortOwner(id, record, abortWaiter);
|
|
3507
|
+
if (record.statusRecovery !== abortWaiter) this.#startUncertainPromptRecovery(id, record, abortWaiter);
|
|
3508
|
+
return;
|
|
3509
|
+
}
|
|
3510
|
+
// Provider-registration requests share this transport error. Recover only when the
|
|
3511
|
+
// prompt's own dispatched acknowledgement is still pending; ordinary acknowledged
|
|
3512
|
+
// turns continue until their terminal frame arrives.
|
|
3513
|
+
const waiter = record.activePrompt;
|
|
3514
|
+
if (waiter?.dispatched && waiter.acknowledgementPending && !waiter.acknowledged)
|
|
3515
|
+
this.#startUncertainPromptRecovery(id, record, waiter);
|
|
3516
|
+
return;
|
|
3517
|
+
}
|
|
3030
3518
|
if (
|
|
3031
3519
|
error instanceof SdkClientError &&
|
|
3032
3520
|
error.code !== "reconnect_exhausted" &&
|
|
@@ -3041,11 +3529,276 @@ export class AcpAgent implements Agent {
|
|
|
3041
3529
|
logger.warn(`ACP session ${id} non-transport error reached reconnect handler; ignoring terminal recovery.`);
|
|
3042
3530
|
return;
|
|
3043
3531
|
}
|
|
3532
|
+
const waiter = record.activePrompt;
|
|
3533
|
+
if (waiter?.terminalReserved && this.#ownsUncertainAbort(record, waiter)) return;
|
|
3534
|
+
if (waiter?.dispatched) {
|
|
3535
|
+
// A prior uncertainty notification already owns recovery for this waiter. Keep
|
|
3536
|
+
// the in-flight lookup alive; only declined, non-recovering waiters fall through
|
|
3537
|
+
// to the ordinary transport-loss teardown and reattachment path.
|
|
3538
|
+
if (record.statusRecovery === waiter || this.#startUncertainPromptRecovery(id, record, waiter)) return;
|
|
3539
|
+
}
|
|
3044
3540
|
const detail = error.message || "SDK transport reconnect failed.";
|
|
3045
3541
|
const terminal = new AcpSdkAdapterError("connection_closed", `ACP session transport was lost: ${detail}`);
|
|
3046
3542
|
void this.#recoverSessionAfterTransportFailureAsync(id, adapter, record.cwd, terminal);
|
|
3047
3543
|
}
|
|
3048
3544
|
|
|
3545
|
+
#startUncertainPromptRecovery(id: string, record: SessionRecord, waiter: PromptWaiter): boolean {
|
|
3546
|
+
if (
|
|
3547
|
+
this.#sessions.get(id) !== record ||
|
|
3548
|
+
promptWaiterRetired(record, waiter) ||
|
|
3549
|
+
!waiter.dispatched ||
|
|
3550
|
+
waiter.terminalReserved ||
|
|
3551
|
+
record.statusRecovery === waiter
|
|
3552
|
+
)
|
|
3553
|
+
return false;
|
|
3554
|
+
record.statusRecovery = waiter;
|
|
3555
|
+
clearPromptWatchdog(waiter);
|
|
3556
|
+
const recovery = this.#recoverUncertainPrompt(id, record, record.adapter, waiter);
|
|
3557
|
+
record.statusRecoveryTask = recovery;
|
|
3558
|
+
const clearRecovery = (): void => {
|
|
3559
|
+
if (record.statusRecovery !== waiter) return;
|
|
3560
|
+
record.statusRecovery = undefined;
|
|
3561
|
+
record.statusRecoveryTask = undefined;
|
|
3562
|
+
};
|
|
3563
|
+
void recovery.then(clearRecovery, clearRecovery);
|
|
3564
|
+
return true;
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
async #recoverUncertainPrompt(
|
|
3568
|
+
id: string,
|
|
3569
|
+
record: SessionRecord,
|
|
3570
|
+
adapter: AcpSdkAdapter,
|
|
3571
|
+
waiter: PromptWaiter,
|
|
3572
|
+
): Promise<UncertainPromptRecoveryResult> {
|
|
3573
|
+
const ownsRecovery = (): boolean =>
|
|
3574
|
+
this.#sessions.get(id) === record &&
|
|
3575
|
+
record.adapter === adapter &&
|
|
3576
|
+
record.statusRecovery === waiter &&
|
|
3577
|
+
!promptWaiterRetired(record, waiter) &&
|
|
3578
|
+
!waiter.terminalReserved;
|
|
3579
|
+
// Keep the lookup selector's authority even if an ack arrives during the read.
|
|
3580
|
+
const acknowledgedAtLookup = waiter.acknowledged;
|
|
3581
|
+
let detail = "status query unavailable";
|
|
3582
|
+
let timer: NodeJS.Timeout | undefined;
|
|
3583
|
+
let timedOut = false;
|
|
3584
|
+
try {
|
|
3585
|
+
const deadline = Promise.withResolvers<never>();
|
|
3586
|
+
timer = setTimeout(() => {
|
|
3587
|
+
timedOut = true;
|
|
3588
|
+
deadline.reject(new Error("status query timeout"));
|
|
3589
|
+
}, 5_000);
|
|
3590
|
+
const refreshAttachment = async (): Promise<void> => {
|
|
3591
|
+
if (this.#sessions.get(id) !== record || record.adapter !== adapter) return;
|
|
3592
|
+
// A reconnect can revoke the adapter's opaque attachment before Router has
|
|
3593
|
+
// published its replacement. Force an authority pass first, then hand the
|
|
3594
|
+
// exact current capability back to the adapter before issuing the read-only
|
|
3595
|
+
// lookup. This pass does not replay the prompt mutation.
|
|
3596
|
+
await this.#router.reconcile({ waitForReplay: false });
|
|
3597
|
+
if (!ownsRecovery()) return;
|
|
3598
|
+
const refreshed = this.#router.attachment(id);
|
|
3599
|
+
if (!refreshed?.isCurrent())
|
|
3600
|
+
throw new AcpSdkAdapterError(
|
|
3601
|
+
"unavailable",
|
|
3602
|
+
"ACP session has no current Router attachment for recovery.",
|
|
3603
|
+
);
|
|
3604
|
+
if (refreshed !== record.attachment || !record.attachment.isCurrent()) {
|
|
3605
|
+
await adapter.attachmentReady(refreshed);
|
|
3606
|
+
record.attachment = refreshed;
|
|
3607
|
+
}
|
|
3608
|
+
};
|
|
3609
|
+
await Promise.race([refreshAttachment(), deadline.promise]);
|
|
3610
|
+
if (!ownsRecovery()) return { kind: "ownership_lost" };
|
|
3611
|
+
// Bound observation only: the mutation and the read are never replayed or aborted.
|
|
3612
|
+
const response = object(
|
|
3613
|
+
await Promise.race([
|
|
3614
|
+
adapter.query("turn.result", {
|
|
3615
|
+
kind: waiter.invocationKind,
|
|
3616
|
+
...(acknowledgedAtLookup ? waiter.correlation : { clientRef: waiter.clientRef }),
|
|
3617
|
+
}),
|
|
3618
|
+
deadline.promise,
|
|
3619
|
+
]),
|
|
3620
|
+
);
|
|
3621
|
+
if (!ownsRecovery()) return { kind: "ownership_lost" };
|
|
3622
|
+
const status = object(response?.result) ?? response;
|
|
3623
|
+
const correlation = strictCorrelationFrom(response, status);
|
|
3624
|
+
const outcome = status ? terminalOutcome(status) : undefined;
|
|
3625
|
+
const content = object(status?.content);
|
|
3626
|
+
const readableText =
|
|
3627
|
+
content?.version === 1 &&
|
|
3628
|
+
content.type === "text" &&
|
|
3629
|
+
typeof content.text === "string" &&
|
|
3630
|
+
content.text.trim().length > 0;
|
|
3631
|
+
const failure = object(status?.error);
|
|
3632
|
+
const conflictingIdentity = [response, status].some(
|
|
3633
|
+
value =>
|
|
3634
|
+
(value?.kind !== undefined && value.kind !== waiter.invocationKind) ||
|
|
3635
|
+
(value?.sessionId !== undefined && value.sessionId !== id) ||
|
|
3636
|
+
(value?.clientRef !== undefined && value.clientRef !== waiter.clientRef),
|
|
3637
|
+
);
|
|
3638
|
+
if (
|
|
3639
|
+
status?.kind !== waiter.invocationKind ||
|
|
3640
|
+
conflictingIdentity ||
|
|
3641
|
+
(!acknowledgedAtLookup && status.clientRef !== waiter.clientRef) ||
|
|
3642
|
+
!correlation ||
|
|
3643
|
+
!hasCompleteCorrelation(correlation) ||
|
|
3644
|
+
(waiter.acknowledged && !correlationsExactlyMatch(waiter.correlation, correlation)) ||
|
|
3645
|
+
this.#hasRetiredPromptCorrelation(id, record, correlation)
|
|
3646
|
+
) {
|
|
3647
|
+
detail = "status correlation is missing, retired, or mismatched";
|
|
3648
|
+
} else if (
|
|
3649
|
+
status.status === "failed" &&
|
|
3650
|
+
status.outcome === undefined &&
|
|
3651
|
+
(status.receiptState === "present" ||
|
|
3652
|
+
status.receiptState === "missing" ||
|
|
3653
|
+
status.receiptState === "unknown") &&
|
|
3654
|
+
typeof failure?.code === "string" &&
|
|
3655
|
+
/^[a-zA-Z0-9_.-]{1,64}$/.test(failure.code) &&
|
|
3656
|
+
typeof failure.message === "string" &&
|
|
3657
|
+
failure.message.trim().length > 0 &&
|
|
3658
|
+
failure.message.length <= 512
|
|
3659
|
+
) {
|
|
3660
|
+
waiter.correlation = correlation;
|
|
3661
|
+
waiter.acknowledged = true;
|
|
3662
|
+
this.#bindUncertainAbortOwner(id, waiter, correlation);
|
|
3663
|
+
this.#clearUncertainAbortOwner(id, record, waiter.invocationKind, correlation);
|
|
3664
|
+
record.busy = record.backgroundBusy;
|
|
3665
|
+
this.#advanceTerminalGeneration(record);
|
|
3666
|
+
void this.#rejectPrompt(record, id, waiter, new AcpSdkAdapterError(failure.code, failure.message));
|
|
3667
|
+
return { kind: "terminal" };
|
|
3668
|
+
} else if (
|
|
3669
|
+
this.#ownsUncertainAbort(record, waiter) &&
|
|
3670
|
+
status.status === "terminal_ok" &&
|
|
3671
|
+
outcome?.kind === "stopped" &&
|
|
3672
|
+
outcome.reason === "end_turn" &&
|
|
3673
|
+
status.receiptState === "missing"
|
|
3674
|
+
) {
|
|
3675
|
+
waiter.correlation = correlation;
|
|
3676
|
+
waiter.acknowledged = true;
|
|
3677
|
+
this.#bindUncertainAbortOwner(id, waiter, correlation);
|
|
3678
|
+
this.#clearUncertainAbortOwner(id, record, waiter.invocationKind, correlation);
|
|
3679
|
+
record.busy = record.backgroundBusy;
|
|
3680
|
+
this.#advanceTerminalGeneration(record);
|
|
3681
|
+
void this.#rejectPrompt(
|
|
3682
|
+
record,
|
|
3683
|
+
id,
|
|
3684
|
+
waiter,
|
|
3685
|
+
new AcpSdkAdapterError(
|
|
3686
|
+
"prompt_failed",
|
|
3687
|
+
"Runtime completed without reportable final response text or artifact_path.",
|
|
3688
|
+
),
|
|
3689
|
+
);
|
|
3690
|
+
return { kind: "terminal" };
|
|
3691
|
+
} else if (
|
|
3692
|
+
(status.status === "terminal_ok" &&
|
|
3693
|
+
outcome?.kind === "stopped" &&
|
|
3694
|
+
(status.receiptState === "present" || status.receiptState === "missing") &&
|
|
3695
|
+
(outcome.reason !== "end_turn" || (status.receiptState === "present" && readableText))) ||
|
|
3696
|
+
(status.status === "failed" &&
|
|
3697
|
+
outcome?.kind === "failed" &&
|
|
3698
|
+
(status.receiptState === "present" ||
|
|
3699
|
+
status.receiptState === "missing" ||
|
|
3700
|
+
status.receiptState === "unknown"))
|
|
3701
|
+
) {
|
|
3702
|
+
waiter.correlation = correlation;
|
|
3703
|
+
waiter.acknowledged = true;
|
|
3704
|
+
this.#bindUncertainAbortOwner(id, waiter, correlation);
|
|
3705
|
+
this.#clearUncertainAbortOwner(id, record, waiter.invocationKind, correlation);
|
|
3706
|
+
record.busy = record.backgroundBusy;
|
|
3707
|
+
this.#advanceTerminalGeneration(record);
|
|
3708
|
+
const settledOutcome =
|
|
3709
|
+
record.cancelRequested && outcome.kind === "stopped" && outcome.reason === "end_turn"
|
|
3710
|
+
? { ...outcome, reason: "cancelled" as const, provenance: "client_cancel" as const }
|
|
3711
|
+
: outcome;
|
|
3712
|
+
const publicationBarrier = record.frameTail;
|
|
3713
|
+
waiter.terminal = { outcome: settledOutcome, correlation };
|
|
3714
|
+
// Recovered final text is published on the async terminal tail below. Record it
|
|
3715
|
+
// before settling so a first-turn startup-readiness failure cannot be retried
|
|
3716
|
+
// after this retained answer has already been committed for publication.
|
|
3717
|
+
if (readableText) record.promptObservedAssistantOutput = true;
|
|
3718
|
+
this.#settlePrompt(id, record, waiter);
|
|
3719
|
+
this.#scheduleTerminalUpdates(
|
|
3720
|
+
id,
|
|
3721
|
+
adapter,
|
|
3722
|
+
record.publicationGeneration,
|
|
3723
|
+
{
|
|
3724
|
+
type: settledOutcome.kind === "failed" ? "agent_failed" : "agent_end",
|
|
3725
|
+
outcome: settledOutcome,
|
|
3726
|
+
finalText: readableText ? content.text : undefined,
|
|
3727
|
+
finalTextTruncated: content?.truncated === true,
|
|
3728
|
+
},
|
|
3729
|
+
waiter,
|
|
3730
|
+
publicationBarrier,
|
|
3731
|
+
);
|
|
3732
|
+
return { kind: "terminal" };
|
|
3733
|
+
} else {
|
|
3734
|
+
if (!acknowledgedAtLookup) {
|
|
3735
|
+
waiter.correlation = correlation;
|
|
3736
|
+
waiter.acknowledged = true;
|
|
3737
|
+
this.#bindUncertainAbortOwner(id, waiter, correlation);
|
|
3738
|
+
const deferred = waiter.deferredFrames.find(({ frame }) => {
|
|
3739
|
+
const candidate = receivedSdkEvent(frame)?.event;
|
|
3740
|
+
if (!candidate) return false;
|
|
3741
|
+
const candidateOutcome = terminalOutcome(candidate);
|
|
3742
|
+
const validTerminal =
|
|
3743
|
+
(candidate.type === "agent_end" && candidateOutcome !== undefined) ||
|
|
3744
|
+
(candidate.type === "agent_failed" && candidateOutcome?.kind === "failed");
|
|
3745
|
+
const candidateCorrelation = sdkFrameCorrelation(frame, candidate);
|
|
3746
|
+
return (
|
|
3747
|
+
validTerminal &&
|
|
3748
|
+
sdkFrameBelongsToSession(frame, candidate, id) &&
|
|
3749
|
+
candidateCorrelation !== undefined &&
|
|
3750
|
+
correlationsExactlyMatch(candidateCorrelation, correlation)
|
|
3751
|
+
);
|
|
3752
|
+
});
|
|
3753
|
+
if (deferred) {
|
|
3754
|
+
waiter.deferredFrames.length = 0;
|
|
3755
|
+
waiter.terminalReserved = true;
|
|
3756
|
+
clearPromptWatchdog(waiter);
|
|
3757
|
+
await record.frameTail;
|
|
3758
|
+
if (
|
|
3759
|
+
this.#sessions.get(id) !== record ||
|
|
3760
|
+
record.adapter !== adapter ||
|
|
3761
|
+
record.activePrompt !== waiter ||
|
|
3762
|
+
waiter.settled
|
|
3763
|
+
)
|
|
3764
|
+
return { kind: "ownership_lost" };
|
|
3765
|
+
await this.#handleSdkFrame(id, adapter, deferred.frame, record.publicationGeneration);
|
|
3766
|
+
return { kind: "terminal" };
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
detail = "status has no usable retained terminal outcome and receipt";
|
|
3770
|
+
}
|
|
3771
|
+
} catch {
|
|
3772
|
+
detail = timedOut ? "status query timed out after 5000ms" : "status query unavailable";
|
|
3773
|
+
} finally {
|
|
3774
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
3775
|
+
}
|
|
3776
|
+
if (!ownsRecovery()) return { kind: "ownership_lost" };
|
|
3777
|
+
const uncertain = new AcpSdkAdapterError(
|
|
3778
|
+
"terminal_uncertain",
|
|
3779
|
+
`ACP prompt outcome remains uncertain: ${detail}. No mutation was replayed. Inspect turn.result for kind=${waiter.invocationKind}, clientRef=${waiter.clientRef}, commandId=${waiter.correlation.commandId ?? "unknown"}, turnId=${waiter.correlation.turnId ?? "unknown"} before submitting more work.`,
|
|
3780
|
+
);
|
|
3781
|
+
const uncertainAbortOwner = this.#uncertainAbortOwners.get(id);
|
|
3782
|
+
if (uncertainAbortOwner?.kind === waiter.invocationKind && uncertainAbortOwner.clientRef === waiter.clientRef) {
|
|
3783
|
+
record.uncertainAbortOwner = uncertainAbortOwner;
|
|
3784
|
+
const ownerCorrelation = uncertainAbortOwner.correlation;
|
|
3785
|
+
if (
|
|
3786
|
+
ownerCorrelation &&
|
|
3787
|
+
!record.backgroundCorrelations.some(owner => correlationsExactlyMatch(owner, ownerCorrelation))
|
|
3788
|
+
)
|
|
3789
|
+
record.backgroundCorrelations.push(ownerCorrelation);
|
|
3790
|
+
record.backgroundBusy = true;
|
|
3791
|
+
record.busy = true;
|
|
3792
|
+
record.cancelRequested = false;
|
|
3793
|
+
record.statusRecovery = undefined;
|
|
3794
|
+
record.statusRecoveryTask = undefined;
|
|
3795
|
+
await this.#rejectPrompt(record, id, waiter, uncertain);
|
|
3796
|
+
return { kind: "uncertain", error: uncertain };
|
|
3797
|
+
}
|
|
3798
|
+
await this.#failSession(id, adapter, uncertain);
|
|
3799
|
+
return { kind: "uncertain", error: uncertain };
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3049
3802
|
async #recoverSessionAfterTransportFailureAsync(
|
|
3050
3803
|
id: string,
|
|
3051
3804
|
adapter: AcpSdkAdapter,
|
|
@@ -3093,13 +3846,15 @@ export class AcpAgent implements Agent {
|
|
|
3093
3846
|
if (attaching) await Promise.allSettled([attaching.task]);
|
|
3094
3847
|
// A CLI-primary host owns its own process lifecycle: release this ACP
|
|
3095
3848
|
// attachment locally and never ask the broker to close the session.
|
|
3096
|
-
|
|
3849
|
+
const closesHost = this.#ownedSessionIds.has(id);
|
|
3850
|
+
await this.#teardownSession(id, "closed", closesHost);
|
|
3097
3851
|
this.#knownSessionCwds.delete(id);
|
|
3098
3852
|
this.#ownedSessionIds.delete(id);
|
|
3099
3853
|
this.#knownSessionMcpServers.delete(id);
|
|
3100
3854
|
this.#knownSessionMetadata.delete(id);
|
|
3101
3855
|
this.#retiredPromptCorrelations.delete(id);
|
|
3102
|
-
|
|
3856
|
+
// Detaching locally cannot retire an outstanding host acknowledgement.
|
|
3857
|
+
if (closesHost) this.#retiredPromptAcknowledgements.delete(id);
|
|
3103
3858
|
return {};
|
|
3104
3859
|
} finally {
|
|
3105
3860
|
this.#finishTeardown(id);
|
|
@@ -3187,7 +3942,10 @@ export class AcpAgent implements Agent {
|
|
|
3187
3942
|
.join("; ");
|
|
3188
3943
|
throw aggregateAcpFailure("terminal_uncertain", `ACP session cleanup is uncertain: ${detail}`, failures);
|
|
3189
3944
|
}
|
|
3190
|
-
if (closeRemote)
|
|
3945
|
+
if (closeRemote) {
|
|
3946
|
+
this.#pendingCloseIdempotencyKeys.delete(id);
|
|
3947
|
+
this.#uncertainAbortOwners.delete(id);
|
|
3948
|
+
}
|
|
3191
3949
|
} finally {
|
|
3192
3950
|
this.#finishTeardown(id);
|
|
3193
3951
|
}
|
|
@@ -3337,7 +4095,7 @@ export class AcpAgent implements Agent {
|
|
|
3337
4095
|
* running forever behind a dead session.
|
|
3338
4096
|
*/
|
|
3339
4097
|
#armPromptWatchdog(id: string, record: SessionRecord, waiter: PromptWaiter): void {
|
|
3340
|
-
if (waiter.settled) return;
|
|
4098
|
+
if (waiter.settled || record.statusRecovery === waiter) return;
|
|
3341
4099
|
waiter.cancelWatchdog?.();
|
|
3342
4100
|
// No frame can still arrive once the transport is known to be gone, so waiting out
|
|
3343
4101
|
// the full bound would only add dead time to an outcome that is already decided.
|
|
@@ -3360,6 +4118,7 @@ export class AcpAgent implements Agent {
|
|
|
3360
4118
|
const waiter = record.activePrompt;
|
|
3361
4119
|
if (!waiter || waiter.settled || waiter.terminalReserved) return;
|
|
3362
4120
|
const event = receivedSdkEvent(frame)?.event;
|
|
4121
|
+
if (event && !sdkFrameBelongsToSession(frame, event, id)) return;
|
|
3363
4122
|
const correlation = watchdogCorrelationFrom(frame, event);
|
|
3364
4123
|
if (!waiter.acknowledged) {
|
|
3365
4124
|
if (hasCorrelation(correlation)) waiter.deferredActivityFrames.push(frame);
|
|
@@ -3400,7 +4159,13 @@ export class AcpAgent implements Agent {
|
|
|
3400
4159
|
* turn can no longer be observed, it does not cancel or tear down the session.
|
|
3401
4160
|
*/
|
|
3402
4161
|
async #expirePromptWatchdog(id: string, record: SessionRecord, waiter: PromptWaiter): Promise<void> {
|
|
3403
|
-
if (
|
|
4162
|
+
if (
|
|
4163
|
+
record.activePrompt !== waiter ||
|
|
4164
|
+
waiter.settled ||
|
|
4165
|
+
waiter.terminalReserved ||
|
|
4166
|
+
record.statusRecovery === waiter
|
|
4167
|
+
)
|
|
4168
|
+
return;
|
|
3404
4169
|
const silenceMs = Math.max(0, this.#promptWatchdogClock.now() - waiter.lastFrameAt);
|
|
3405
4170
|
// A prompt still in preflight has never reached the host, so "stopped producing frames"
|
|
3406
4171
|
// would point the reader at the wrong phase: nothing was ever dispatched to produce them.
|
|
@@ -3448,6 +4213,20 @@ export class AcpAgent implements Agent {
|
|
|
3448
4213
|
#enqueueSdkFrame(id: string, adapter: AcpSdkAdapter, frame: JsonObject): void {
|
|
3449
4214
|
const record = this.#sessions.get(id);
|
|
3450
4215
|
if (!record || record.adapter !== adapter) return;
|
|
4216
|
+
const received = receivedSdkEvent(frame);
|
|
4217
|
+
const ingressEvent = received?.event;
|
|
4218
|
+
if (!sdkFrameBelongsToSession(frame, ingressEvent, id)) {
|
|
4219
|
+
if (ingressEvent?.type === "agent_end" || ingressEvent?.type === "agent_failed")
|
|
4220
|
+
logDroppedPromptTerminal(
|
|
4221
|
+
id,
|
|
4222
|
+
ingressEvent,
|
|
4223
|
+
"session_mismatch",
|
|
4224
|
+
sdkFrameCorrelation(frame, ingressEvent) ?? {},
|
|
4225
|
+
record.activePrompt?.correlation,
|
|
4226
|
+
);
|
|
4227
|
+
else logger.warn(`ACP session ${id} dropped an event from a foreign session identity.`);
|
|
4228
|
+
return;
|
|
4229
|
+
}
|
|
3451
4230
|
if (frame.type !== "hello" && frame.type !== "server_hello" && typeof frame.connectionId === "string")
|
|
3452
4231
|
record.connectionId = frame.connectionId;
|
|
3453
4232
|
// Ingress ordering is recorded before queued work begins.
|
|
@@ -3455,8 +4234,6 @@ export class AcpAgent implements Agent {
|
|
|
3455
4234
|
// Correlation is checked at ingress before a prompt-owned frame may refresh the
|
|
3456
4235
|
// watchdog, so queued processing cannot turn unrelated host traffic into turn liveness.
|
|
3457
4236
|
this.#refreshPromptWatchdog(id, record, frame);
|
|
3458
|
-
const received = receivedSdkEvent(frame);
|
|
3459
|
-
const ingressEvent = received?.event;
|
|
3460
4237
|
const ingressOutcome =
|
|
3461
4238
|
ingressEvent?.type === "agent_end" || ingressEvent?.type === "agent_failed"
|
|
3462
4239
|
? terminalOutcome(ingressEvent)
|
|
@@ -3506,6 +4283,19 @@ export class AcpAgent implements Agent {
|
|
|
3506
4283
|
await this.#settleCancelledPrompt(id, record, waiter);
|
|
3507
4284
|
return;
|
|
3508
4285
|
}
|
|
4286
|
+
if (waiter.terminalReserved) return;
|
|
4287
|
+
if (record.statusRecovery === waiter && this.#ownsUncertainAbort(record, waiter)) return;
|
|
4288
|
+
if (record.statusRecovery === waiter || (waiter.dispatched && !waiter.acknowledged)) {
|
|
4289
|
+
await this.#failSession(
|
|
4290
|
+
id,
|
|
4291
|
+
adapter,
|
|
4292
|
+
new AcpSdkAdapterError(
|
|
4293
|
+
"terminal_uncertain",
|
|
4294
|
+
`Prompt owner connection changed before retained evidence was resolved. No mutation was replayed. Inspect turn.result for kind=${waiter.invocationKind}, clientRef=${waiter.clientRef}.`,
|
|
4295
|
+
),
|
|
4296
|
+
);
|
|
4297
|
+
return;
|
|
4298
|
+
}
|
|
3509
4299
|
record.activePrompt = undefined;
|
|
3510
4300
|
record.busy = record.backgroundBusy;
|
|
3511
4301
|
clearPromptWatchdog(waiter);
|
|
@@ -3549,14 +4339,75 @@ export class AcpAgent implements Agent {
|
|
|
3549
4339
|
const ownsBackgroundTerminal =
|
|
3550
4340
|
ownedBackgroundCorrelation !== undefined ||
|
|
3551
4341
|
(!hasCorrelation(correlation) && record.backgroundAnonymousCount > 0);
|
|
4342
|
+
const provisionalAbortOwner = this.#uncertainAbortOwners.get(id);
|
|
4343
|
+
const retiredPromptAcknowledgement = this.#retiredPromptAcknowledgements.get(id);
|
|
4344
|
+
const validTerminalProof =
|
|
4345
|
+
(event.type === "agent_end" && outcome !== undefined) ||
|
|
4346
|
+
(event.type === "agent_failed" && outcome?.kind === "failed");
|
|
4347
|
+
if (isTerminal && !sdkFrameBelongsToSession(frame, event, id)) {
|
|
4348
|
+
logDroppedPromptTerminal(id, event, "session_mismatch", correlation, activePrompt?.correlation);
|
|
4349
|
+
return;
|
|
4350
|
+
}
|
|
4351
|
+
if (
|
|
4352
|
+
isTerminal &&
|
|
4353
|
+
!activePrompt &&
|
|
4354
|
+
provisionalAbortOwner &&
|
|
4355
|
+
!provisionalAbortOwner.correlation &&
|
|
4356
|
+
retiredPromptAcknowledgement?.settled &&
|
|
4357
|
+
retiredPromptAcknowledgement.acknowledgementPending &&
|
|
4358
|
+
!retiredPromptAcknowledgement.acknowledged &&
|
|
4359
|
+
provisionalAbortOwner.kind === retiredPromptAcknowledgement.invocationKind &&
|
|
4360
|
+
provisionalAbortOwner.clientRef === retiredPromptAcknowledgement.clientRef &&
|
|
4361
|
+
validTerminalProof &&
|
|
4362
|
+
hasCompleteCorrelation(correlation) &&
|
|
4363
|
+
sdkFrameBelongsToSession(frame, event, id) &&
|
|
4364
|
+
!this.#hasRetiredPromptCorrelation(id, record, correlation)
|
|
4365
|
+
) {
|
|
4366
|
+
// Query-by-clientRef has not yet yielded the command/turn identity. Retain
|
|
4367
|
+
// a bounded set of distinct candidates without publishing them until the
|
|
4368
|
+
// delayed prompt acknowledgement proves which exact correlation belongs.
|
|
4369
|
+
const duplicateCandidate = retiredPromptAcknowledgement.deferredFrames.some(({ frame: candidateFrame }) => {
|
|
4370
|
+
const candidateEvent = receivedSdkEvent(candidateFrame)?.event;
|
|
4371
|
+
const candidateCorrelation = candidateEvent
|
|
4372
|
+
? sdkFrameCorrelation(candidateFrame, candidateEvent)
|
|
4373
|
+
: undefined;
|
|
4374
|
+
return candidateCorrelation !== undefined && correlationsExactlyMatch(candidateCorrelation, correlation);
|
|
4375
|
+
});
|
|
4376
|
+
if (
|
|
4377
|
+
!duplicateCandidate &&
|
|
4378
|
+
retiredPromptAcknowledgement.deferredFrames.length < PROVISIONAL_ABORT_TERMINAL_CANDIDATE_RETENTION
|
|
4379
|
+
)
|
|
4380
|
+
retiredPromptAcknowledgement.deferredFrames.push({ frame, publicationGeneration });
|
|
4381
|
+
return;
|
|
4382
|
+
}
|
|
3552
4383
|
if (isTerminal) {
|
|
3553
4384
|
if (!activePrompt) {
|
|
3554
|
-
if (
|
|
4385
|
+
if (
|
|
4386
|
+
!record.backgroundBusy ||
|
|
4387
|
+
!ownsBackgroundTerminal ||
|
|
4388
|
+
(settledCorrelation && !ownedBackgroundCorrelation)
|
|
4389
|
+
)
|
|
4390
|
+
return;
|
|
4391
|
+
const uncertainAbortOwner = this.#uncertainAbortOwners.get(id);
|
|
4392
|
+
if (
|
|
4393
|
+
uncertainAbortOwner?.correlation &&
|
|
4394
|
+
ownedBackgroundCorrelation &&
|
|
4395
|
+
correlationsExactlyMatch(uncertainAbortOwner.correlation, ownedBackgroundCorrelation) &&
|
|
4396
|
+
!validTerminalProof
|
|
4397
|
+
)
|
|
4398
|
+
return;
|
|
3555
4399
|
if (ownedBackgroundCorrelation)
|
|
3556
4400
|
record.backgroundCorrelations = record.backgroundCorrelations.filter(
|
|
3557
4401
|
owner => !correlationsExactlyMatch(owner, ownedBackgroundCorrelation),
|
|
3558
4402
|
);
|
|
3559
4403
|
else record.backgroundAnonymousCount--;
|
|
4404
|
+
if (
|
|
4405
|
+
uncertainAbortOwner?.correlation &&
|
|
4406
|
+
ownedBackgroundCorrelation &&
|
|
4407
|
+
validTerminalProof &&
|
|
4408
|
+
correlationsExactlyMatch(uncertainAbortOwner.correlation, ownedBackgroundCorrelation)
|
|
4409
|
+
)
|
|
4410
|
+
this.#clearUncertainAbortOwner(id, record, uncertainAbortOwner.kind, ownedBackgroundCorrelation);
|
|
3560
4411
|
record.backgroundBusy = record.backgroundAnonymousCount > 0 || record.backgroundCorrelations.length > 0;
|
|
3561
4412
|
record.busy = record.backgroundBusy;
|
|
3562
4413
|
await this.#publishPromptPhase(id, record.adapter, undefined);
|
|
@@ -3605,6 +4456,18 @@ export class AcpAgent implements Agent {
|
|
|
3605
4456
|
this.#advanceTerminalGeneration(record);
|
|
3606
4457
|
publicationGeneration = record.publicationGeneration;
|
|
3607
4458
|
if (!outcome) {
|
|
4459
|
+
const uncertainAbortOwner = this.#uncertainAbortOwners.get(id);
|
|
4460
|
+
if (
|
|
4461
|
+
uncertainAbortOwner?.kind === activePrompt.invocationKind &&
|
|
4462
|
+
uncertainAbortOwner.correlation &&
|
|
4463
|
+
correlationsExactlyMatch(uncertainAbortOwner.correlation, correlation)
|
|
4464
|
+
) {
|
|
4465
|
+
record.uncertainAbortOwner = uncertainAbortOwner;
|
|
4466
|
+
if (!record.backgroundCorrelations.some(owner => correlationsExactlyMatch(owner, correlation)))
|
|
4467
|
+
record.backgroundCorrelations.push(uncertainAbortOwner.correlation);
|
|
4468
|
+
record.backgroundBusy = true;
|
|
4469
|
+
record.busy = true;
|
|
4470
|
+
}
|
|
3608
4471
|
await this.#rejectPrompt(
|
|
3609
4472
|
record,
|
|
3610
4473
|
id,
|
|
@@ -3613,6 +4476,13 @@ export class AcpAgent implements Agent {
|
|
|
3613
4476
|
);
|
|
3614
4477
|
return;
|
|
3615
4478
|
}
|
|
4479
|
+
const uncertainAbortOwner = this.#uncertainAbortOwners.get(id);
|
|
4480
|
+
if (
|
|
4481
|
+
uncertainAbortOwner?.kind === activePrompt.invocationKind &&
|
|
4482
|
+
uncertainAbortOwner.correlation &&
|
|
4483
|
+
correlationsExactlyMatch(uncertainAbortOwner.correlation, correlation)
|
|
4484
|
+
)
|
|
4485
|
+
this.#clearUncertainAbortOwner(id, record, uncertainAbortOwner.kind, correlation);
|
|
3616
4486
|
// When a client cancel has been requested, a trailing stopped terminal may
|
|
3617
4487
|
// still carry the normal `end_turn` reason (the model finished its response as
|
|
3618
4488
|
// the cancel arrived mid-stream, or the cancel was processed before the prompt
|
|
@@ -3796,13 +4666,15 @@ export class AcpAgent implements Agent {
|
|
|
3796
4666
|
publicationGeneration: number,
|
|
3797
4667
|
event: JsonObject,
|
|
3798
4668
|
promptOwner: PromptWaiter | undefined,
|
|
4669
|
+
publicationBarrier?: Promise<void>,
|
|
3799
4670
|
): void {
|
|
3800
4671
|
const failedTerminal = event.type === "agent_failed";
|
|
3801
4672
|
if (promptOwner) this.#flushFailureDiagnostics(id, promptOwner, adapter);
|
|
3802
|
-
let decorationStart = Promise.resolve();
|
|
4673
|
+
let decorationStart = publicationBarrier ?? Promise.resolve();
|
|
3803
4674
|
const finalText = typeof event.finalText === "string" ? event.finalText : "";
|
|
3804
4675
|
if (promptOwner && hasAcpFinalTextContent(finalText)) {
|
|
3805
4676
|
const finalTextTask = (async () => {
|
|
4677
|
+
if (publicationBarrier) await publicationBarrier;
|
|
3806
4678
|
await Bun.sleep(0);
|
|
3807
4679
|
const resolution = resolveAcpFinalText(promptOwner.emittedAssistantText, finalText);
|
|
3808
4680
|
if (resolution.kind === "emit") {
|
|
@@ -3814,7 +4686,9 @@ export class AcpAgent implements Agent {
|
|
|
3814
4686
|
update: {
|
|
3815
4687
|
sessionUpdate: "agent_message_chunk",
|
|
3816
4688
|
content: { type: "text", text: resolution.text },
|
|
3817
|
-
...(resolution.final.truncated
|
|
4689
|
+
...(resolution.final.truncated || event.finalTextTruncated === true
|
|
4690
|
+
? { _meta: { gjcFinalTextTruncated: true } }
|
|
4691
|
+
: {}),
|
|
3818
4692
|
},
|
|
3819
4693
|
},
|
|
3820
4694
|
adapter,
|
|
@@ -3829,6 +4703,24 @@ export class AcpAgent implements Agent {
|
|
|
3829
4703
|
finalLength: resolution.final.text.length,
|
|
3830
4704
|
});
|
|
3831
4705
|
}
|
|
4706
|
+
if (resolution.kind !== "emit" && event.finalTextTruncated === true) {
|
|
4707
|
+
// A retained prefix may already equal the streamed text, leaving no suffix for
|
|
4708
|
+
// `resolveAcpFinalText` to emit. Still publish a metadata-only chunk so the
|
|
4709
|
+
// caller can distinguish an incomplete retained result from a complete answer.
|
|
4710
|
+
await this.#publishSessionUpdate(
|
|
4711
|
+
id,
|
|
4712
|
+
{
|
|
4713
|
+
sessionId: id,
|
|
4714
|
+
update: {
|
|
4715
|
+
sessionUpdate: "agent_message_chunk",
|
|
4716
|
+
content: { type: "text", text: "" },
|
|
4717
|
+
_meta: { gjcFinalTextTruncated: true },
|
|
4718
|
+
},
|
|
4719
|
+
},
|
|
4720
|
+
adapter,
|
|
4721
|
+
publicationGeneration,
|
|
4722
|
+
);
|
|
4723
|
+
}
|
|
3832
4724
|
})();
|
|
3833
4725
|
let finalTextTail: Promise<void>;
|
|
3834
4726
|
finalTextTail = finalTextTask
|
|
@@ -3865,7 +4757,32 @@ export class AcpAgent implements Agent {
|
|
|
3865
4757
|
clearPromptWatchdog(waiter);
|
|
3866
4758
|
waiter.settled = true;
|
|
3867
4759
|
this.#fenceRetiredPromptAcknowledgement(id, waiter);
|
|
4760
|
+
const abortOwner = this.#uncertainAbortOwners.get(id);
|
|
4761
|
+
const preserveAbortTerminal =
|
|
4762
|
+
abortOwner?.kind === waiter.invocationKind &&
|
|
4763
|
+
abortOwner.clientRef === waiter.clientRef &&
|
|
4764
|
+
abortOwner.correlation === undefined &&
|
|
4765
|
+
waiter.acknowledgementPending &&
|
|
4766
|
+
!waiter.acknowledged;
|
|
4767
|
+
const candidateTerminal = preserveAbortTerminal
|
|
4768
|
+
? waiter.deferredFrames.find(({ frame }) => {
|
|
4769
|
+
const candidate = receivedSdkEvent(frame)?.event;
|
|
4770
|
+
if (!candidate) return false;
|
|
4771
|
+
const candidateOutcome = terminalOutcome(candidate);
|
|
4772
|
+
const validTerminal =
|
|
4773
|
+
(candidate.type === "agent_end" && candidateOutcome !== undefined) ||
|
|
4774
|
+
(candidate.type === "agent_failed" && candidateOutcome?.kind === "failed");
|
|
4775
|
+
const candidateCorrelation = sdkFrameCorrelation(frame, candidate);
|
|
4776
|
+
return (
|
|
4777
|
+
validTerminal &&
|
|
4778
|
+
sdkFrameBelongsToSession(frame, candidate, id) &&
|
|
4779
|
+
hasCompleteCorrelation(candidateCorrelation ?? {}) &&
|
|
4780
|
+
!this.#hasRetiredPromptCorrelation(id, record, candidateCorrelation ?? {})
|
|
4781
|
+
);
|
|
4782
|
+
})
|
|
4783
|
+
: undefined;
|
|
3868
4784
|
waiter.deferredFrames.length = 0;
|
|
4785
|
+
if (candidateTerminal) waiter.deferredFrames.push(candidateTerminal);
|
|
3869
4786
|
waiter.deferredActivityFrames.length = 0;
|
|
3870
4787
|
waiter.terminal = undefined;
|
|
3871
4788
|
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|