@gajae-code/coding-agent 0.17.1 → 0.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +266 -0
- package/dist/types/async/job-manager.d.ts +7 -6
- package/dist/types/cli/daemon-cli.d.ts +3 -1
- package/dist/types/cli/github-release.d.ts +17 -0
- package/dist/types/cli/public-command-entry.d.ts +56 -0
- package/dist/types/cli/public-command-errors.d.ts +126 -0
- package/dist/types/cli/public-command-evidence.d.ts +99 -0
- package/dist/types/cli/public-command-help.d.ts +65 -0
- package/dist/types/cli/public-command-registry.d.ts +69 -0
- package/dist/types/cli/session-picker.d.ts +1 -1
- package/dist/types/cli/setup-cli.d.ts +1 -0
- package/dist/types/cli/skills-cli.d.ts +5 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +4 -50
- package/dist/types/commands/sdk.d.ts +20 -37
- package/dist/types/commands/setup.d.ts +3 -0
- package/dist/types/commands/skills.d.ts +9 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/file-lock.d.ts +10 -1
- package/dist/types/config/model-profile-activation.d.ts +63 -4
- package/dist/types/config/model-registry.d.ts +20 -2
- package/dist/types/config/settings-schema.d.ts +11 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
- package/dist/types/coordinator-mcp/server.d.ts +3 -0
- package/dist/types/daemon/operator-contract.d.ts +5 -1
- package/dist/types/eval/js/worker-protocol.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +12 -0
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +18 -0
- package/dist/types/extensibility/shared-events.d.ts +3 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
- package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/parse.d.ts +9 -1
- package/dist/types/main.d.ts +22 -0
- package/dist/types/modes/acp/acp-agent.d.ts +94 -1
- package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
- package/dist/types/modes/components/session-selector.d.ts +5 -2
- package/dist/types/modes/components/thinking-selector.d.ts +2 -1
- package/dist/types/modes/components/user-message-selector.d.ts +5 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/runtime/process-lifecycle.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +56 -0
- package/dist/types/runtime-mcp/pool.d.ts +17 -0
- package/dist/types/runtime-mcp/redaction.d.ts +20 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
- package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
- package/dist/types/sdk/acp/adapter.d.ts +27 -0
- package/dist/types/sdk/acp/final-text.d.ts +8 -0
- package/dist/types/sdk/broker/broker.d.ts +13 -2
- package/dist/types/sdk/broker/ensure.d.ts +46 -1
- package/dist/types/sdk/broker/identity.d.ts +3 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
- package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
- package/dist/types/sdk/broker/runtime.d.ts +1 -1
- package/dist/types/sdk/broker/session-index.d.ts +22 -3
- package/dist/types/sdk/broker/spawn-authority.d.ts +4 -0
- package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
- package/dist/types/sdk/bus/index.d.ts +19 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
- package/dist/types/sdk/cli/master-cli.d.ts +6 -10
- package/dist/types/sdk/cli/rows.d.ts +19 -1
- package/dist/types/sdk/cli/session-cli.d.ts +22 -5
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/guides/cli.d.ts +3 -4
- package/dist/types/sdk/guides/verify.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +9 -0
- package/dist/types/sdk/host/session-runtime.d.ts +31 -0
- package/dist/types/sdk/host/types.d.ts +10 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +83 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +38 -1
- package/dist/types/sdk/prompt-failure.d.ts +30 -1
- package/dist/types/sdk/prompt-status.d.ts +8 -0
- package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
- package/dist/types/sdk/router/replay-retention-gap-cache.d.ts +13 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -0
- package/dist/types/sdk/transport/relay.d.ts +1 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +73 -2
- package/dist/types/sdk/transport/stdio.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +168 -21
- package/dist/types/session/fallback-chain-controller.d.ts +6 -6
- package/dist/types/session/fold-coordinator.d.ts +17 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
- package/dist/types/session/session-manager.d.ts +64 -1
- package/dist/types/session/session-work-lease.d.ts +14 -0
- package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
- package/dist/types/setup/provider-onboarding.d.ts +81 -1
- package/dist/types/thinking.d.ts +3 -1
- package/dist/types/tools/bash.d.ts +37 -11
- package/dist/types/tools/job.d.ts +1 -1
- package/dist/types/tools/steer-fold.d.ts +31 -0
- package/dist/types/utils/git.d.ts +28 -2
- package/dist/types/utils/mupdf-embedded.d.ts +2 -0
- package/dist/types/utils/mupdf-wasm-embedded.d.ts +2 -0
- package/dist/types/utils/mupdf.d.ts +4 -0
- package/dist/types/utils/session-title-generation.d.ts +6 -0
- package/dist/types/utils/title-generator.d.ts +11 -2
- package/dist/types/web/scrapers/types.d.ts +3 -0
- package/package.json +18 -10
- package/scripts/build-binary.ts +9 -3
- package/scripts/compile-args.ts +0 -9
- package/scripts/embed-mupdf.ts +52 -0
- package/scripts/generate-sdk-operation-inventory.ts +31 -1
- package/scripts/vendor-markit.ts +196 -0
- package/src/async/job-manager.ts +60 -9
- package/src/cli/daemon-cli.ts +53 -18
- package/src/cli/github-release.ts +87 -5
- package/src/cli/mcp-cli.ts +38 -4
- package/src/cli/public-command-entry.ts +450 -0
- package/src/cli/public-command-errors.ts +581 -0
- package/src/cli/public-command-evidence.ts +717 -0
- package/src/cli/public-command-help.ts +360 -0
- package/src/cli/public-command-registry.ts +535 -0
- package/src/cli/session-picker.ts +2 -1
- package/src/cli/setup-cli.ts +9 -1
- package/src/cli/skills-cli.ts +46 -2
- package/src/cli.ts +61 -1
- package/src/commands/daemon.ts +65 -69
- package/src/commands/sdk.ts +232 -100
- package/src/commands/setup.ts +2 -0
- package/src/commands/skills.ts +11 -0
- package/src/config/autorouting-tier-map.ts +16 -0
- package/src/config/file-lock.ts +199 -68
- package/src/config/model-profile-activation.ts +406 -53
- package/src/config/model-registry.ts +204 -25
- package/src/config/settings-schema.ts +26 -2
- package/src/coordinator-mcp/policy.ts +10 -5
- package/src/coordinator-mcp/question-gate-codec.ts +17 -0
- package/src/coordinator-mcp/question-state.ts +175 -8
- package/src/coordinator-mcp/server.ts +528 -87
- package/src/coordinator-mcp/session-reaper.ts +6 -4
- package/src/crash/record-loader.ts +6 -2
- package/src/daemon/operator-contract.ts +26 -12
- package/src/debug/report-bundle.ts +114 -24
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
- package/src/eval/js/context-manager.ts +8 -8
- package/src/eval/js/worker-core.ts +9 -4
- package/src/eval/js/worker-protocol.ts +2 -0
- package/src/extensibility/extensions/runner.ts +52 -4
- package/src/extensibility/extensions/types.ts +10 -0
- package/src/extensibility/gjc-plugins/registry.ts +196 -37
- package/src/extensibility/runtime-skill-discovery.ts +61 -3
- package/src/extensibility/shared-events.ts +3 -1
- package/src/gjc-runtime/managed-owner-admission.ts +67 -50
- package/src/gjc-runtime/session-state-lock.ts +165 -24
- package/src/gjc-runtime/ultragoal-succession.ts +31 -3
- package/src/internal-urls/docs-index.generated.ts +21 -19
- package/src/internal-urls/embedded-protocol.ts +83 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/parse.ts +21 -1
- package/src/internal-urls/router.ts +4 -2
- package/src/main.ts +123 -55
- package/src/modes/acp/acp-agent.ts +593 -47
- package/src/modes/acp/acp-event-mapper.ts +9 -0
- package/src/modes/acp/startup-provenance.ts +59 -0
- package/src/modes/components/custom-provider-wizard.ts +308 -7
- package/src/modes/components/model-selector.ts +31 -15
- package/src/modes/components/oauth-selector.ts +9 -0
- package/src/modes/components/session-selector.ts +47 -4
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/components/thinking-selector.ts +7 -3
- package/src/modes/components/user-message-selector.ts +57 -15
- package/src/modes/controllers/command-controller.ts +58 -32
- package/src/modes/controllers/extension-ui-controller.ts +33 -7
- package/src/modes/controllers/input-controller.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +177 -18
- package/src/modes/interactive-mode.ts +21 -7
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
- package/src/modes/types.ts +2 -2
- package/src/prompts/agent-fragments/browser-aside.md +1 -0
- package/src/prompts/system/title-system.md +1 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/read.md +2 -0
- package/src/runtime/network-prewarm-service.test.ts +36 -1
- package/src/runtime/network-prewarm-service.ts +28 -3
- package/src/runtime/process-lifecycle.ts +22 -11
- package/src/runtime-mcp/config.ts +16 -1
- package/src/runtime-mcp/manager.ts +689 -55
- package/src/runtime-mcp/pool.test.ts +31 -0
- package/src/runtime-mcp/pool.ts +304 -28
- package/src/runtime-mcp/redaction.ts +42 -0
- package/src/runtime-mcp/startup-policy.ts +7 -0
- package/src/runtime-mcp/tool-bridge.ts +71 -0
- package/src/runtime-mcp/transports/http.ts +108 -25
- package/src/sdk/acp/adapter.ts +90 -9
- package/src/sdk/acp/final-text.ts +19 -2
- package/src/sdk/broker/broker.ts +378 -52
- package/src/sdk/broker/ensure.ts +97 -19
- package/src/sdk/broker/identity.ts +9 -1
- package/src/sdk/broker/lifecycle-ledger.ts +95 -31
- package/src/sdk/broker/lifecycle.ts +690 -141
- package/src/sdk/broker/runtime.ts +1 -1
- package/src/sdk/broker/session-index.ts +97 -4
- package/src/sdk/broker/spawn-authority.ts +35 -7
- package/src/sdk/broker/stderr-drainer.ts +59 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/index.ts +721 -84
- package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -3
- package/src/sdk/cli/master-cli.ts +47 -36
- package/src/sdk/cli/rows.ts +30 -7
- package/src/sdk/cli/session-cli.ts +687 -184
- package/src/sdk/client/client.ts +18 -0
- package/src/sdk/guides/catalog.ts +2 -2
- package/src/sdk/guides/cli.ts +20 -62
- package/src/sdk/guides/verify.ts +14 -0
- package/src/sdk/host/host.ts +49 -5
- package/src/sdk/host/session-runtime.test.ts +636 -9
- package/src/sdk/host/session-runtime.ts +499 -118
- package/src/sdk/host/types.ts +12 -1
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
- package/src/sdk/host/websocket-transport.ts +40 -6
- package/src/sdk/index.ts +11 -0
- package/src/sdk/prompt-deadline-flush.ts +334 -0
- package/src/sdk/prompt-deadline-manager.ts +136 -4
- package/src/sdk/prompt-failure.ts +43 -1
- package/src/sdk/prompt-status.ts +8 -0
- package/src/sdk/prompt-tool-boundary.ts +121 -0
- package/src/sdk/protocol/operation-inventory.generated.json +106 -0
- package/src/sdk/router/replay-retention-gap-cache.ts +27 -0
- package/src/sdk/router/session-router.ts +31 -4
- package/src/sdk/session.ts +194 -16
- package/src/sdk/startup-capability.ts +1 -1
- package/src/sdk/transport/relay.ts +71 -6
- package/src/sdk/transport/serve-cli.ts +494 -49
- package/src/sdk/transport/socket.ts +12 -11
- package/src/sdk/transport/stdio.ts +7 -3
- package/src/session/agent-session.ts +2437 -400
- package/src/session/fallback-chain-controller.ts +6 -7
- package/src/session/fold-coordinator.ts +24 -5
- package/src/session/internal/managed-session-scope.ts +58 -18
- package/src/session/internal/managed-session-storage.ts +304 -11
- package/src/session/session-manager.ts +382 -44
- package/src/session/session-work-lease.ts +34 -0
- package/src/session-import/codex.ts +12 -3
- package/src/setup/model-onboarding-guidance.ts +1 -1
- package/src/setup/provider-onboarding.ts +397 -24
- package/src/slash-commands/builtin-registry.ts +258 -14
- package/src/task/index.ts +14 -2
- package/src/thinking.ts +21 -2
- package/src/tools/bash.ts +98 -78
- package/src/tools/fetch.ts +119 -20
- package/src/tools/job.ts +151 -25
- package/src/tools/output-meta.ts +15 -2
- package/src/tools/path-utils.ts +27 -3
- package/src/tools/read.ts +3 -3
- package/src/tools/steer-fold.ts +107 -0
- package/src/tools/tool-catalog.generated.ts +29 -2
- package/src/tools/tool-errors.ts +4 -0
- package/src/utils/git.ts +53 -3
- package/src/utils/markit.ts +55 -41
- package/src/utils/mupdf-assets.d.ts +4 -0
- package/src/utils/mupdf-embedded.ts +4 -0
- package/src/utils/mupdf-wasm-embedded.ts +3 -0
- package/src/utils/mupdf-wasm.ts +24 -2
- package/src/utils/mupdf.ts +129 -0
- package/src/utils/session-title-generation.ts +29 -0
- package/src/utils/title-generator.ts +46 -2
- package/src/web/scrapers/types.ts +24 -3
- package/vendor/markit-ai/LICENSE +21 -0
- package/vendor/markit-ai/MANIFEST.json +412 -0
- package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
- package/vendor/markit-ai/dist/commands/config.js +139 -0
- package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
- package/vendor/markit-ai/dist/commands/convert.js +113 -0
- package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/formats.js +92 -0
- package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/init.js +29 -0
- package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/onboard.js +61 -0
- package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
- package/vendor/markit-ai/dist/commands/plugin.js +58 -0
- package/vendor/markit-ai/dist/config.d.ts +26 -0
- package/vendor/markit-ai/dist/config.js +42 -0
- package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
- package/vendor/markit-ai/dist/converters/audio.js +104 -0
- package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
- package/vendor/markit-ai/dist/converters/csv.js +83 -0
- package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/docx.js +57 -0
- package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/epub.js +128 -0
- package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
- package/vendor/markit-ai/dist/converters/github.js +148 -0
- package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/html.js +30 -0
- package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/image.js +124 -0
- package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
- package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
- package/vendor/markit-ai/dist/converters/iwork.js +391 -0
- package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/json.js +21 -0
- package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
- package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
- package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
- package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
- package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
- package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
- package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
- package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
- package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
- package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
- package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
- package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
- package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
- package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
- package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
- package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
- package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/pptx.js +289 -0
- package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
- package/vendor/markit-ai/dist/converters/rss.js +138 -0
- package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
- package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
- package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/xml.js +18 -0
- package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/yaml.js +17 -0
- package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/zip.js +57 -0
- package/vendor/markit-ai/dist/index.d.ts +30 -0
- package/vendor/markit-ai/dist/index.js +26 -0
- package/vendor/markit-ai/dist/main.d.ts +2 -0
- package/vendor/markit-ai/dist/main.js +191 -0
- package/vendor/markit-ai/dist/markit.d.ts +45 -0
- package/vendor/markit-ai/dist/markit.js +267 -0
- package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
- package/vendor/markit-ai/dist/plugins/api.js +42 -0
- package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
- package/vendor/markit-ai/dist/plugins/index.js +3 -0
- package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
- package/vendor/markit-ai/dist/plugins/installer.js +176 -0
- package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
- package/vendor/markit-ai/dist/plugins/loader.js +61 -0
- package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
- package/vendor/markit-ai/dist/plugins/types.js +1 -0
- package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
- package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
- package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
- package/vendor/markit-ai/dist/providers/index.js +56 -0
- package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
- package/vendor/markit-ai/dist/providers/openai.js +76 -0
- package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
- package/vendor/markit-ai/dist/providers/types.js +1 -0
- package/vendor/markit-ai/dist/types.d.ts +36 -0
- package/vendor/markit-ai/dist/types.js +1 -0
- package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
- package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
- package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
- package/vendor/markit-ai/dist/utils/output.js +31 -0
- package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
- package/vendor/markit-ai/dist/utils/turndown.js +70 -0
- package/vendor/markit-ai/package.json +74 -0
- package/vendor/markit-ai.patch +49 -0
|
@@ -46,15 +46,25 @@ import {
|
|
|
46
46
|
AcpSdkAdapterError,
|
|
47
47
|
acpMcpLaunchFailure,
|
|
48
48
|
} from "../../sdk/acp";
|
|
49
|
-
import { resolveAcpFinalText } from "../../sdk/acp/final-text";
|
|
49
|
+
import { hasAcpFinalTextContent, resolveAcpFinalText } from "../../sdk/acp/final-text";
|
|
50
50
|
import type { SessionLifecycleMcpServer } from "../../sdk/acp/mcp";
|
|
51
51
|
import { ensureBroker } from "../../sdk/broker/ensure";
|
|
52
52
|
import { canonicalSessionCwd } from "../../sdk/broker/session-index";
|
|
53
53
|
import { readSdkBrokerDiscovery, SdkClient, SdkClientError } from "../../sdk/client";
|
|
54
54
|
import type { AbortScope } from "../../sdk/host/control/operations";
|
|
55
55
|
import { SYNTHETIC_PROVIDER_ID } from "../../sdk/model-profile-namespace";
|
|
56
|
-
import {
|
|
57
|
-
|
|
56
|
+
import {
|
|
57
|
+
failedPromptOutcome,
|
|
58
|
+
isSafePromptFailureCode,
|
|
59
|
+
isSdkPromptFailurePhase,
|
|
60
|
+
promptFailureRetryability,
|
|
61
|
+
rephaseFailedOutcome,
|
|
62
|
+
} from "../../sdk/prompt-failure";
|
|
63
|
+
import type {
|
|
64
|
+
SdkPromptFailureCategory,
|
|
65
|
+
SdkPromptFailurePhase,
|
|
66
|
+
SdkPromptTerminalOutcome,
|
|
67
|
+
} from "../../sdk/prompt-status";
|
|
58
68
|
import { PromptActivity, type PromptWatchdogClock, systemPromptWatchdogClock } from "../../sdk/prompt-watchdog";
|
|
59
69
|
import { validateRequiredPromptText } from "../../sdk/protocol/adapter-validation";
|
|
60
70
|
import { type SessionAttachment, SessionRouter, type SessionRouterFrame } from "../../sdk/router";
|
|
@@ -68,6 +78,7 @@ import {
|
|
|
68
78
|
} from "./acp-event-mapper";
|
|
69
79
|
import { resolveAcpPermissionMode } from "./permission-mode";
|
|
70
80
|
import type { AcpStartupOptions } from "./startup-options";
|
|
81
|
+
import { resolveStartupProvenance } from "./startup-provenance";
|
|
71
82
|
import { ACP_TERMINAL_AUTH_FLAG } from "./terminal-auth";
|
|
72
83
|
|
|
73
84
|
const ACP_DEFAULT_MODE_ID = "default";
|
|
@@ -153,6 +164,9 @@ const ACP_FIRST_PROMPT_RETRY_BASE_DELAY_MS = 250;
|
|
|
153
164
|
type JsonObject = Record<string, unknown>;
|
|
154
165
|
interface PromptWaiter {
|
|
155
166
|
acknowledged: boolean;
|
|
167
|
+
invocationKind: "prompt" | "skill";
|
|
168
|
+
/** ACP-owned identity, never inherited from caller metadata or reused for replay. */
|
|
169
|
+
clientRef: string;
|
|
156
170
|
/** True once turn.prompt / skill.invoke has been sent; cancel must not fake-settle after this. */
|
|
157
171
|
dispatched: boolean;
|
|
158
172
|
/** True only while the dispatched control request can still reveal its correlation. */
|
|
@@ -177,6 +191,8 @@ interface PromptWaiter {
|
|
|
177
191
|
lastFrameAt: number;
|
|
178
192
|
/** Type of that prompt-owned frame, reported when the watchdog expires. */
|
|
179
193
|
lastFrameType: string;
|
|
194
|
+
/** True while the prompt is blocked in provider preflight, before the turn is dispatched. */
|
|
195
|
+
awaitingProviderPreflight: boolean;
|
|
180
196
|
/** Cancels the armed inactivity watchdog; re-armed by prompt-owned frames. */
|
|
181
197
|
cancelWatchdog?: () => void;
|
|
182
198
|
/** What the host is observably doing — a tool running, a model call unanswered — and the bound that follows from it. */
|
|
@@ -185,6 +201,12 @@ interface PromptWaiter {
|
|
|
185
201
|
observedTurnActivity: boolean;
|
|
186
202
|
/** True once a prompt-owned tool execution frame was observed; vetoes a first-turn retry that would re-run it. */
|
|
187
203
|
observedToolExecution: boolean;
|
|
204
|
+
/**
|
|
205
|
+
* Newest `plan` update published for this prompt, retained so an abandoned turn can report
|
|
206
|
+
* what it had left to do (issue #5669). Absent means no plan was ever observed, which is
|
|
207
|
+
* evidence of nothing — distinct from an observed plan whose entries are all `completed`.
|
|
208
|
+
*/
|
|
209
|
+
planSnapshot?: AcpPlanSnapshot;
|
|
188
210
|
/** Coordinates a prompt-control rejection racing an acknowledged ACP cancellation. */
|
|
189
211
|
cancelAttempt?: Promise<boolean>;
|
|
190
212
|
/** Whether ANY overlapping cancel attempt for this prompt was acknowledged:
|
|
@@ -239,6 +261,8 @@ type SessionRecord = {
|
|
|
239
261
|
/** Replayable startup notice captured before ACP bootstrap; emitted once after the session id is known. */
|
|
240
262
|
routingInactiveNotice?: string;
|
|
241
263
|
activePrompt?: PromptWaiter;
|
|
264
|
+
/** One bounded read-only recovery owner for this attachment and waiter. */
|
|
265
|
+
statusRecovery?: PromptWaiter;
|
|
242
266
|
/** Set by `session/cancel` so an in-flight prompt settles as `cancelled`, never as an error. */
|
|
243
267
|
cancelRequested?: boolean;
|
|
244
268
|
/** True once the session's first logical prompt has settled; gates first-turn readiness retries. */
|
|
@@ -618,7 +642,7 @@ type SdkPromptFailedOutcome = Extract<SdkPromptTerminalOutcome, { kind: "failed"
|
|
|
618
642
|
* category — so settlement carries that classification instead of discarding it into a bare
|
|
619
643
|
* `AcpSdkAdapterError` (review P1, issue #5574).
|
|
620
644
|
*/
|
|
621
|
-
class AcpPromptFailureError extends AcpSdkAdapterError {
|
|
645
|
+
export class AcpPromptFailureError extends AcpSdkAdapterError {
|
|
622
646
|
readonly failure: SdkPromptFailedOutcome;
|
|
623
647
|
constructor(failure: SdkPromptFailedOutcome) {
|
|
624
648
|
super(failure.code, failure.message);
|
|
@@ -626,6 +650,202 @@ class AcpPromptFailureError extends AcpSdkAdapterError {
|
|
|
626
650
|
}
|
|
627
651
|
}
|
|
628
652
|
|
|
653
|
+
/**
|
|
654
|
+
* Recover the shared prompt-failure projection for a direct SDK rejection.
|
|
655
|
+
*
|
|
656
|
+
* A terminal frame normally reaches {@link AcpPromptFailureError} through
|
|
657
|
+
* `#settlePrompt`, but a host may reject the `turn.prompt` control request
|
|
658
|
+
* itself with `code: "prompt_failed"`. That path never has a terminal outcome
|
|
659
|
+
* to retain, so translating the raw adapter error used to bypass
|
|
660
|
+
* `promptFailureWireData` and publish only the bare `code`/`details` pair.
|
|
661
|
+
* Keep the fallback deliberately narrow: only the two prompt terminal codes
|
|
662
|
+
* are upgraded, and any optional provider classifier is re-validated before
|
|
663
|
+
* it enters the existing classifier/projection seam.
|
|
664
|
+
*/
|
|
665
|
+
function promptFailureFromDirectError(error: unknown): SdkPromptFailedOutcome | undefined {
|
|
666
|
+
if (error instanceof AcpPromptFailureError) return error.failure;
|
|
667
|
+
let code: unknown;
|
|
668
|
+
let providerCode: unknown;
|
|
669
|
+
let phase: unknown;
|
|
670
|
+
try {
|
|
671
|
+
const candidate = error as {
|
|
672
|
+
code?: unknown;
|
|
673
|
+
providerCode?: unknown;
|
|
674
|
+
transportFailure?: { providerCode?: unknown };
|
|
675
|
+
phase?: unknown;
|
|
676
|
+
};
|
|
677
|
+
code = candidate?.code;
|
|
678
|
+
providerCode = candidate?.providerCode ?? candidate?.transportFailure?.providerCode;
|
|
679
|
+
phase = candidate?.phase;
|
|
680
|
+
} catch {
|
|
681
|
+
return undefined;
|
|
682
|
+
}
|
|
683
|
+
if (code !== "prompt_failed" && code !== "prompt_deadline_exceeded") return undefined;
|
|
684
|
+
return failedPromptOutcome({
|
|
685
|
+
code,
|
|
686
|
+
provenance: code === "prompt_deadline_exceeded" ? "deadline" : "agent_failed",
|
|
687
|
+
...(isSafePromptFailureCode(providerCode) ? { providerCode } : {}),
|
|
688
|
+
...(isSdkPromptFailurePhase(phase) ? { phase } : {}),
|
|
689
|
+
evidence: {},
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* The wire projection of a prompt terminal's classification (issue #5615).
|
|
695
|
+
*
|
|
696
|
+
* `super(failure.code, failure.message)` above narrows the error to the generic
|
|
697
|
+
* `prompt_failed` plus its fixed redacted message, so the classification the
|
|
698
|
+
* terminal already computed used to die at the JSON-RPC boundary: an ACP client
|
|
699
|
+
* saw only `-32603 {code, details}` and could not tell a transient provider blip
|
|
700
|
+
* from a dead session without parsing English. These fields carry it across.
|
|
701
|
+
*
|
|
702
|
+
* `providerCode` is re-checked against the safe-token rule rather than trusted:
|
|
703
|
+
* `terminalOutcome` accepts the host's `providerCode` on a `typeof` check alone,
|
|
704
|
+
* and this is the first path that puts it on the wire, so an unbounded value is
|
|
705
|
+
* dropped instead of becoming a leak of provider text (the redaction contract in
|
|
706
|
+
* `sanitizePromptFailure`). A dropped or absent code is omitted, never nulled.
|
|
707
|
+
*/
|
|
708
|
+
export const OPERATOR_UPSTREAM_LABEL = "Upstream provider failure";
|
|
709
|
+
export const OPERATOR_UPSTREAM_SUFFIX = ": the model provider ended this turn.";
|
|
710
|
+
/**
|
|
711
|
+
* `post_start` proves execution began, so tools may already have run and had effects before the
|
|
712
|
+
* provider died. The wording therefore qualifies the turn rather than absolving the task: saying
|
|
713
|
+
* the task was unaffected would assert something the bounded classifiers do not establish.
|
|
714
|
+
*/
|
|
715
|
+
export const OPERATOR_PARTIAL_WORK_NOTE =
|
|
716
|
+
" Work this turn had already performed may have taken effect; check before retrying.";
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Operator-facing wording for a post-start terminal, assembled ONLY from bounded safe tokens: the
|
|
720
|
+
* classifier `phase`, the `category`, and a `providerCode` that passes `isSafePromptFailureCode`.
|
|
721
|
+
* Raw provider text never reaches this function and must never reach it.
|
|
722
|
+
*
|
|
723
|
+
* Gated on BOTH `phase === "post_start"` and `category === "provider_transport"`. A submission-phase
|
|
724
|
+
* provider failure is rejected by the phase gate: nothing executed, so wording about a turn the
|
|
725
|
+
* provider ended would describe an event that never occurred.
|
|
726
|
+
*
|
|
727
|
+
* It claims only what those tokens prove — that the upstream provider ended the turn — and says
|
|
728
|
+
* nothing about whether the operator's work survived. `post_start` proves execution began, so the
|
|
729
|
+
* wording qualifies that earlier work may already have taken effect instead of absolving the task.
|
|
730
|
+
*
|
|
731
|
+
* This is additive. The wire `code`/`details` pair and every `PROMPT_FAILURE_MESSAGE_*` constant
|
|
732
|
+
* stay exactly as they are — pinned ACP core-v1 conformance asserts on them — so this wording
|
|
733
|
+
* travels alongside the redacted message instead of repurposing it.
|
|
734
|
+
*
|
|
735
|
+
* It says NOTHING about the worktree, deliberately. Nothing on this path inspects the worktree, so
|
|
736
|
+
* any sentence about uncommitted work would assert a check that never ran — reporting an
|
|
737
|
+
* uninspected tree as clean is the absence-of-evidence-as-evidence-of-absence error, and an
|
|
738
|
+
* operator who believed it would sweep work away. Silence is the only honest option until something
|
|
739
|
+
* actually looks.
|
|
740
|
+
*
|
|
741
|
+
* Returns `undefined` for every other category: `phase` and `category` are already on the wire, so
|
|
742
|
+
* restating them in prose tells an operator nothing they do not already have.
|
|
743
|
+
*/
|
|
744
|
+
export function postStartOperatorMessage(input: {
|
|
745
|
+
phase: SdkPromptFailurePhase;
|
|
746
|
+
category: SdkPromptFailureCategory;
|
|
747
|
+
providerCode?: string;
|
|
748
|
+
}): string | undefined {
|
|
749
|
+
// Phase first: a submission-phase provider failure never started executing, so wording about a
|
|
750
|
+
// turn that ended mid-flight would describe something that did not happen. `phase` is a bounded
|
|
751
|
+
// classifier already on the wire, so gating on it widens nothing.
|
|
752
|
+
if (input.phase !== "post_start") return undefined;
|
|
753
|
+
if (input.category !== "provider_transport") return undefined;
|
|
754
|
+
const code = isSafePromptFailureCode(input.providerCode) ? ` (${input.providerCode})` : "";
|
|
755
|
+
return `${OPERATOR_UPSTREAM_LABEL}${code}${OPERATOR_UPSTREAM_SUFFIX}${OPERATOR_PARTIAL_WORK_NOTE}`;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function promptFailureWireData(failure: SdkPromptFailedOutcome): Record<string, string> {
|
|
759
|
+
// `operatorMessage` says, in words, what the classifier tokens already say in codes: that the
|
|
760
|
+
// cause was an upstream provider problem rather than a failure of the operator's task. Built
|
|
761
|
+
// only from those same bounded tokens, so it widens nothing that reaches the wire.
|
|
762
|
+
const operatorMessage = postStartOperatorMessage({
|
|
763
|
+
phase: failure.phase,
|
|
764
|
+
category: failure.category,
|
|
765
|
+
...(failure.providerCode === undefined ? {} : { providerCode: failure.providerCode }),
|
|
766
|
+
});
|
|
767
|
+
return {
|
|
768
|
+
phase: failure.phase,
|
|
769
|
+
category: failure.category,
|
|
770
|
+
retryability: promptFailureRetryability(failure.category),
|
|
771
|
+
...(isSafePromptFailureCode(failure.providerCode) ? { providerCode: failure.providerCode } : {}),
|
|
772
|
+
...(operatorMessage === undefined ? {} : { operatorMessage }),
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* What an ACP `plan` update said the turn intended to do. Structural rather than the SDK's
|
|
778
|
+
* `PlanEntry` so only the fields this evidence reads are depended on.
|
|
779
|
+
*/
|
|
780
|
+
type AcpPlanSnapshot = Array<{
|
|
781
|
+
content: string;
|
|
782
|
+
status: "pending" | "in_progress" | "completed";
|
|
783
|
+
_meta?: { gjcTodoStatus?: unknown } | null;
|
|
784
|
+
}>;
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* An abandoned prompt that still carries the plan its turn was working through (issue #5669).
|
|
788
|
+
*
|
|
789
|
+
* The watchdog settles a prompt whose host stopped producing frames, and that rejection used to
|
|
790
|
+
* be a bare `AcpSdkAdapterError`: `{code, details}` and nothing else. A wrapper could not tell
|
|
791
|
+
* "the agent finished" from "the agent stopped with work left", so an abandoned turn's unverified
|
|
792
|
+
* diff was published as if it were complete. The last observed plan is the evidence that settles
|
|
793
|
+
* that question, so settlement carries it instead of dropping it at the JSON-RPC boundary.
|
|
794
|
+
*/
|
|
795
|
+
export class AcpPromptAbandonedError extends AcpSdkAdapterError {
|
|
796
|
+
readonly plan: AcpPlanSnapshot | undefined;
|
|
797
|
+
constructor(code: string, message: string, plan: AcpPlanSnapshot | undefined) {
|
|
798
|
+
super(code, message);
|
|
799
|
+
this.plan = plan;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** Most pending plan entries that may reach the wire. */
|
|
804
|
+
const ACP_ABANDON_PLAN_MAX_ENTRIES = 10;
|
|
805
|
+
/** Longest one pending entry's text may be on the wire. */
|
|
806
|
+
const ACP_ABANDON_PLAN_MAX_CONTENT_CHARS = 200;
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Whether a plan entry describes work the turn never finished (issue #5669).
|
|
810
|
+
*
|
|
811
|
+
* An entry whose ACP status is `completed` can still be unfinished: ACP's `PlanEntryStatus` has no
|
|
812
|
+
* `abandoned` member, so a task dropped via `/todo drop` is projected as `completed` for the
|
|
813
|
+
* client's plan UI while the mapper keeps the internal status in `_meta.gjcTodoStatus`. Completion
|
|
814
|
+
* evidence has to read that internal truth — reading the wire status alone would report a plan of
|
|
815
|
+
* nothing but dropped tasks as a finished turn. `_meta` crosses a JSON boundary, so the internal
|
|
816
|
+
* status is compared as `unknown` rather than asserted into a type.
|
|
817
|
+
*/
|
|
818
|
+
function planEntryUnfinished(entry: AcpPlanSnapshot[number]): boolean {
|
|
819
|
+
return entry.status !== "completed" || entry._meta?.gjcTodoStatus === "abandoned";
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* The wire projection of an abandoned prompt's plan (issue #5669).
|
|
824
|
+
*
|
|
825
|
+
* Plan text is model-authored, so it is bounded the same way `promptFailureWireData` bounds a
|
|
826
|
+
* provider classifier: a capped number of entries, each capped in length, and never restated in
|
|
827
|
+
* the human message. The counts are what a wrapper actually gates on; the contents are there so
|
|
828
|
+
* the reader can name the unfinished steps without a log dive.
|
|
829
|
+
*
|
|
830
|
+
* An absent plan yields no keys at all. "No plan was ever observed" is not the same claim as
|
|
831
|
+
* "the plan was complete", and a client must be able to tell them apart, so the fields are
|
|
832
|
+
* omitted rather than nulled or reported as unknown.
|
|
833
|
+
*/
|
|
834
|
+
function promptAbandonWireData(plan: AcpPlanSnapshot | undefined): Record<string, string> {
|
|
835
|
+
if (!plan) return {};
|
|
836
|
+
const pending = plan.filter(planEntryUnfinished);
|
|
837
|
+
return {
|
|
838
|
+
planIncomplete: pending.length > 0 ? "true" : "false",
|
|
839
|
+
planPendingCount: String(pending.length),
|
|
840
|
+
planTotalCount: String(plan.length),
|
|
841
|
+
planPending: JSON.stringify(
|
|
842
|
+
pending
|
|
843
|
+
.slice(0, ACP_ABANDON_PLAN_MAX_ENTRIES)
|
|
844
|
+
.map(entry => entry.content.slice(0, ACP_ABANDON_PLAN_MAX_CONTENT_CHARS)),
|
|
845
|
+
),
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
629
849
|
/**
|
|
630
850
|
* The startup-readiness failure class (issue #5574): the turn failed AFTER it started, with a
|
|
631
851
|
* bounded provider/transport classifier — the signature of a host that accepted the turn before
|
|
@@ -1251,9 +1471,22 @@ export function acpRequestFailure(error: unknown): unknown {
|
|
|
1251
1471
|
const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
|
|
1252
1472
|
if (typeof code !== "string") return error;
|
|
1253
1473
|
const message = error instanceof Error ? error.message : code;
|
|
1474
|
+
// A prompt terminal additionally publishes its classification (issue #5615). The
|
|
1475
|
+
// spread is empty for every other error, so their payloads are byte-identical to
|
|
1476
|
+
// before. `code`/`details` and the JSON-RPC code itself are untouched either way:
|
|
1477
|
+
// this enriches `data`, it does not restate the redacted message.
|
|
1478
|
+
const promptFailure = promptFailureFromDirectError(error);
|
|
1479
|
+
const data =
|
|
1480
|
+
promptFailure !== undefined
|
|
1481
|
+
? { code, details: message, ...promptFailureWireData(promptFailure) }
|
|
1482
|
+
: { code, details: message };
|
|
1483
|
+
// An abandoned prompt additionally publishes the plan it never finished (issue #5669).
|
|
1484
|
+
// Same rule as above: `code`/`details` and the JSON-RPC code are untouched, and an abandon
|
|
1485
|
+
// that observed no plan adds no keys, so its payload stays byte-identical to before.
|
|
1486
|
+
if (error instanceof AcpPromptAbandonedError) Object.assign(data, promptAbandonWireData(error.plan));
|
|
1254
1487
|
switch (code) {
|
|
1255
1488
|
case "authentication_failed":
|
|
1256
|
-
return RequestError.authRequired(
|
|
1489
|
+
return RequestError.authRequired(data, message);
|
|
1257
1490
|
// `not_found` stays -32603 with its discriminator in `data`: ACP's
|
|
1258
1491
|
// `resourceNotFound` (-32002) is a URI-addressed resource error, and an unknown
|
|
1259
1492
|
// session id is not a resource URI. Pinned ACP core-v1 conformance also requires
|
|
@@ -1261,12 +1494,15 @@ export function acpRequestFailure(error: unknown): unknown {
|
|
|
1261
1494
|
case "invalid_input":
|
|
1262
1495
|
case "unsupported":
|
|
1263
1496
|
case "unsupported_content":
|
|
1264
|
-
return RequestError.invalidParams(
|
|
1497
|
+
return RequestError.invalidParams(data, message);
|
|
1265
1498
|
default:
|
|
1266
1499
|
// The remaining internal codes (conflict, unavailable, busy, …) have no ACP
|
|
1267
1500
|
// counterpart and stay -32603. Keep the discriminator in `data` so a client can
|
|
1268
|
-
// branch on retry/reconnect instead of parsing an English message.
|
|
1269
|
-
|
|
1501
|
+
// branch on retry/reconnect instead of parsing an English message. `prompt_failed`
|
|
1502
|
+
// and `prompt_deadline_exceeded` land here and keep -32603 by design: pinned ACP
|
|
1503
|
+
// core-v1 conformance expects -32603/-32000 for this class, so the added
|
|
1504
|
+
// classification travels in `data` rather than in a renumbered code.
|
|
1505
|
+
return RequestError.internalError(data, message);
|
|
1270
1506
|
}
|
|
1271
1507
|
}
|
|
1272
1508
|
|
|
@@ -1384,7 +1620,7 @@ export class AcpAgent implements Agent {
|
|
|
1384
1620
|
/** Retain settled prompt identities across automatic transport reattachment. */
|
|
1385
1621
|
readonly #retiredPromptCorrelations = new Map<string, PromptCorrelation[]>();
|
|
1386
1622
|
/** Prevent a successor admission while a retired prompt can still reveal its identity. */
|
|
1387
|
-
readonly #retiredPromptAcknowledgements = new
|
|
1623
|
+
readonly #retiredPromptAcknowledgements = new Map<string, PromptWaiter>();
|
|
1388
1624
|
/** Ready terminal metadata writes retain ownership across same-id record replacement. */
|
|
1389
1625
|
readonly #terminalMetadataTails = new Map<string, Promise<void>>();
|
|
1390
1626
|
/** Unstreamed terminal text retains transcript ownership across same-id replacement. */
|
|
@@ -1986,6 +2222,7 @@ export class AcpAgent implements Agent {
|
|
|
1986
2222
|
|
|
1987
2223
|
const payload = acpPromptPayload(params.prompt);
|
|
1988
2224
|
const skillInvocation = acpSkillInvocation(params.prompt);
|
|
2225
|
+
const clientRef = randomUUID();
|
|
1989
2226
|
if (!skillInvocation) {
|
|
1990
2227
|
const promptError = validateRequiredPromptText("turn.prompt", {
|
|
1991
2228
|
text: payload.text,
|
|
@@ -2020,7 +2257,7 @@ export class AcpAgent implements Agent {
|
|
|
2020
2257
|
JSON.stringify({
|
|
2021
2258
|
type: "control_request",
|
|
2022
2259
|
operation: skillInvocation ? "skill.invoke" : "turn.prompt",
|
|
2023
|
-
input: skillInvocation ?? { text: payload.text, images: payload.images },
|
|
2260
|
+
input: { ...(skillInvocation ?? { text: payload.text, images: payload.images }), clientRef },
|
|
2024
2261
|
// AcpSdkAdapter.control passes `confirm: false` to SdkClient, which serializes
|
|
2025
2262
|
// the field even though it is not part of the skill input payload.
|
|
2026
2263
|
...(skillInvocation ? { confirm: false } : {}),
|
|
@@ -2042,6 +2279,8 @@ export class AcpAgent implements Agent {
|
|
|
2042
2279
|
record.promptObservedAssistantOutput = false;
|
|
2043
2280
|
const { promise: response, resolve, reject } = Promise.withResolvers<PromptResponse>();
|
|
2044
2281
|
const waiter: PromptWaiter = {
|
|
2282
|
+
invocationKind: skillInvocation ? "skill" : "prompt",
|
|
2283
|
+
clientRef,
|
|
2045
2284
|
acknowledged: false,
|
|
2046
2285
|
dispatched: false,
|
|
2047
2286
|
acknowledgementPending: false,
|
|
@@ -2055,6 +2294,7 @@ export class AcpAgent implements Agent {
|
|
|
2055
2294
|
deferredActivityFrames: [],
|
|
2056
2295
|
lastFrameAt: this.#promptWatchdogClock.now(),
|
|
2057
2296
|
lastFrameType: "prompt_dispatch",
|
|
2297
|
+
awaitingProviderPreflight: true,
|
|
2058
2298
|
activity: new PromptActivity(),
|
|
2059
2299
|
observedTurnActivity: false,
|
|
2060
2300
|
observedToolExecution: false,
|
|
@@ -2066,38 +2306,60 @@ export class AcpAgent implements Agent {
|
|
|
2066
2306
|
// pending. Retain the original promise for the caller, but mark that delayed rejection
|
|
2067
2307
|
// as observed until prompt() can resume and await it.
|
|
2068
2308
|
void response.catch(() => undefined);
|
|
2309
|
+
// Silence has to be bounded from the moment the prompt owns the session: a host that
|
|
2310
|
+
// dies before it ever answers is exactly the failure that leaves the client running.
|
|
2311
|
+
// Provider preflight is part of that window — it awaits a lease the host may never win
|
|
2312
|
+
// — so the bound is armed before it, not after (issue #5658). The arm below re-arms
|
|
2313
|
+
// this same waiter once preflight clears, and #armPromptWatchdog cancels the previous
|
|
2314
|
+
// timer first, so only one timer is ever live.
|
|
2315
|
+
this.#armPromptWatchdog(params.sessionId, record, waiter);
|
|
2316
|
+
// The watchdog settles `response`, not this await, so a preflight that never returns
|
|
2317
|
+
// would pin session/prompt open with the caller's promise already rejected. Racing the
|
|
2318
|
+
// settlement lets the re-checks below hand that rejection back to the caller.
|
|
2319
|
+
const settlement = response.then(
|
|
2320
|
+
() => undefined,
|
|
2321
|
+
() => undefined,
|
|
2322
|
+
);
|
|
2069
2323
|
try {
|
|
2070
|
-
await record.adapter.ensureProviders();
|
|
2324
|
+
await Promise.race([record.adapter.ensureProviders(), settlement]);
|
|
2071
2325
|
} catch (error) {
|
|
2326
|
+
waiter.awaitingProviderPreflight = false;
|
|
2072
2327
|
if (waiter.settled || record.activePrompt !== waiter) {
|
|
2073
|
-
this.#
|
|
2328
|
+
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2074
2329
|
return await response;
|
|
2075
2330
|
}
|
|
2076
2331
|
if (record.cancelRequested && waiter.cancelAcknowledged) {
|
|
2077
|
-
this.#
|
|
2332
|
+
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2078
2333
|
await this.#settleCancelledPrompt(params.sessionId, record, waiter);
|
|
2079
2334
|
return await response;
|
|
2080
2335
|
}
|
|
2081
2336
|
if (record.activePrompt === waiter) {
|
|
2082
2337
|
record.activePrompt = undefined;
|
|
2083
2338
|
record.busy = record.backgroundBusy;
|
|
2084
|
-
|
|
2339
|
+
clearPromptWatchdog(waiter);
|
|
2340
|
+
// #5526's guarded release replaces the unguarded delete: it only clears
|
|
2341
|
+
// the entry when it still belongs to THIS waiter, so a successor's
|
|
2342
|
+
// acknowledgement is never dropped. Strictly narrower than the delete
|
|
2343
|
+
// it supersedes; dev's watchdog clear above is unaffected.
|
|
2344
|
+
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2085
2345
|
void this.#publishPromptPhaseIdle(params.sessionId, record.adapter);
|
|
2086
2346
|
}
|
|
2087
2347
|
throw error;
|
|
2088
2348
|
}
|
|
2349
|
+
waiter.awaitingProviderPreflight = false;
|
|
2089
2350
|
if (waiter.settled || record.activePrompt !== waiter) {
|
|
2090
|
-
this.#
|
|
2351
|
+
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2091
2352
|
return await response;
|
|
2092
2353
|
}
|
|
2093
2354
|
if (record.cancelRequested && waiter.cancelAcknowledged) {
|
|
2094
|
-
this.#
|
|
2355
|
+
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2095
2356
|
await this.#settleCancelledPrompt(params.sessionId, record, waiter);
|
|
2096
2357
|
return await response;
|
|
2097
2358
|
}
|
|
2098
2359
|
|
|
2099
|
-
//
|
|
2100
|
-
//
|
|
2360
|
+
// Re-arm now that preflight is behind us: the bound armed before it already covered
|
|
2361
|
+
// this prompt's ownership window, and this call replaces that timer rather than
|
|
2362
|
+
// racing it, so the dispatched turn gets the full inactivity bound of its own.
|
|
2101
2363
|
this.#armPromptWatchdog(params.sessionId, record, waiter);
|
|
2102
2364
|
// Echo the user's own message back as `user_message_chunk`. Clients render their
|
|
2103
2365
|
// transcript from session/update, so without this a prompt's text and any attached
|
|
@@ -2133,14 +2395,18 @@ export class AcpAgent implements Agent {
|
|
|
2133
2395
|
}
|
|
2134
2396
|
|
|
2135
2397
|
waiter.acknowledgementPending = true;
|
|
2398
|
+
const promptAdapter = record.adapter;
|
|
2136
2399
|
const acknowledgementTask = (async (): Promise<PromptResponse> => {
|
|
2137
2400
|
if (waiter.settled || record.activePrompt !== waiter) return await response;
|
|
2138
2401
|
const acknowledgement = skillInvocation
|
|
2139
|
-
? await
|
|
2140
|
-
: await
|
|
2402
|
+
? await promptAdapter.control("skill.invoke", { ...skillInvocation, clientRef })
|
|
2403
|
+
: await promptAdapter.prompt({
|
|
2141
2404
|
text: payload.text,
|
|
2405
|
+
clientRef,
|
|
2142
2406
|
...(payload.images.length ? { images: payload.images } : {}),
|
|
2143
2407
|
});
|
|
2408
|
+
// A recovered waiter already owns its exact identity; a late ack cannot rebind it.
|
|
2409
|
+
if (waiter.settled && waiter.acknowledged) return await response;
|
|
2144
2410
|
|
|
2145
2411
|
const acknowledgementCorrelation = promptAcknowledgement(acknowledgement);
|
|
2146
2412
|
if (!acknowledgementCorrelation)
|
|
@@ -2148,6 +2414,14 @@ export class AcpAgent implements Agent {
|
|
|
2148
2414
|
"invalid_prompt_acknowledgement",
|
|
2149
2415
|
"SDK prompt acknowledgement must accept the prompt and include commandId and turnId.",
|
|
2150
2416
|
);
|
|
2417
|
+
if (
|
|
2418
|
+
this.#sessions.get(params.sessionId) !== record ||
|
|
2419
|
+
record.adapter !== promptAdapter ||
|
|
2420
|
+
record.activePrompt !== waiter
|
|
2421
|
+
) {
|
|
2422
|
+
this.#rememberSettledPromptCorrelation(params.sessionId, record, acknowledgementCorrelation);
|
|
2423
|
+
return await response;
|
|
2424
|
+
}
|
|
2151
2425
|
if (this.#hasRetiredPromptCorrelation(params.sessionId, record, acknowledgementCorrelation)) {
|
|
2152
2426
|
try {
|
|
2153
2427
|
const abortAcknowledgement = await record.adapter.cancel("turn");
|
|
@@ -2282,10 +2556,20 @@ export class AcpAgent implements Agent {
|
|
|
2282
2556
|
}
|
|
2283
2557
|
this.#settlePrompt(params.sessionId, record, waiter);
|
|
2284
2558
|
return await response;
|
|
2285
|
-
})()
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2559
|
+
})()
|
|
2560
|
+
.catch(async error => {
|
|
2561
|
+
if (
|
|
2562
|
+
!(error instanceof SdkClientError || error instanceof AcpSdkAdapterError) ||
|
|
2563
|
+
error.code !== "uncertain_after_send"
|
|
2564
|
+
)
|
|
2565
|
+
throw error;
|
|
2566
|
+
if (record.adapter === promptAdapter) this.#startUncertainPromptRecovery(params.sessionId, record, waiter);
|
|
2567
|
+
return await response;
|
|
2568
|
+
})
|
|
2569
|
+
.finally(() => {
|
|
2570
|
+
waiter.acknowledgementPending = false;
|
|
2571
|
+
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2572
|
+
});
|
|
2289
2573
|
// Settlement can win before the SDK answers `turn.prompt`; acknowledgement processing
|
|
2290
2574
|
// must remain alive to tombstone its eventual correlation without holding the ACP caller.
|
|
2291
2575
|
void acknowledgementTask.catch(() => undefined);
|
|
@@ -2744,24 +3028,23 @@ export class AcpAgent implements Agent {
|
|
|
2744
3028
|
// a broker-launched SDK host keeps ACP ownership across reconnects.
|
|
2745
3029
|
await adapter.start({ activateProviders: false });
|
|
2746
3030
|
let capabilities: JsonObject | undefined;
|
|
3031
|
+
// A query that never answered and a host that answered without provenance are
|
|
3032
|
+
// different operator problems, so the failure is retained instead of discarded.
|
|
3033
|
+
let capabilitiesFailure: unknown;
|
|
2747
3034
|
try {
|
|
2748
3035
|
const response = object(await adapter.query("runtime.capabilities"));
|
|
2749
3036
|
const result = object(response?.result) ?? response;
|
|
2750
3037
|
// Q18 is a paged query surface: the capability object arrives as the single
|
|
2751
3038
|
// page item, so fall back to the envelope only for direct-result hosts.
|
|
2752
3039
|
capabilities = object(pageItems(result)[0]) ?? result;
|
|
2753
|
-
} catch {
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
throw new AcpSdkAdapterError(
|
|
2762
|
-
"unavailable",
|
|
2763
|
-
"This ACP client requires a newer GJC SDK session with startup control provenance; restart the session.",
|
|
2764
|
-
);
|
|
3040
|
+
} catch (error) {
|
|
3041
|
+
capabilitiesFailure = error;
|
|
3042
|
+
}
|
|
3043
|
+
const primaryControlSurface = resolveStartupProvenance({
|
|
3044
|
+
sessionId: id,
|
|
3045
|
+
capabilities,
|
|
3046
|
+
queryFailure: capabilitiesFailure,
|
|
3047
|
+
});
|
|
2765
3048
|
if (primaryControlSurface === "sdk") {
|
|
2766
3049
|
adapter.authorizeProviderActivation();
|
|
2767
3050
|
await adapter.ensureProviders();
|
|
@@ -2872,7 +3155,11 @@ export class AcpAgent implements Agent {
|
|
|
2872
3155
|
|
|
2873
3156
|
#fenceRetiredPromptAcknowledgement(id: string, waiter: PromptWaiter): void {
|
|
2874
3157
|
if (waiter.dispatched && waiter.acknowledgementPending && !waiter.acknowledged)
|
|
2875
|
-
this.#retiredPromptAcknowledgements.
|
|
3158
|
+
this.#retiredPromptAcknowledgements.set(id, waiter);
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
#releaseRetiredPromptAcknowledgement(id: string, waiter: PromptWaiter): void {
|
|
3162
|
+
if (this.#retiredPromptAcknowledgements.get(id) === waiter) this.#retiredPromptAcknowledgements.delete(id);
|
|
2876
3163
|
}
|
|
2877
3164
|
|
|
2878
3165
|
#advanceTerminalGeneration(record: SessionRecord): void {
|
|
@@ -2882,6 +3169,15 @@ export class AcpAgent implements Agent {
|
|
|
2882
3169
|
#recoverSessionAfterTransportFailure(id: string, adapter: AcpSdkAdapter, error: Error): void {
|
|
2883
3170
|
const record = this.#sessions.get(id);
|
|
2884
3171
|
if (!record || record.adapter !== adapter) return;
|
|
3172
|
+
if (error instanceof SdkClientError && error.code === "uncertain_after_send") {
|
|
3173
|
+
// Provider-registration requests share this transport error. Recover only when the
|
|
3174
|
+
// prompt's own dispatched acknowledgement is still pending; an acknowledged turn
|
|
3175
|
+
// must continue until its terminal frame arrives.
|
|
3176
|
+
const waiter = record.activePrompt;
|
|
3177
|
+
if (waiter?.dispatched && waiter.acknowledgementPending && !waiter.acknowledged)
|
|
3178
|
+
this.#startUncertainPromptRecovery(id, record, waiter);
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
2885
3181
|
if (
|
|
2886
3182
|
error instanceof SdkClientError &&
|
|
2887
3183
|
error.code !== "reconnect_exhausted" &&
|
|
@@ -2896,11 +3192,191 @@ export class AcpAgent implements Agent {
|
|
|
2896
3192
|
logger.warn(`ACP session ${id} non-transport error reached reconnect handler; ignoring terminal recovery.`);
|
|
2897
3193
|
return;
|
|
2898
3194
|
}
|
|
3195
|
+
const waiter = record.activePrompt;
|
|
3196
|
+
if (waiter?.dispatched) {
|
|
3197
|
+
// A prior uncertainty notification already owns recovery for this waiter. Keep
|
|
3198
|
+
// the in-flight lookup alive; only declined, non-recovering waiters fall through
|
|
3199
|
+
// to the ordinary transport-loss teardown and reattachment path.
|
|
3200
|
+
if (record.statusRecovery === waiter || this.#startUncertainPromptRecovery(id, record, waiter)) return;
|
|
3201
|
+
}
|
|
2899
3202
|
const detail = error.message || "SDK transport reconnect failed.";
|
|
2900
3203
|
const terminal = new AcpSdkAdapterError("connection_closed", `ACP session transport was lost: ${detail}`);
|
|
2901
3204
|
void this.#recoverSessionAfterTransportFailureAsync(id, adapter, record.cwd, terminal);
|
|
2902
3205
|
}
|
|
2903
3206
|
|
|
3207
|
+
#startUncertainPromptRecovery(id: string, record: SessionRecord, waiter: PromptWaiter): boolean {
|
|
3208
|
+
if (
|
|
3209
|
+
this.#sessions.get(id) !== record ||
|
|
3210
|
+
promptWaiterRetired(record, waiter) ||
|
|
3211
|
+
!waiter.dispatched ||
|
|
3212
|
+
waiter.terminalReserved ||
|
|
3213
|
+
record.statusRecovery === waiter
|
|
3214
|
+
)
|
|
3215
|
+
return false;
|
|
3216
|
+
record.statusRecovery = waiter;
|
|
3217
|
+
clearPromptWatchdog(waiter);
|
|
3218
|
+
void this.#recoverUncertainPrompt(id, record, record.adapter, waiter);
|
|
3219
|
+
return true;
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
async #recoverUncertainPrompt(
|
|
3223
|
+
id: string,
|
|
3224
|
+
record: SessionRecord,
|
|
3225
|
+
adapter: AcpSdkAdapter,
|
|
3226
|
+
waiter: PromptWaiter,
|
|
3227
|
+
): Promise<void> {
|
|
3228
|
+
const ownsRecovery = (): boolean =>
|
|
3229
|
+
this.#sessions.get(id) === record &&
|
|
3230
|
+
record.adapter === adapter &&
|
|
3231
|
+
record.statusRecovery === waiter &&
|
|
3232
|
+
!promptWaiterRetired(record, waiter) &&
|
|
3233
|
+
!waiter.terminalReserved;
|
|
3234
|
+
// Keep the lookup selector's authority even if an ack arrives during the read.
|
|
3235
|
+
const acknowledgedAtLookup = waiter.acknowledged;
|
|
3236
|
+
let detail = "status query unavailable";
|
|
3237
|
+
let timer: NodeJS.Timeout | undefined;
|
|
3238
|
+
let timedOut = false;
|
|
3239
|
+
try {
|
|
3240
|
+
const deadline = Promise.withResolvers<never>();
|
|
3241
|
+
timer = setTimeout(() => {
|
|
3242
|
+
timedOut = true;
|
|
3243
|
+
deadline.reject(new Error("status query timeout"));
|
|
3244
|
+
}, 5_000);
|
|
3245
|
+
const refreshAttachment = async (): Promise<void> => {
|
|
3246
|
+
if (this.#sessions.get(id) !== record || record.adapter !== adapter) return;
|
|
3247
|
+
// A reconnect can revoke the adapter's opaque attachment before Router has
|
|
3248
|
+
// published its replacement. Force an authority pass first, then hand the
|
|
3249
|
+
// exact current capability back to the adapter before issuing the read-only
|
|
3250
|
+
// lookup. This pass does not replay the prompt mutation.
|
|
3251
|
+
await this.#router.reconcile({ waitForReplay: false });
|
|
3252
|
+
if (!ownsRecovery()) return;
|
|
3253
|
+
const refreshed = this.#router.attachment(id);
|
|
3254
|
+
if (!refreshed?.isCurrent())
|
|
3255
|
+
throw new AcpSdkAdapterError(
|
|
3256
|
+
"unavailable",
|
|
3257
|
+
"ACP session has no current Router attachment for recovery.",
|
|
3258
|
+
);
|
|
3259
|
+
if (refreshed !== record.attachment || !record.attachment.isCurrent()) {
|
|
3260
|
+
await adapter.attachmentReady(refreshed);
|
|
3261
|
+
record.attachment = refreshed;
|
|
3262
|
+
}
|
|
3263
|
+
};
|
|
3264
|
+
await Promise.race([refreshAttachment(), deadline.promise]);
|
|
3265
|
+
if (!ownsRecovery()) return;
|
|
3266
|
+
// Bound observation only: the mutation and the read are never replayed or aborted.
|
|
3267
|
+
const response = object(
|
|
3268
|
+
await Promise.race([
|
|
3269
|
+
adapter.query("turn.result", {
|
|
3270
|
+
kind: waiter.invocationKind,
|
|
3271
|
+
...(acknowledgedAtLookup ? waiter.correlation : { clientRef: waiter.clientRef }),
|
|
3272
|
+
}),
|
|
3273
|
+
deadline.promise,
|
|
3274
|
+
]),
|
|
3275
|
+
);
|
|
3276
|
+
if (!ownsRecovery()) return;
|
|
3277
|
+
const status = object(response?.result) ?? response;
|
|
3278
|
+
const correlation = strictCorrelationFrom(response, status);
|
|
3279
|
+
const outcome = status ? terminalOutcome(status) : undefined;
|
|
3280
|
+
const content = object(status?.content);
|
|
3281
|
+
const readableText =
|
|
3282
|
+
content?.version === 1 &&
|
|
3283
|
+
content.type === "text" &&
|
|
3284
|
+
typeof content.text === "string" &&
|
|
3285
|
+
content.text.trim().length > 0;
|
|
3286
|
+
const failure = object(status?.error);
|
|
3287
|
+
const conflictingIdentity = [response, status].some(
|
|
3288
|
+
value =>
|
|
3289
|
+
(value?.kind !== undefined && value.kind !== waiter.invocationKind) ||
|
|
3290
|
+
(value?.sessionId !== undefined && value.sessionId !== id) ||
|
|
3291
|
+
(value?.clientRef !== undefined && value.clientRef !== waiter.clientRef),
|
|
3292
|
+
);
|
|
3293
|
+
if (
|
|
3294
|
+
status?.kind !== waiter.invocationKind ||
|
|
3295
|
+
conflictingIdentity ||
|
|
3296
|
+
(!acknowledgedAtLookup && status.clientRef !== waiter.clientRef) ||
|
|
3297
|
+
!correlation ||
|
|
3298
|
+
!hasCompleteCorrelation(correlation) ||
|
|
3299
|
+
(waiter.acknowledged && !correlationsExactlyMatch(waiter.correlation, correlation)) ||
|
|
3300
|
+
this.#hasRetiredPromptCorrelation(id, record, correlation)
|
|
3301
|
+
) {
|
|
3302
|
+
detail = "status correlation is missing, retired, or mismatched";
|
|
3303
|
+
} else if (
|
|
3304
|
+
status.status === "failed" &&
|
|
3305
|
+
status.outcome === undefined &&
|
|
3306
|
+
(status.receiptState === "present" ||
|
|
3307
|
+
status.receiptState === "missing" ||
|
|
3308
|
+
status.receiptState === "unknown") &&
|
|
3309
|
+
typeof failure?.code === "string" &&
|
|
3310
|
+
/^[a-zA-Z0-9_.-]{1,64}$/.test(failure.code) &&
|
|
3311
|
+
typeof failure.message === "string" &&
|
|
3312
|
+
failure.message.trim().length > 0 &&
|
|
3313
|
+
failure.message.length <= 512
|
|
3314
|
+
) {
|
|
3315
|
+
waiter.correlation = correlation;
|
|
3316
|
+
waiter.acknowledged = true;
|
|
3317
|
+
record.busy = record.backgroundBusy;
|
|
3318
|
+
this.#advanceTerminalGeneration(record);
|
|
3319
|
+
void this.#rejectPrompt(record, id, waiter, new AcpSdkAdapterError(failure.code, failure.message));
|
|
3320
|
+
return;
|
|
3321
|
+
} else if (
|
|
3322
|
+
(status.status === "terminal_ok" &&
|
|
3323
|
+
outcome?.kind === "stopped" &&
|
|
3324
|
+
(status.receiptState === "present" || status.receiptState === "missing") &&
|
|
3325
|
+
(outcome.reason !== "end_turn" || (status.receiptState === "present" && readableText))) ||
|
|
3326
|
+
(status.status === "failed" &&
|
|
3327
|
+
outcome?.kind === "failed" &&
|
|
3328
|
+
(status.receiptState === "present" ||
|
|
3329
|
+
status.receiptState === "missing" ||
|
|
3330
|
+
status.receiptState === "unknown"))
|
|
3331
|
+
) {
|
|
3332
|
+
waiter.correlation = correlation;
|
|
3333
|
+
waiter.acknowledged = true;
|
|
3334
|
+
record.busy = record.backgroundBusy;
|
|
3335
|
+
this.#advanceTerminalGeneration(record);
|
|
3336
|
+
const settledOutcome =
|
|
3337
|
+
record.cancelRequested && outcome.kind === "stopped" && outcome.reason === "end_turn"
|
|
3338
|
+
? { ...outcome, reason: "cancelled" as const, provenance: "client_cancel" as const }
|
|
3339
|
+
: outcome;
|
|
3340
|
+
const publicationBarrier = record.frameTail;
|
|
3341
|
+
waiter.terminal = { outcome: settledOutcome, correlation };
|
|
3342
|
+
// Recovered final text is published on the async terminal tail below. Record it
|
|
3343
|
+
// before settling so a first-turn startup-readiness failure cannot be retried
|
|
3344
|
+
// after this retained answer has already been committed for publication.
|
|
3345
|
+
if (readableText) record.promptObservedAssistantOutput = true;
|
|
3346
|
+
this.#settlePrompt(id, record, waiter);
|
|
3347
|
+
this.#scheduleTerminalUpdates(
|
|
3348
|
+
id,
|
|
3349
|
+
adapter,
|
|
3350
|
+
record.publicationGeneration,
|
|
3351
|
+
{
|
|
3352
|
+
type: settledOutcome.kind === "failed" ? "agent_failed" : "agent_end",
|
|
3353
|
+
outcome: settledOutcome,
|
|
3354
|
+
finalText: readableText ? content.text : undefined,
|
|
3355
|
+
finalTextTruncated: content?.truncated === true,
|
|
3356
|
+
},
|
|
3357
|
+
waiter,
|
|
3358
|
+
publicationBarrier,
|
|
3359
|
+
);
|
|
3360
|
+
return;
|
|
3361
|
+
} else {
|
|
3362
|
+
detail = "status has no usable retained terminal outcome and receipt";
|
|
3363
|
+
}
|
|
3364
|
+
} catch {
|
|
3365
|
+
detail = timedOut ? "status query timed out after 5000ms" : "status query unavailable";
|
|
3366
|
+
} finally {
|
|
3367
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
3368
|
+
}
|
|
3369
|
+
if (!ownsRecovery()) return;
|
|
3370
|
+
await this.#failSession(
|
|
3371
|
+
id,
|
|
3372
|
+
adapter,
|
|
3373
|
+
new AcpSdkAdapterError(
|
|
3374
|
+
"terminal_uncertain",
|
|
3375
|
+
`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.`,
|
|
3376
|
+
),
|
|
3377
|
+
);
|
|
3378
|
+
}
|
|
3379
|
+
|
|
2904
3380
|
async #recoverSessionAfterTransportFailureAsync(
|
|
2905
3381
|
id: string,
|
|
2906
3382
|
adapter: AcpSdkAdapter,
|
|
@@ -2948,13 +3424,15 @@ export class AcpAgent implements Agent {
|
|
|
2948
3424
|
if (attaching) await Promise.allSettled([attaching.task]);
|
|
2949
3425
|
// A CLI-primary host owns its own process lifecycle: release this ACP
|
|
2950
3426
|
// attachment locally and never ask the broker to close the session.
|
|
2951
|
-
|
|
3427
|
+
const closesHost = this.#ownedSessionIds.has(id);
|
|
3428
|
+
await this.#teardownSession(id, "closed", closesHost);
|
|
2952
3429
|
this.#knownSessionCwds.delete(id);
|
|
2953
3430
|
this.#ownedSessionIds.delete(id);
|
|
2954
3431
|
this.#knownSessionMcpServers.delete(id);
|
|
2955
3432
|
this.#knownSessionMetadata.delete(id);
|
|
2956
3433
|
this.#retiredPromptCorrelations.delete(id);
|
|
2957
|
-
|
|
3434
|
+
// Detaching locally cannot retire an outstanding host acknowledgement.
|
|
3435
|
+
if (closesHost) this.#retiredPromptAcknowledgements.delete(id);
|
|
2958
3436
|
return {};
|
|
2959
3437
|
} finally {
|
|
2960
3438
|
this.#finishTeardown(id);
|
|
@@ -3192,7 +3670,7 @@ export class AcpAgent implements Agent {
|
|
|
3192
3670
|
* running forever behind a dead session.
|
|
3193
3671
|
*/
|
|
3194
3672
|
#armPromptWatchdog(id: string, record: SessionRecord, waiter: PromptWaiter): void {
|
|
3195
|
-
if (waiter.settled) return;
|
|
3673
|
+
if (waiter.settled || record.statusRecovery === waiter) return;
|
|
3196
3674
|
waiter.cancelWatchdog?.();
|
|
3197
3675
|
// No frame can still arrive once the transport is known to be gone, so waiting out
|
|
3198
3676
|
// the full bound would only add dead time to an outcome that is already decided.
|
|
@@ -3255,17 +3733,40 @@ export class AcpAgent implements Agent {
|
|
|
3255
3733
|
* turn can no longer be observed, it does not cancel or tear down the session.
|
|
3256
3734
|
*/
|
|
3257
3735
|
async #expirePromptWatchdog(id: string, record: SessionRecord, waiter: PromptWaiter): Promise<void> {
|
|
3258
|
-
if (
|
|
3736
|
+
if (
|
|
3737
|
+
record.activePrompt !== waiter ||
|
|
3738
|
+
waiter.settled ||
|
|
3739
|
+
waiter.terminalReserved ||
|
|
3740
|
+
record.statusRecovery === waiter
|
|
3741
|
+
)
|
|
3742
|
+
return;
|
|
3259
3743
|
const silenceMs = Math.max(0, this.#promptWatchdogClock.now() - waiter.lastFrameAt);
|
|
3260
|
-
|
|
3744
|
+
// A prompt still in preflight has never reached the host, so "stopped producing frames"
|
|
3745
|
+
// would point the reader at the wrong phase: nothing was ever dispatched to produce them.
|
|
3746
|
+
const cause =
|
|
3747
|
+
this.#promptTransportGone(id, record) ??
|
|
3748
|
+
(waiter.awaitingProviderPreflight
|
|
3749
|
+
? "the SDK session host never finished provider preflight"
|
|
3750
|
+
: "the SDK session host stopped producing frames");
|
|
3751
|
+
// Counts only: the plan's contents are model-authored and belong on the bounded wire
|
|
3752
|
+
// projection, not in the log line (issue #5669).
|
|
3753
|
+
const plan = waiter.planSnapshot;
|
|
3261
3754
|
logger.error("acp_prompt_watchdog_expired", {
|
|
3262
3755
|
sessionId: id,
|
|
3263
3756
|
cause,
|
|
3264
3757
|
silenceMs,
|
|
3265
3758
|
lastFrameType: waiter.lastFrameType,
|
|
3759
|
+
awaitingProviderPreflight: waiter.awaitingProviderPreflight,
|
|
3266
3760
|
inactivityBoundMs: waiter.activity.inactivityBoundMs,
|
|
3267
3761
|
toolRunning: waiter.activity.running,
|
|
3268
3762
|
awaitingModel: waiter.activity.awaitingModel,
|
|
3763
|
+
...(plan
|
|
3764
|
+
? {
|
|
3765
|
+
planIncomplete: plan.some(planEntryUnfinished),
|
|
3766
|
+
planPendingCount: plan.filter(planEntryUnfinished).length,
|
|
3767
|
+
planTotalCount: plan.length,
|
|
3768
|
+
}
|
|
3769
|
+
: {}),
|
|
3269
3770
|
...(waiter.correlation.commandId ? { commandId: waiter.correlation.commandId } : {}),
|
|
3270
3771
|
...(waiter.correlation.turnId ? { turnId: waiter.correlation.turnId } : {}),
|
|
3271
3772
|
});
|
|
@@ -3273,11 +3774,12 @@ export class AcpAgent implements Agent {
|
|
|
3273
3774
|
record,
|
|
3274
3775
|
id,
|
|
3275
3776
|
waiter,
|
|
3276
|
-
new
|
|
3777
|
+
new AcpPromptAbandonedError(
|
|
3277
3778
|
"prompt_abandoned",
|
|
3278
3779
|
`ACP prompt was abandoned after ${Math.round(silenceMs / 1_000)}s of silence: ${cause}. Last frame was ` +
|
|
3279
3780
|
`"${waiter.lastFrameType}" (${describeCorrelation(waiter.correlation)}). The turn was settled so the ` +
|
|
3280
3781
|
`client stops waiting; the session still accepts the next prompt.`,
|
|
3782
|
+
plan,
|
|
3281
3783
|
),
|
|
3282
3784
|
);
|
|
3283
3785
|
}
|
|
@@ -3343,6 +3845,18 @@ export class AcpAgent implements Agent {
|
|
|
3343
3845
|
await this.#settleCancelledPrompt(id, record, waiter);
|
|
3344
3846
|
return;
|
|
3345
3847
|
}
|
|
3848
|
+
if (waiter.terminalReserved) return;
|
|
3849
|
+
if (record.statusRecovery === waiter || (waiter.dispatched && !waiter.acknowledged)) {
|
|
3850
|
+
await this.#failSession(
|
|
3851
|
+
id,
|
|
3852
|
+
adapter,
|
|
3853
|
+
new AcpSdkAdapterError(
|
|
3854
|
+
"terminal_uncertain",
|
|
3855
|
+
`Prompt owner connection changed before retained evidence was resolved. No mutation was replayed. Inspect turn.result for kind=${waiter.invocationKind}, clientRef=${waiter.clientRef}.`,
|
|
3856
|
+
),
|
|
3857
|
+
);
|
|
3858
|
+
return;
|
|
3859
|
+
}
|
|
3346
3860
|
record.activePrompt = undefined;
|
|
3347
3861
|
record.busy = record.backgroundBusy;
|
|
3348
3862
|
clearPromptWatchdog(waiter);
|
|
@@ -3467,7 +3981,11 @@ export class AcpAgent implements Agent {
|
|
|
3467
3981
|
// the first-turn retry gate — which the settlement below releases — would see
|
|
3468
3982
|
// "started, no output", resubmit, and let BOTH this terminal's final text and the
|
|
3469
3983
|
// retry's answer reach ACP consumers. Record the output before settling (review P1).
|
|
3470
|
-
|
|
3984
|
+
// Whitespace-only final text carries no assistant content. `hasAcpFinalTextContent` is the
|
|
3985
|
+
// single presence predicate shared with the terminal publication gate below, so a terminal
|
|
3986
|
+
// this gate treats as "no output" can never publish a chunk alongside the retry's answer.
|
|
3987
|
+
if (typeof event.finalText === "string" && hasAcpFinalTextContent(event.finalText))
|
|
3988
|
+
record.promptObservedAssistantOutput = true;
|
|
3471
3989
|
// Failure diagnostics are useful but advisory. Settle before any mapped
|
|
3472
3990
|
// session update can await a backpressured client transport; otherwise an
|
|
3473
3991
|
// already-decided failure can still lose to the inactivity watchdog.
|
|
@@ -3534,6 +4052,12 @@ export class AcpAgent implements Agent {
|
|
|
3534
4052
|
notification.update.content.type === "text"
|
|
3535
4053
|
)
|
|
3536
4054
|
promptOwner.emittedAssistantText += notification.update.content.text;
|
|
4055
|
+
// The newest plan REPLACES the retained one: a plan update is the whole list, not a
|
|
4056
|
+
// delta, so `todo_auto_clear`'s empty `entries` correctly leaves no pending work behind
|
|
4057
|
+
// (issue #5669). Retained here rather than at frame ingress because this is where the
|
|
4058
|
+
// mapper has already decided what the client is being told the plan is.
|
|
4059
|
+
if (promptOwner && notification.update.sessionUpdate === "plan")
|
|
4060
|
+
promptOwner.planSnapshot = notification.update.entries;
|
|
3537
4061
|
// A live assistant text/thought chunk for this prompt owner has now been exposed to
|
|
3538
4062
|
// ACP consumers. Record it so the first-turn readiness retry is vetoed: a re-submit
|
|
3539
4063
|
// would deliver a second attempt's output on top of these chunks (issue #5574).
|
|
@@ -3623,13 +4147,15 @@ export class AcpAgent implements Agent {
|
|
|
3623
4147
|
publicationGeneration: number,
|
|
3624
4148
|
event: JsonObject,
|
|
3625
4149
|
promptOwner: PromptWaiter | undefined,
|
|
4150
|
+
publicationBarrier?: Promise<void>,
|
|
3626
4151
|
): void {
|
|
3627
4152
|
const failedTerminal = event.type === "agent_failed";
|
|
3628
4153
|
if (promptOwner) this.#flushFailureDiagnostics(id, promptOwner, adapter);
|
|
3629
|
-
let decorationStart = Promise.resolve();
|
|
4154
|
+
let decorationStart = publicationBarrier ?? Promise.resolve();
|
|
3630
4155
|
const finalText = typeof event.finalText === "string" ? event.finalText : "";
|
|
3631
|
-
if (promptOwner && finalText) {
|
|
4156
|
+
if (promptOwner && hasAcpFinalTextContent(finalText)) {
|
|
3632
4157
|
const finalTextTask = (async () => {
|
|
4158
|
+
if (publicationBarrier) await publicationBarrier;
|
|
3633
4159
|
await Bun.sleep(0);
|
|
3634
4160
|
const resolution = resolveAcpFinalText(promptOwner.emittedAssistantText, finalText);
|
|
3635
4161
|
if (resolution.kind === "emit") {
|
|
@@ -3641,7 +4167,9 @@ export class AcpAgent implements Agent {
|
|
|
3641
4167
|
update: {
|
|
3642
4168
|
sessionUpdate: "agent_message_chunk",
|
|
3643
4169
|
content: { type: "text", text: resolution.text },
|
|
3644
|
-
...(resolution.final.truncated
|
|
4170
|
+
...(resolution.final.truncated || event.finalTextTruncated === true
|
|
4171
|
+
? { _meta: { gjcFinalTextTruncated: true } }
|
|
4172
|
+
: {}),
|
|
3645
4173
|
},
|
|
3646
4174
|
},
|
|
3647
4175
|
adapter,
|
|
@@ -3656,6 +4184,24 @@ export class AcpAgent implements Agent {
|
|
|
3656
4184
|
finalLength: resolution.final.text.length,
|
|
3657
4185
|
});
|
|
3658
4186
|
}
|
|
4187
|
+
if (resolution.kind !== "emit" && event.finalTextTruncated === true) {
|
|
4188
|
+
// A retained prefix may already equal the streamed text, leaving no suffix for
|
|
4189
|
+
// `resolveAcpFinalText` to emit. Still publish a metadata-only chunk so the
|
|
4190
|
+
// caller can distinguish an incomplete retained result from a complete answer.
|
|
4191
|
+
await this.#publishSessionUpdate(
|
|
4192
|
+
id,
|
|
4193
|
+
{
|
|
4194
|
+
sessionId: id,
|
|
4195
|
+
update: {
|
|
4196
|
+
sessionUpdate: "agent_message_chunk",
|
|
4197
|
+
content: { type: "text", text: "" },
|
|
4198
|
+
_meta: { gjcFinalTextTruncated: true },
|
|
4199
|
+
},
|
|
4200
|
+
},
|
|
4201
|
+
adapter,
|
|
4202
|
+
publicationGeneration,
|
|
4203
|
+
);
|
|
4204
|
+
}
|
|
3659
4205
|
})();
|
|
3660
4206
|
let finalTextTail: Promise<void>;
|
|
3661
4207
|
finalTextTail = finalTextTask
|