@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
|
@@ -131,6 +131,7 @@ import {
|
|
|
131
131
|
SERVER_OVERLOADED_PROVIDER_CODE,
|
|
132
132
|
STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
|
|
133
133
|
} from "@gajae-code/ai/utils/fallback-transport";
|
|
134
|
+
import { REPETITION_GUARD_ERROR_CODE } from "@gajae-code/ai/utils/stream-repetition-guard";
|
|
134
135
|
import { AttemptRecordStore } from "./attempt-record-store";
|
|
135
136
|
import {
|
|
136
137
|
BTW_MAX_ANSWER_UTF8_BYTES,
|
|
@@ -142,6 +143,7 @@ import {
|
|
|
142
143
|
truncateUtf8,
|
|
143
144
|
utf8ByteLength,
|
|
144
145
|
} from "./btw-contract";
|
|
146
|
+
import { createSessionWorkLease, type SessionWorkLease, type SessionWorkLeaseHandle } from "./session-work-lease";
|
|
145
147
|
import { DEFAULT_ARTIFACT_MAX_BYTES, truncateHeadBytes } from "./streaming-output";
|
|
146
148
|
|
|
147
149
|
export interface ForkContextSeedMetadata {
|
|
@@ -197,6 +199,7 @@ import {
|
|
|
197
199
|
logger,
|
|
198
200
|
prompt,
|
|
199
201
|
Snowflake,
|
|
202
|
+
safeErrorDescription,
|
|
200
203
|
} from "@gajae-code/utils";
|
|
201
204
|
import { createAppendOnlyContextManager, resolveAppendOnlyMode } from "../append-only-mode";
|
|
202
205
|
import {
|
|
@@ -211,7 +214,12 @@ import {
|
|
|
211
214
|
import { reset as resetCapabilities } from "../capability";
|
|
212
215
|
import type { Rule } from "../capability/rule";
|
|
213
216
|
import type { CasReceipt } from "../config/atomic-yaml-patch";
|
|
214
|
-
import {
|
|
217
|
+
import {
|
|
218
|
+
activateModelProfile,
|
|
219
|
+
applyModelProfileRuntimeBindings,
|
|
220
|
+
materializeActiveModelProfileAssignment,
|
|
221
|
+
resolveMissingSessionModelRecovery,
|
|
222
|
+
} from "../config/model-profile-activation";
|
|
215
223
|
import {
|
|
216
224
|
ModelProfileRegistryError,
|
|
217
225
|
resolveModelProfileName,
|
|
@@ -236,6 +244,7 @@ import {
|
|
|
236
244
|
resolveModelChainWithAuth,
|
|
237
245
|
resolveModelRoleValue,
|
|
238
246
|
type ScopedModelSelection,
|
|
247
|
+
splitSelectorThinkingSuffix,
|
|
239
248
|
} from "../config/model-resolver";
|
|
240
249
|
import { type ModelSelectorValue, normalizeModelSelectorValue } from "../config/model-selector-value";
|
|
241
250
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
@@ -385,7 +394,8 @@ import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
|
385
394
|
import type { LazyService } from "../runtime/lazy-service";
|
|
386
395
|
import type { NetworkPrewarmRuntime } from "../runtime/network-prewarm-service";
|
|
387
396
|
import type { WorkspaceTreeRuntime } from "../runtime/workspace-tree-service";
|
|
388
|
-
import { MCPManager } from "../runtime-mcp/manager";
|
|
397
|
+
import { type ExactMcpServerControlResult, MCPManager } from "../runtime-mcp/manager";
|
|
398
|
+
import { attachExactMcpControls, getExactMcpControls, revokeExactMcpControls } from "../runtime-mcp/redaction";
|
|
389
399
|
import type { NotificationSessionController } from "../sdk/bus/session-control";
|
|
390
400
|
import { buildSyntheticModelId, syntheticNamespaceCollision } from "../sdk/model-profile-model";
|
|
391
401
|
import { sanitizePromptFailure } from "../sdk/prompt-failure";
|
|
@@ -432,6 +442,7 @@ import { parseCommandArgs } from "../utils/command-args";
|
|
|
432
442
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
433
443
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
434
444
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
445
|
+
import { invalidateSessionTitleGeneration } from "../utils/session-title-generation";
|
|
435
446
|
import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-choice";
|
|
436
447
|
import { buildWorkflowIntentDiff, WORKFLOW_INTENT_DIFF_CUSTOM_TYPE } from "../workflow/workflow-intent-diff";
|
|
437
448
|
import { buildWorkspaceTree, type WorkspaceTree } from "../workspace-tree";
|
|
@@ -449,6 +460,7 @@ import {
|
|
|
449
460
|
effectiveFallbackDelay,
|
|
450
461
|
FallbackChainController,
|
|
451
462
|
type FallbackChainRuntimeState,
|
|
463
|
+
sanitizeCompactionCandidateFailureMessage,
|
|
452
464
|
} from "./fallback-chain-controller";
|
|
453
465
|
|
|
454
466
|
export { DefaultModelSelectionRecoveryError } from "./default-model-selection";
|
|
@@ -508,6 +520,7 @@ import {
|
|
|
508
520
|
getLatestCompactionEntry,
|
|
509
521
|
getSessionMessageEntryId,
|
|
510
522
|
getSessionMessageObservationId,
|
|
523
|
+
SESSION_LIMIT_RECOVERY_ACTIONS,
|
|
511
524
|
SessionAppendPersistenceError,
|
|
512
525
|
SessionContextTooLargeError,
|
|
513
526
|
SessionManager,
|
|
@@ -835,6 +848,56 @@ export type AgentSessionEvent =
|
|
|
835
848
|
| { type: "thinking_level_changed"; thinkingLevel: ThinkingLevel | undefined }
|
|
836
849
|
| { type: "goal_updated"; goal: Goal | null; state?: GoalModeState };
|
|
837
850
|
|
|
851
|
+
export type QueuedInputDelivery = "steer" | "followUp";
|
|
852
|
+
export type QueuedInputQueuePolicy = "respect-mode" | "sequential";
|
|
853
|
+
export type QueuedInputRemovalReason = "cancelled" | "removed";
|
|
854
|
+
|
|
855
|
+
export interface QueuedInputAdmission {
|
|
856
|
+
submissionId: string;
|
|
857
|
+
delivery: QueuedInputDelivery;
|
|
858
|
+
queuePolicy: QueuedInputQueuePolicy;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export type QueuedInputExecution =
|
|
862
|
+
| {
|
|
863
|
+
submissionId: string;
|
|
864
|
+
delivery: QueuedInputDelivery;
|
|
865
|
+
disposition: "joined-current-run" | "promoted-to-run";
|
|
866
|
+
attemptScope: AttemptScopeRef;
|
|
867
|
+
}
|
|
868
|
+
| {
|
|
869
|
+
submissionId: string;
|
|
870
|
+
delivery: QueuedInputDelivery;
|
|
871
|
+
disposition: "removed";
|
|
872
|
+
reason: QueuedInputRemovalReason;
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
export type QueuedInputTerminal =
|
|
876
|
+
| {
|
|
877
|
+
submissionId: string;
|
|
878
|
+
delivery: QueuedInputDelivery;
|
|
879
|
+
disposition: "completed";
|
|
880
|
+
attemptScope: AttemptScopeRef;
|
|
881
|
+
}
|
|
882
|
+
| {
|
|
883
|
+
submissionId: string;
|
|
884
|
+
delivery: QueuedInputDelivery;
|
|
885
|
+
disposition: "removed";
|
|
886
|
+
reason: QueuedInputRemovalReason;
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Lifecycle receipt for one explicitly queued `submitUserMessage` submission.
|
|
891
|
+
*/
|
|
892
|
+
export interface QueuedInputSubmission {
|
|
893
|
+
readonly submissionId: string;
|
|
894
|
+
readonly admitted: Promise<QueuedInputAdmission>;
|
|
895
|
+
readonly execution: Promise<QueuedInputExecution>;
|
|
896
|
+
readonly terminal: Promise<QueuedInputTerminal>;
|
|
897
|
+
/** Remove the submission if it is still queued. Returns false after execution or removal. */
|
|
898
|
+
readonly cancel: () => boolean;
|
|
899
|
+
}
|
|
900
|
+
|
|
838
901
|
/** Listener function for agent session events */
|
|
839
902
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
840
903
|
export type AsyncJobSnapshotItem = Pick<
|
|
@@ -1115,6 +1178,29 @@ export interface PromptOptions {
|
|
|
1115
1178
|
sdkRunCapability?: unknown;
|
|
1116
1179
|
}
|
|
1117
1180
|
|
|
1181
|
+
export interface SendUserMessageOptions {
|
|
1182
|
+
deliverAs?: QueuedInputDelivery;
|
|
1183
|
+
/** Preserve a busy SDK dispatch as queued work across an admission fence. */
|
|
1184
|
+
queuedAtDispatch?: boolean;
|
|
1185
|
+
/** Delivery ordering for a tracked or SDK-queued submission. */
|
|
1186
|
+
queuePolicy?: QueuedInputQueuePolicy;
|
|
1187
|
+
onPreflightAccepted?: () => void;
|
|
1188
|
+
onPreflightAcceptCommit?: () => void | Promise<void>;
|
|
1189
|
+
/** Internal SDK ownership callback. */
|
|
1190
|
+
onQueuedPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
|
|
1191
|
+
/** Internal dispatch disposition used before actual queue consumption. */
|
|
1192
|
+
onDispatchDisposition?: (promotion: { startsOwnRun: boolean }) => void;
|
|
1193
|
+
preflightSignal?: AbortSignal;
|
|
1194
|
+
sdkRunCapability?: unknown;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
export interface TrackedSendUserMessageOptions extends SendUserMessageOptions {
|
|
1198
|
+
deliverAs: QueuedInputDelivery;
|
|
1199
|
+
trackSubmission: true;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
type SendUserMessageDispatchOptions = SendUserMessageOptions & { trackSubmission?: boolean };
|
|
1203
|
+
|
|
1118
1204
|
type InternalPromptOptions = PromptOptions & { sdkRunToken?: string };
|
|
1119
1205
|
type InternalCustomMessageOptions = Pick<
|
|
1120
1206
|
PromptOptions,
|
|
@@ -1129,6 +1215,32 @@ type InternalCustomMessageOptions = Pick<
|
|
|
1129
1215
|
sdkRunToken?: string;
|
|
1130
1216
|
};
|
|
1131
1217
|
|
|
1218
|
+
function assertQueuedInputQueuePolicy(queuePolicy: unknown): asserts queuePolicy is QueuedInputQueuePolicy | undefined {
|
|
1219
|
+
if (queuePolicy !== undefined && queuePolicy !== "respect-mode" && queuePolicy !== "sequential") {
|
|
1220
|
+
throw Object.assign(new Error("queuePolicy must be respect-mode or sequential."), { code: "invalid_input" });
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function assertTrackedSendUserMessageOptions(options: unknown): asserts options is TrackedSendUserMessageOptions {
|
|
1225
|
+
const candidate = options !== null && typeof options === "object" ? (options as Record<string, unknown>) : undefined;
|
|
1226
|
+
if (candidate?.trackSubmission !== true || (candidate.deliverAs !== "steer" && candidate.deliverAs !== "followUp")) {
|
|
1227
|
+
throw Object.assign(
|
|
1228
|
+
new Error("submitUserMessage requires trackSubmission: true and deliverAs: steer or followUp."),
|
|
1229
|
+
{ code: "invalid_input" },
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
assertQueuedInputQueuePolicy(candidate?.queuePolicy);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function assertLegacySendUserMessageOptions(options: unknown): void {
|
|
1236
|
+
const candidate = options !== null && typeof options === "object" ? (options as Record<string, unknown>) : undefined;
|
|
1237
|
+
if (candidate?.trackSubmission !== undefined) {
|
|
1238
|
+
throw Object.assign(new Error("sendUserMessage does not support trackSubmission; use submitUserMessage."), {
|
|
1239
|
+
code: "invalid_input",
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1132
1244
|
function promptPreflightCancelledError(): Error {
|
|
1133
1245
|
const error = Object.assign(new Error("Prompt preflight was cancelled before execution."), { code: "busy" });
|
|
1134
1246
|
error.name = "PromptPreflightCancelledError";
|
|
@@ -2004,6 +2116,40 @@ function extractPermissionLocations(
|
|
|
2004
2116
|
type QueuedDisplayEntry = { text: string; tag?: string; sequence: number; message?: AgentMessage };
|
|
2005
2117
|
type IrcRosterClaim = { token: symbol; signature: string; epoch: number; message: CustomMessage };
|
|
2006
2118
|
type QueuedFollowUpOwner = { cancel(): boolean };
|
|
2119
|
+
type QueueFollowUpOptions = {
|
|
2120
|
+
forceOneAtATime?: boolean;
|
|
2121
|
+
claimsGenuineUserIntent?: boolean;
|
|
2122
|
+
onPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
|
|
2123
|
+
sdkRunToken?: string;
|
|
2124
|
+
onQueued?: (message: AgentMessage) => void;
|
|
2125
|
+
scheduleNonAdmittedWake?: boolean;
|
|
2126
|
+
onQueuedAfterAdmission?: (message: AgentMessage, cancelQueued: () => boolean) => void;
|
|
2127
|
+
allowDuringSessionTransition?: boolean;
|
|
2128
|
+
allowCancelAndSubmit?: boolean;
|
|
2129
|
+
createDisplayEntry?: boolean;
|
|
2130
|
+
trackExternalFollowUp?: boolean;
|
|
2131
|
+
};
|
|
2132
|
+
type DeferredValue<T> = {
|
|
2133
|
+
promise: Promise<T>;
|
|
2134
|
+
resolve: (value: T | PromiseLike<T>) => void;
|
|
2135
|
+
reject: (reason?: unknown) => void;
|
|
2136
|
+
};
|
|
2137
|
+
type TrackedQueuedInput = {
|
|
2138
|
+
submission: QueuedInputSubmission;
|
|
2139
|
+
admitted: DeferredValue<QueuedInputAdmission>;
|
|
2140
|
+
execution: DeferredValue<QueuedInputExecution>;
|
|
2141
|
+
terminal: DeferredValue<QueuedInputTerminal>;
|
|
2142
|
+
delivery: QueuedInputDelivery;
|
|
2143
|
+
queuePolicy: QueuedInputQueuePolicy;
|
|
2144
|
+
message?: AgentMessage;
|
|
2145
|
+
cancelQueued?: () => boolean;
|
|
2146
|
+
removePreflightAbortListener?: () => void;
|
|
2147
|
+
attemptScope?: AttemptScope;
|
|
2148
|
+
logicalRunId?: AttemptRunHandle["logicalRunId"];
|
|
2149
|
+
executionSettled: boolean;
|
|
2150
|
+
terminalSettled: boolean;
|
|
2151
|
+
cancelRequested: boolean;
|
|
2152
|
+
};
|
|
2007
2153
|
export type QueuedMessageEditMode = "steer" | "followUp";
|
|
2008
2154
|
|
|
2009
2155
|
export interface QueuedMessageEditEntry {
|
|
@@ -2033,6 +2179,7 @@ export type BeforeAgentStartContributor = (event: {
|
|
|
2033
2179
|
}) => Promise<BeforeAgentStartInternalMessage | undefined>;
|
|
2034
2180
|
|
|
2035
2181
|
const AGENT_END_WORKER_INTEGRATION_TIMEOUT_MS = 5_000;
|
|
2182
|
+
const COORDINATOR_PERSISTENCE_TEST_IDLE_TIMEOUT_MS = 1_000;
|
|
2036
2183
|
const POST_PUBLICATION_ERROR_MAX_BYTES = 512;
|
|
2037
2184
|
|
|
2038
2185
|
/**
|
|
@@ -2294,7 +2441,93 @@ export class StreamingEditFileCache {
|
|
|
2294
2441
|
return this.#totalBytes;
|
|
2295
2442
|
}
|
|
2296
2443
|
}
|
|
2297
|
-
type SessionAdmissionKind = "prompt" | "selection";
|
|
2444
|
+
type SessionAdmissionKind = "prompt" | "selection" | "mcp-control";
|
|
2445
|
+
|
|
2446
|
+
/**
|
|
2447
|
+
* Startup phase of the session-owned exact-config MCP manager.
|
|
2448
|
+
*
|
|
2449
|
+
* `not-started` and `no-servers-declared` are distinguishable only because the
|
|
2450
|
+
* snapshot reads the declared server list from the exact config; the manager
|
|
2451
|
+
* alone reports an empty name list in both cases.
|
|
2452
|
+
*/
|
|
2453
|
+
export type ExactMcpStartupState = "not-started" | "no-servers-declared" | "in-flight" | "settled";
|
|
2454
|
+
|
|
2455
|
+
/**
|
|
2456
|
+
* Per-server state.
|
|
2457
|
+
*
|
|
2458
|
+
* `unavailable` covers a server the config declares that the manager has no
|
|
2459
|
+
* live record of. A tools-only manager erases a server from its own bookkeeping
|
|
2460
|
+
* when the connection fails, so a failed server and one whose startup has not
|
|
2461
|
+
* reached it are indistinguishable from here; both surface as `unavailable`
|
|
2462
|
+
* rather than being dropped from the table.
|
|
2463
|
+
*/
|
|
2464
|
+
export type ExactMcpServerState = "connecting" | "connected" | "disconnected" | "unavailable";
|
|
2465
|
+
export type ExactMcpServerDisplayState = ExactMcpServerState | "suspended";
|
|
2466
|
+
|
|
2467
|
+
/**
|
|
2468
|
+
* Transport a server is declared with. Taken from the config's own
|
|
2469
|
+
* discriminator, never from a live connection: `getConnection` throws on a
|
|
2470
|
+
* tools-only manager, so the declaration is the only source available here.
|
|
2471
|
+
*/
|
|
2472
|
+
export type ExactMcpTransport = "stdio" | "http" | "sse" | "unknown";
|
|
2473
|
+
|
|
2474
|
+
export interface ExactMcpServerStatus {
|
|
2475
|
+
readonly name: string;
|
|
2476
|
+
readonly transport: ExactMcpTransport;
|
|
2477
|
+
readonly state: ExactMcpServerDisplayState;
|
|
2478
|
+
readonly toolCount: number;
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
/**
|
|
2482
|
+
* Whitelisted status shape for the TUI `/mcp` command. Every field here is
|
|
2483
|
+
* either a server name the operator already supplied or a derived enum/count;
|
|
2484
|
+
* pool keys, endpoints, commands, env, headers, tokens, and raw exception text
|
|
2485
|
+
* have no field to travel in.
|
|
2486
|
+
*/
|
|
2487
|
+
export interface ExactMcpStatusSnapshot {
|
|
2488
|
+
readonly startup: ExactMcpStartupState;
|
|
2489
|
+
readonly servers: readonly ExactMcpServerStatus[];
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
export type ExactMcpControlAction = "suspend" | "resume" | "reconnect";
|
|
2493
|
+
|
|
2494
|
+
export interface ExactMcpControlResult extends ExactMcpServerControlResult {
|
|
2495
|
+
readonly action: ExactMcpControlAction;
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
type ExactMcpSessionStateSnapshot = {
|
|
2499
|
+
toolRegistry: Map<string, AgentTool>;
|
|
2500
|
+
discoverableMCPTools: Map<string, DiscoverableTool>;
|
|
2501
|
+
selectedMCPToolNames: Set<string>;
|
|
2502
|
+
selectedDiscoveredToolNames: Set<string>;
|
|
2503
|
+
tools: AgentTool[];
|
|
2504
|
+
baseSystemPrompt: string[];
|
|
2505
|
+
systemPrompt: string[];
|
|
2506
|
+
lastAppliedToolSignature: string | undefined;
|
|
2507
|
+
pendingAppliedToolSignature: string | undefined;
|
|
2508
|
+
defaultModelSelectionMutationRevision: number;
|
|
2509
|
+
baseSystemPromptGeneration: number;
|
|
2510
|
+
suspendedTools: Map<string, { selected: string[]; active: string[] }>;
|
|
2511
|
+
};
|
|
2512
|
+
type PreparedExactMcpRetirement = {
|
|
2513
|
+
canCommit(): boolean;
|
|
2514
|
+
commit(): Promise<void>;
|
|
2515
|
+
abort(): Promise<void>;
|
|
2516
|
+
};
|
|
2517
|
+
|
|
2518
|
+
/**
|
|
2519
|
+
* An entry states its transport with `type`, or implies it: a `command` entry is
|
|
2520
|
+
* stdio and a `url` entry is http. Only the discriminator is read — the command
|
|
2521
|
+
* string and URL themselves stay inside the config.
|
|
2522
|
+
*/
|
|
2523
|
+
function exactMcpTransportOf(entry: unknown): ExactMcpTransport {
|
|
2524
|
+
if (!entry || typeof entry !== "object") return "unknown";
|
|
2525
|
+
const record = entry as { type?: unknown; command?: unknown; url?: unknown };
|
|
2526
|
+
if (record.type === "stdio" || record.type === "http" || record.type === "sse") return record.type;
|
|
2527
|
+
if (typeof record.command === "string") return "stdio";
|
|
2528
|
+
if (typeof record.url === "string") return "http";
|
|
2529
|
+
return "unknown";
|
|
2530
|
+
}
|
|
2298
2531
|
class SessionRunCancellationDomainBridge implements RunCancellationDomainBridge {
|
|
2299
2532
|
#domains = new Map<string, { domain: RunCancellationDomain; controller: AbortController }>();
|
|
2300
2533
|
#released = new Set<string>();
|
|
@@ -2349,6 +2582,13 @@ type SessionAdmissionEntry = {
|
|
|
2349
2582
|
continuationCapability?: symbol;
|
|
2350
2583
|
};
|
|
2351
2584
|
|
|
2585
|
+
type SuccessorSessionAdmission = {
|
|
2586
|
+
generation: number;
|
|
2587
|
+
sessionId: string | undefined;
|
|
2588
|
+
capability: symbol;
|
|
2589
|
+
active: boolean;
|
|
2590
|
+
};
|
|
2591
|
+
|
|
2352
2592
|
type SessionAdmissionLease = {
|
|
2353
2593
|
release(): void;
|
|
2354
2594
|
};
|
|
@@ -2421,6 +2661,17 @@ type CoordinatorRuntimeStatePersistContext = {
|
|
|
2421
2661
|
sessionFile: string | undefined;
|
|
2422
2662
|
stateFile: string | null;
|
|
2423
2663
|
};
|
|
2664
|
+
|
|
2665
|
+
/** A lost continuation may only settle the emitter/session pair that opened it. */
|
|
2666
|
+
export function isCurrentWorkflowGateContinuation(
|
|
2667
|
+
currentSessionId: string,
|
|
2668
|
+
expectedSessionId: string,
|
|
2669
|
+
currentEmitter: WorkflowGateEmitter | undefined,
|
|
2670
|
+
expectedEmitter: WorkflowGateEmitter,
|
|
2671
|
+
): boolean {
|
|
2672
|
+
return currentSessionId === expectedSessionId && currentEmitter === expectedEmitter;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2424
2675
|
export class AgentSession {
|
|
2425
2676
|
#provisionalStreamingToolCallIds = new Set<string>();
|
|
2426
2677
|
readonly agent: Agent;
|
|
@@ -2465,6 +2716,7 @@ export class AgentSession {
|
|
|
2465
2716
|
#sessionAdmissionClosing = false;
|
|
2466
2717
|
#sessionAdmissionClosed = false;
|
|
2467
2718
|
#sessionAdmissionContext = new AsyncLocalStorage<SessionAdmissionEntry>();
|
|
2719
|
+
#successorSessionAdmissionContext = new AsyncLocalStorage<SuccessorSessionAdmission>();
|
|
2468
2720
|
#selectionFenceGenerationContext = new AsyncLocalStorage<number>();
|
|
2469
2721
|
#selectionFenceTail: Promise<void> = Promise.resolve();
|
|
2470
2722
|
#pendingSelectionFences = 0;
|
|
@@ -2477,6 +2729,7 @@ export class AgentSession {
|
|
|
2477
2729
|
/** Epochs of follow-up reservations still between reservation and durable enqueue. */
|
|
2478
2730
|
#activeFollowUpReservationEpochs = new Set<number>();
|
|
2479
2731
|
#followUpReservationDrainWaiters = new Set<() => void>();
|
|
2732
|
+
#followUpReservationTransitionWaiters = new Set<() => void>();
|
|
2480
2733
|
#selectionFenceGeneration = 0;
|
|
2481
2734
|
#defaultModelSelectionMutationRevision = 0;
|
|
2482
2735
|
#thinkingLevelMutationRevision = 0;
|
|
@@ -2558,8 +2811,15 @@ export class AgentSession {
|
|
|
2558
2811
|
AgentMessage,
|
|
2559
2812
|
(promotion: { startsOwnRun?: boolean; removed?: boolean }) => void
|
|
2560
2813
|
>();
|
|
2561
|
-
/**
|
|
2814
|
+
/** Deferred follow-ups held outside Agent's live queue until the active run ends. */
|
|
2562
2815
|
#deferredSdkFollowUps: AgentMessage[] = [];
|
|
2816
|
+
/** Per-message delivery policy for deferred follow-ups; WeakSet metadata survives array snapshots. */
|
|
2817
|
+
#deferredFollowUpForceOneAtATime = new WeakSet<AgentMessage>();
|
|
2818
|
+
/** Atomic all-mode cohorts that must be released together from the deferred queue. */
|
|
2819
|
+
#deferredFollowUpBatches = new WeakMap<AgentMessage, readonly AgentMessage[]>();
|
|
2820
|
+
/** Admission order for externally submitted queued messages across both queue stores. */
|
|
2821
|
+
#queuedAdmissionSequence = 0;
|
|
2822
|
+
readonly #queuedAdmissionSeq = new WeakMap<AgentMessage, number>();
|
|
2563
2823
|
// Client/SDK steering (turn.prompt diverted to steer while streaming, or an
|
|
2564
2824
|
// explicit turn.steer) is an independent root-turn request ONLY when it is
|
|
2565
2825
|
// admitted AFTER the terminal abort snapshot: a terminal abort admitted
|
|
@@ -2597,26 +2857,230 @@ export class AgentSession {
|
|
|
2597
2857
|
readonly #terminalAbortSteeringSnapshotKeys = new Map<number, string>();
|
|
2598
2858
|
#terminalAbortAdmissionSeq = 0;
|
|
2599
2859
|
#queuedDisplaySequence = 0;
|
|
2860
|
+
readonly #trackedQueuedInputs = new Map<string, TrackedQueuedInput>();
|
|
2861
|
+
readonly #trackedQueuedInputsAwaitingOwnRun = new Set<TrackedQueuedInput>();
|
|
2862
|
+
readonly #trackedQueuedInputsByLogicalRunId = new Map<AttemptRunHandle["logicalRunId"], Set<TrackedQueuedInput>>();
|
|
2863
|
+
readonly #trackedQueuedInputsByAttemptScope = new WeakMap<AttemptScope, Set<TrackedQueuedInput>>();
|
|
2864
|
+
|
|
2865
|
+
#createTrackedQueuedInput(delivery: QueuedInputDelivery, queuePolicy: QueuedInputQueuePolicy): TrackedQueuedInput {
|
|
2866
|
+
const admitted = Promise.withResolvers<QueuedInputAdmission>();
|
|
2867
|
+
const execution = Promise.withResolvers<QueuedInputExecution>();
|
|
2868
|
+
const terminal = Promise.withResolvers<QueuedInputTerminal>();
|
|
2869
|
+
const submissionId = `queued-${crypto.randomUUID()}`;
|
|
2870
|
+
const state = {} as TrackedQueuedInput;
|
|
2871
|
+
const submission: QueuedInputSubmission = {
|
|
2872
|
+
submissionId,
|
|
2873
|
+
admitted: admitted.promise,
|
|
2874
|
+
execution: execution.promise,
|
|
2875
|
+
terminal: terminal.promise,
|
|
2876
|
+
cancel: () => {
|
|
2877
|
+
if (state.executionSettled || state.terminalSettled || state.cancelQueued === undefined) return false;
|
|
2878
|
+
state.cancelRequested = true;
|
|
2879
|
+
const removed = state.cancelQueued();
|
|
2880
|
+
if (!removed) state.cancelRequested = false;
|
|
2881
|
+
return removed;
|
|
2882
|
+
},
|
|
2883
|
+
};
|
|
2884
|
+
state.submission = submission;
|
|
2885
|
+
state.admitted = admitted;
|
|
2886
|
+
state.execution = execution;
|
|
2887
|
+
state.terminal = terminal;
|
|
2888
|
+
state.delivery = delivery;
|
|
2889
|
+
state.queuePolicy = queuePolicy;
|
|
2890
|
+
state.executionSettled = false;
|
|
2891
|
+
state.terminalSettled = false;
|
|
2892
|
+
state.cancelRequested = false;
|
|
2893
|
+
this.#trackedQueuedInputs.set(submissionId, state);
|
|
2894
|
+
return state;
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
#scopeRef(scope: AttemptScope): AttemptScopeRef {
|
|
2898
|
+
return { attemptId: scope.attemptId, generation: scope.generation, lineage: scope.lineage };
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
#forgetTrackedQueuedInputOwnership(state: TrackedQueuedInput): void {
|
|
2902
|
+
if (state.logicalRunId !== undefined) {
|
|
2903
|
+
const states = this.#trackedQueuedInputsByLogicalRunId.get(state.logicalRunId);
|
|
2904
|
+
if (states) {
|
|
2905
|
+
states.delete(state);
|
|
2906
|
+
if (states.size === 0) this.#trackedQueuedInputsByLogicalRunId.delete(state.logicalRunId);
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
if (state.attemptScope !== undefined) {
|
|
2910
|
+
const states = this.#trackedQueuedInputsByAttemptScope.get(state.attemptScope);
|
|
2911
|
+
if (states) {
|
|
2912
|
+
states.delete(state);
|
|
2913
|
+
if (states.size === 0) this.#trackedQueuedInputsByAttemptScope.delete(state.attemptScope);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
#clearTrackedQueuedInputQueueOwnership(state: TrackedQueuedInput): void {
|
|
2919
|
+
state.removePreflightAbortListener?.();
|
|
2920
|
+
state.removePreflightAbortListener = undefined;
|
|
2921
|
+
state.cancelQueued = undefined;
|
|
2922
|
+
state.message = undefined;
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
#admitTrackedQueuedInput(state: TrackedQueuedInput, message: AgentMessage, cancelQueued: () => boolean): void {
|
|
2926
|
+
if (state.message !== undefined || state.terminalSettled) return;
|
|
2927
|
+
state.message = message;
|
|
2928
|
+
state.cancelQueued = cancelQueued;
|
|
2929
|
+
state.admitted.resolve({
|
|
2930
|
+
submissionId: state.submission.submissionId,
|
|
2931
|
+
delivery: state.delivery,
|
|
2932
|
+
queuePolicy: state.queuePolicy,
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
#settleTrackedAdmissionCallbackFailure(state: TrackedQueuedInput, owner: QueuedFollowUpOwner): void {
|
|
2936
|
+
if (owner.cancel()) return;
|
|
2937
|
+
this.#settleTrackedQueuedInputRemoved(state, "removed");
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
#settleTrackedExecution(
|
|
2941
|
+
state: TrackedQueuedInput,
|
|
2942
|
+
disposition: "joined-current-run" | "promoted-to-run",
|
|
2943
|
+
scope: AttemptScope,
|
|
2944
|
+
logicalRunId?: AttemptRunHandle["logicalRunId"],
|
|
2945
|
+
): void {
|
|
2946
|
+
if (state.executionSettled || state.terminalSettled) return;
|
|
2947
|
+
state.executionSettled = true;
|
|
2948
|
+
this.#clearTrackedQueuedInputQueueOwnership(state);
|
|
2949
|
+
state.attemptScope = scope;
|
|
2950
|
+
const owningLogicalRunId =
|
|
2951
|
+
logicalRunId ?? this.#logicalRunIdByAttemptScope.get(scope) ?? this.#activeLogicalRunId;
|
|
2952
|
+
state.logicalRunId = owningLogicalRunId;
|
|
2953
|
+
if (owningLogicalRunId !== undefined) {
|
|
2954
|
+
let logicalStates = this.#trackedQueuedInputsByLogicalRunId.get(owningLogicalRunId);
|
|
2955
|
+
if (logicalStates === undefined) {
|
|
2956
|
+
logicalStates = new Set<TrackedQueuedInput>();
|
|
2957
|
+
this.#trackedQueuedInputsByLogicalRunId.set(owningLogicalRunId, logicalStates);
|
|
2958
|
+
}
|
|
2959
|
+
logicalStates.add(state);
|
|
2960
|
+
}
|
|
2961
|
+
let scopeStates = this.#trackedQueuedInputsByAttemptScope.get(scope);
|
|
2962
|
+
if (scopeStates === undefined) {
|
|
2963
|
+
scopeStates = new Set<TrackedQueuedInput>();
|
|
2964
|
+
this.#trackedQueuedInputsByAttemptScope.set(scope, scopeStates);
|
|
2965
|
+
}
|
|
2966
|
+
scopeStates.add(state);
|
|
2967
|
+
state.execution.resolve({
|
|
2968
|
+
submissionId: state.submission.submissionId,
|
|
2969
|
+
delivery: state.delivery,
|
|
2970
|
+
disposition,
|
|
2971
|
+
attemptScope: this.#scopeRef(scope),
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
#settleTrackedOwnRunPromotions(handle: AttemptRunHandle): void {
|
|
2976
|
+
if (this.#trackedQueuedInputsAwaitingOwnRun.size === 0) return;
|
|
2977
|
+
for (const state of [...this.#trackedQueuedInputsAwaitingOwnRun]) {
|
|
2978
|
+
this.#trackedQueuedInputsAwaitingOwnRun.delete(state);
|
|
2979
|
+
this.#settleTrackedExecution(state, "promoted-to-run", handle.scope, handle.logicalRunId);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
#settleTrackedOwnRunPromotionFailures(): void {
|
|
2984
|
+
for (const state of [...this.#trackedQueuedInputsAwaitingOwnRun])
|
|
2985
|
+
this.#settleTrackedQueuedInputRemoved(state, "removed");
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
#settleTrackedQueuedInputRemoved(state: TrackedQueuedInput, reason: QueuedInputRemovalReason): void {
|
|
2989
|
+
if (!state.executionSettled) {
|
|
2990
|
+
state.executionSettled = true;
|
|
2991
|
+
state.execution.resolve({
|
|
2992
|
+
submissionId: state.submission.submissionId,
|
|
2993
|
+
delivery: state.delivery,
|
|
2994
|
+
disposition: "removed",
|
|
2995
|
+
reason,
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
if (state.terminalSettled) return;
|
|
2999
|
+
state.terminalSettled = true;
|
|
3000
|
+
this.#clearTrackedQueuedInputQueueOwnership(state);
|
|
3001
|
+
state.terminal.resolve({
|
|
3002
|
+
submissionId: state.submission.submissionId,
|
|
3003
|
+
delivery: state.delivery,
|
|
3004
|
+
disposition: "removed",
|
|
3005
|
+
reason,
|
|
3006
|
+
});
|
|
3007
|
+
this.#trackedQueuedInputsAwaitingOwnRun.delete(state);
|
|
3008
|
+
this.#forgetTrackedQueuedInputOwnership(state);
|
|
3009
|
+
this.#trackedQueuedInputs.delete(state.submission.submissionId);
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
#handleTrackedQueuedInputPromotion(
|
|
3013
|
+
state: TrackedQueuedInput,
|
|
3014
|
+
promotion: { startsOwnRun?: boolean; removed?: boolean },
|
|
3015
|
+
): void {
|
|
3016
|
+
if (promotion.removed) {
|
|
3017
|
+
this.#settleTrackedQueuedInputRemoved(state, state.cancelRequested ? "cancelled" : "removed");
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
3020
|
+
if (promotion.startsOwnRun === true) {
|
|
3021
|
+
this.#trackedQueuedInputsAwaitingOwnRun.add(state);
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
const scope = this.#activeAttemptScope;
|
|
3025
|
+
if (scope === undefined) {
|
|
3026
|
+
this.#settleTrackedQueuedInputRemoved(state, "removed");
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
3029
|
+
this.#settleTrackedExecution(state, "joined-current-run", scope);
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
#settleTrackedQueuedInputTerminal(scope: AttemptScope | undefined): void {
|
|
3033
|
+
if (scope === undefined) return;
|
|
3034
|
+
const logicalRunId = this.#logicalRunIdByAttemptScope.get(scope);
|
|
3035
|
+
const states =
|
|
3036
|
+
(logicalRunId === undefined ? undefined : this.#trackedQueuedInputsByLogicalRunId.get(logicalRunId)) ??
|
|
3037
|
+
this.#trackedQueuedInputsByAttemptScope.get(scope);
|
|
3038
|
+
if (states === undefined) return;
|
|
3039
|
+
for (const state of [...states]) {
|
|
3040
|
+
if (state.terminalSettled) continue;
|
|
3041
|
+
state.terminalSettled = true;
|
|
3042
|
+
this.#clearTrackedQueuedInputQueueOwnership(state);
|
|
3043
|
+
state.terminal.resolve({
|
|
3044
|
+
submissionId: state.submission.submissionId,
|
|
3045
|
+
delivery: state.delivery,
|
|
3046
|
+
disposition: "completed",
|
|
3047
|
+
attemptScope: this.#scopeRef(scope),
|
|
3048
|
+
});
|
|
3049
|
+
this.#forgetTrackedQueuedInputOwnership(state);
|
|
3050
|
+
this.#trackedQueuedInputs.delete(state.submission.submissionId);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
|
|
2600
3054
|
/** Fire the stored promotion hook with a REMOVAL disposition for messages
|
|
2601
3055
|
* leaving their queue without consumption (queue.message.remove, positional
|
|
2602
3056
|
* editing, clearQueue, or the terminal-abort purge). The SDK terminalizes
|
|
2603
3057
|
* those accepted submissions boundedly instead of leaving them accepted
|
|
2604
3058
|
* forever (#4668 review P1). */
|
|
2605
3059
|
#fireQueuedRemovalHooks(messages: readonly AgentMessage[]): void {
|
|
3060
|
+
let callbackError: unknown;
|
|
2606
3061
|
for (const message of messages) {
|
|
2607
3062
|
const steerHook = this.#steerPromotionHooks.get(message);
|
|
2608
3063
|
if (steerHook) {
|
|
2609
3064
|
this.#steerPromotionHooks.delete(message);
|
|
2610
|
-
|
|
3065
|
+
try {
|
|
3066
|
+
steerHook({ startsOwnRun: false, removed: true });
|
|
3067
|
+
} catch (error) {
|
|
3068
|
+
callbackError ??= error;
|
|
3069
|
+
}
|
|
2611
3070
|
}
|
|
2612
3071
|
const followUpHook = this.#followUpPromotionHooks.get(message);
|
|
2613
3072
|
if (followUpHook) {
|
|
2614
3073
|
this.#followUpPromotionHooks.delete(message);
|
|
2615
|
-
|
|
3074
|
+
try {
|
|
3075
|
+
followUpHook({ startsOwnRun: true, removed: true });
|
|
3076
|
+
} catch (error) {
|
|
3077
|
+
callbackError ??= error;
|
|
3078
|
+
}
|
|
2616
3079
|
}
|
|
2617
3080
|
this.#sdkRunTokensByQueuedMessage.delete(message);
|
|
2618
3081
|
this.#deepInterviewGenuineUserMessageEpochs.delete(message);
|
|
2619
3082
|
}
|
|
3083
|
+
if (callbackError !== undefined) logger.warn("Queued removal callback failed", { error: callbackError });
|
|
2620
3084
|
}
|
|
2621
3085
|
|
|
2622
3086
|
/**
|
|
@@ -2737,22 +3201,28 @@ export class AgentSession {
|
|
|
2737
3201
|
// follow-up queued after them, preserving submission order. Marking
|
|
2738
3202
|
// each as sequential first, then restoring ahead of the existing queue,
|
|
2739
3203
|
// keeps the existing follow-ups' identity and per-message marks intact.
|
|
2740
|
-
|
|
2741
|
-
const
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
3204
|
+
this.#markSteeringAsFollowUpPolicy(rearmed);
|
|
3205
|
+
const deferredAlreadyPresent = this.#deferredSdkFollowUps.length > 0;
|
|
3206
|
+
if (deferredAlreadyPresent) {
|
|
3207
|
+
// Deferred follow-ups are an older cross-store FIFO head. Keep rearmed
|
|
3208
|
+
// steering in that store so it cannot block the head in Agent's queue or
|
|
3209
|
+
// be prepended ahead of it when this terminal reclassifies the queue.
|
|
3210
|
+
for (const message of rearmed) {
|
|
3211
|
+
if (this.#sequentialSteerMessages.has(message)) this.#deferredFollowUpForceOneAtATime.add(message);
|
|
3212
|
+
}
|
|
3213
|
+
const combined = [...this.#deferredSdkFollowUps, ...rearmed];
|
|
3214
|
+
combined.sort((left, right) => {
|
|
3215
|
+
const leftSequence = this.#queuedAdmissionSeq.get(left);
|
|
3216
|
+
const rightSequence = this.#queuedAdmissionSeq.get(right);
|
|
3217
|
+
if (leftSequence === undefined || rightSequence === undefined) return 0;
|
|
3218
|
+
return leftSequence - rightSequence;
|
|
3219
|
+
});
|
|
3220
|
+
this.#deferredSdkFollowUps = combined;
|
|
3221
|
+
this.#followUpMessages = [...this.#followUpMessages, ...rearmedDisplays];
|
|
3222
|
+
} else {
|
|
3223
|
+
this.agent.restoreFollowUp(rearmed);
|
|
3224
|
+
this.#followUpMessages = [...rearmedDisplays, ...this.#followUpMessages];
|
|
2752
3225
|
}
|
|
2753
|
-
flushBatch();
|
|
2754
|
-
this.agent.restoreFollowUp(rearmed);
|
|
2755
|
-
this.#followUpMessages = [...rearmedDisplays, ...this.#followUpMessages];
|
|
2756
3226
|
for (const message of rearmed) {
|
|
2757
3227
|
// External SDK steers keep their promotion hook: it now fires at the
|
|
2758
3228
|
// follow-up promotion boundary with startsOwnRun: true.
|
|
@@ -2769,22 +3239,61 @@ export class AgentSession {
|
|
|
2769
3239
|
// once the queue is drained.
|
|
2770
3240
|
this.#scheduleQueuedDelivery();
|
|
2771
3241
|
}
|
|
3242
|
+
#markSteeringAsFollowUpPolicy(messages: readonly AgentMessage[]): void {
|
|
3243
|
+
let batch: AgentMessage[] = [];
|
|
3244
|
+
const flushBatch = () => {
|
|
3245
|
+
if (batch.length > 1) {
|
|
3246
|
+
this.agent.markFollowUpBatch(batch);
|
|
3247
|
+
for (const message of batch) this.#deferredFollowUpBatches.set(message, batch.slice());
|
|
3248
|
+
}
|
|
3249
|
+
batch = [];
|
|
3250
|
+
};
|
|
3251
|
+
for (const message of messages) {
|
|
3252
|
+
if (this.#sequentialSteerMessages.has(message)) {
|
|
3253
|
+
flushBatch();
|
|
3254
|
+
this.agent.markFollowUpSequential(message);
|
|
3255
|
+
} else if (this.agent.getSteeringMode() === "all") {
|
|
3256
|
+
batch.push(message);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
flushBatch();
|
|
3260
|
+
this.#markTrackedFollowUpSequential(messages);
|
|
3261
|
+
}
|
|
3262
|
+
#markTrackedFollowUpSequential(messages: readonly AgentMessage[]): void {
|
|
3263
|
+
if (messages.length === 0 || this.#trackedQueuedInputs.size === 0) return;
|
|
3264
|
+
const trackedMessages = new Set(messages);
|
|
3265
|
+
for (const state of this.#trackedQueuedInputs.values()) {
|
|
3266
|
+
if (state.queuePolicy === "sequential" && state.message !== undefined && trackedMessages.has(state.message)) {
|
|
3267
|
+
this.agent.markFollowUpSequential(state.message);
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
2772
3271
|
#fireQueuedPromotionHooks(messages: readonly AgentMessage[], promotion?: { startsOwnRun?: boolean }): void {
|
|
3272
|
+
let callbackError: unknown;
|
|
2773
3273
|
for (const message of messages) {
|
|
2774
3274
|
const steerHook = this.#steerPromotionHooks.get(message);
|
|
2775
3275
|
if (steerHook) {
|
|
2776
3276
|
this.#steerPromotionHooks.delete(message);
|
|
2777
3277
|
// A steer is consumed INSIDE the currently running turn: no new
|
|
2778
3278
|
// agent_start follows for it (#4668 review).
|
|
2779
|
-
|
|
3279
|
+
try {
|
|
3280
|
+
steerHook({ startsOwnRun: promotion?.startsOwnRun ?? false });
|
|
3281
|
+
} catch (error) {
|
|
3282
|
+
callbackError ??= error;
|
|
3283
|
+
}
|
|
2780
3284
|
}
|
|
2781
3285
|
const followUpHook = this.#followUpPromotionHooks.get(message);
|
|
2782
3286
|
if (followUpHook) {
|
|
2783
3287
|
this.#followUpPromotionHooks.delete(message);
|
|
2784
3288
|
// A follow-up is promoted to its own run, whose agent_start follows.
|
|
2785
|
-
|
|
3289
|
+
try {
|
|
3290
|
+
followUpHook({ startsOwnRun: promotion?.startsOwnRun ?? true });
|
|
3291
|
+
} catch (error) {
|
|
3292
|
+
callbackError ??= error;
|
|
3293
|
+
}
|
|
2786
3294
|
}
|
|
2787
3295
|
}
|
|
3296
|
+
if (callbackError !== undefined) logger.warn("Queued promotion callback failed", { error: callbackError });
|
|
2788
3297
|
}
|
|
2789
3298
|
#queuedMessagesForSessionTransition(): AgentMessage[] {
|
|
2790
3299
|
return [
|
|
@@ -2792,17 +3301,81 @@ export class AgentSession {
|
|
|
2792
3301
|
...this.agent.snapshotSteering(),
|
|
2793
3302
|
...this.agent.snapshotFollowUp(),
|
|
2794
3303
|
...this.#deferredSdkFollowUps,
|
|
3304
|
+
...this.#pendingNextTurnMessages.map(entry => entry.message),
|
|
2795
3305
|
]),
|
|
2796
3306
|
];
|
|
2797
3307
|
}
|
|
3308
|
+
/** Settle consumed tracked work before a maintenance path disconnects Agent events. */
|
|
3309
|
+
#settleTrackedQueuedInputsBeforeAgentDisconnect(): void {
|
|
3310
|
+
const queued = new Set([
|
|
3311
|
+
...this.agent.snapshotSteering(),
|
|
3312
|
+
...this.agent.snapshotFollowUp(),
|
|
3313
|
+
...this.#deferredSdkFollowUps,
|
|
3314
|
+
]);
|
|
3315
|
+
for (const state of [...this.#trackedQueuedInputs.values()]) {
|
|
3316
|
+
if (
|
|
3317
|
+
state.executionSettled ||
|
|
3318
|
+
this.#trackedQueuedInputsAwaitingOwnRun.has(state) ||
|
|
3319
|
+
(state.message !== undefined && !queued.has(state.message))
|
|
3320
|
+
) {
|
|
3321
|
+
this.#settleTrackedQueuedInputRemoved(state, "removed");
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
#reconcileCompactionQueueDisplay(snapshot: readonly QueuedDisplayEntry[], mode: "steering" | "followUp"): void {
|
|
3326
|
+
const queued = mode === "steering" ? this.agent.snapshotSteering() : this.agent.snapshotFollowUp();
|
|
3327
|
+
const displayEntries = mode === "steering" ? this.#steeringMessages : this.#followUpMessages;
|
|
3328
|
+
const currentByMessage = new Map<AgentMessage, QueuedDisplayEntry>();
|
|
3329
|
+
for (const entry of displayEntries) {
|
|
3330
|
+
if (entry.message !== undefined && !currentByMessage.has(entry.message)) {
|
|
3331
|
+
currentByMessage.set(entry.message, entry);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
const snapshotByMessage = new Map<AgentMessage, QueuedDisplayEntry>();
|
|
3335
|
+
for (const entry of snapshot) {
|
|
3336
|
+
if (entry.message !== undefined && !snapshotByMessage.has(entry.message)) {
|
|
3337
|
+
snapshotByMessage.set(entry.message, entry);
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
const selected = new Set<QueuedDisplayEntry>();
|
|
3341
|
+
const next: QueuedDisplayEntry[] = [];
|
|
3342
|
+
for (const message of queued) {
|
|
3343
|
+
const entry = currentByMessage.get(message) ?? snapshotByMessage.get(message);
|
|
3344
|
+
if (!entry || selected.has(entry)) continue;
|
|
3345
|
+
selected.add(entry);
|
|
3346
|
+
next.push(entry);
|
|
3347
|
+
}
|
|
3348
|
+
const queuedTags = new Set(
|
|
3349
|
+
queued.flatMap(message => {
|
|
3350
|
+
if (message.role !== "custom") return [];
|
|
3351
|
+
const tag = readPendingDisplayTag(message.details);
|
|
3352
|
+
return tag === undefined ? [] : [tag];
|
|
3353
|
+
}),
|
|
3354
|
+
);
|
|
3355
|
+
for (const entry of displayEntries) {
|
|
3356
|
+
if (entry.message !== undefined || selected.has(entry)) continue;
|
|
3357
|
+
if (entry.tag !== undefined && queuedTags.has(entry.tag)) {
|
|
3358
|
+
selected.add(entry);
|
|
3359
|
+
next.push(entry);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
if (mode === "steering") this.#steeringMessages = next;
|
|
3363
|
+
else this.#followUpMessages = next;
|
|
3364
|
+
}
|
|
2798
3365
|
/** Drop queued SDK work when the session identity is replaced. The old
|
|
2799
3366
|
* promotion hooks belong to the predecessor runtime; retaining the message
|
|
2800
3367
|
* would let it execute later under the successor without an owner. */
|
|
2801
|
-
#terminalizeQueuedSdkWorkForSessionTransition(
|
|
2802
|
-
|
|
2803
|
-
this.#
|
|
2804
|
-
|
|
2805
|
-
|
|
3368
|
+
#terminalizeQueuedSdkWorkForSessionTransition(
|
|
3369
|
+
messages: readonly AgentMessage[],
|
|
3370
|
+
predecessorStates: readonly TrackedQueuedInput[] = [...this.#trackedQueuedInputs.values()],
|
|
3371
|
+
): void {
|
|
3372
|
+
if (messages.length > 0) {
|
|
3373
|
+
this.#fireQueuedRemovalHooks(messages);
|
|
3374
|
+
this.#settleDeliveredOwnedRegistrations(messages);
|
|
3375
|
+
for (const message of messages) this.#sdkRunTokensByQueuedMessage.delete(message);
|
|
3376
|
+
this.#deferredSdkFollowUps = this.#deferredSdkFollowUps.filter(message => !messages.includes(message));
|
|
3377
|
+
}
|
|
3378
|
+
for (const state of predecessorStates) this.#settleTrackedQueuedInputRemoved(state, "removed");
|
|
2806
3379
|
}
|
|
2807
3380
|
#resetActiveSdkRunOwnership(): void {
|
|
2808
3381
|
this.#activeSdkRunToken = undefined;
|
|
@@ -2819,6 +3392,7 @@ export class AgentSession {
|
|
|
2819
3392
|
predecessorScope !== undefined &&
|
|
2820
3393
|
this.#skipPostPromptRecoveryWaitByAttemptScope.delete(predecessorScope);
|
|
2821
3394
|
this.#acceptRunHandle(handle);
|
|
3395
|
+
this.#settleTrackedOwnRunPromotions(handle);
|
|
2822
3396
|
if (sdkRunToken !== undefined) {
|
|
2823
3397
|
this.#activeSdkRunToken = sdkRunToken;
|
|
2824
3398
|
this.#sdkRunTokensByAttemptScope.set(handle.scope, sdkRunToken);
|
|
@@ -2865,6 +3439,7 @@ export class AgentSession {
|
|
|
2865
3439
|
// Compaction state
|
|
2866
3440
|
#compactionAbortController: AbortController | undefined = undefined;
|
|
2867
3441
|
#compactionCompletion: Promise<void> | undefined;
|
|
3442
|
+
#compactionQueuedDeliveryArmed = false;
|
|
2868
3443
|
#autoCompactionCompletions = new Set<Promise<AutoCompactionTerminalStatus>>();
|
|
2869
3444
|
#autoCompactionAbortController: AbortController | undefined = undefined;
|
|
2870
3445
|
|
|
@@ -2938,6 +3513,7 @@ export class AgentSession {
|
|
|
2938
3513
|
#retryPromise: Promise<void> | undefined = undefined;
|
|
2939
3514
|
#retryResolve: (() => void) | undefined = undefined;
|
|
2940
3515
|
#defaultFallbackController: FallbackChainController | undefined;
|
|
3516
|
+
#startupRecoveryBindingsRequired = false;
|
|
2941
3517
|
#fallbackTransitionGeneration = 0;
|
|
2942
3518
|
/** Managed escaped-non-ASCII retries issued for the current logical run. Bounded so a deterministic escaper cannot loop forever through un-charged fallback retries. */
|
|
2943
3519
|
#escapedNonAsciiManagedRetries = 0;
|
|
@@ -3119,6 +3695,7 @@ export class AgentSession {
|
|
|
3119
3695
|
#disposeCallerPromise: Promise<void> | undefined;
|
|
3120
3696
|
#disposeCompleted = false;
|
|
3121
3697
|
#disposeTerminalError: unknown;
|
|
3698
|
+
#sessionShutdownReason: "detached_idle" | undefined;
|
|
3122
3699
|
readonly #disposeAbortController = new AbortController();
|
|
3123
3700
|
#disposeAdmissionClosed: Promise<void> | undefined;
|
|
3124
3701
|
#disposePostPromptDrain: Promise<void> | undefined;
|
|
@@ -3143,12 +3720,14 @@ export class AgentSession {
|
|
|
3143
3720
|
#attemptAuthority!: AttemptScopeAuthority;
|
|
3144
3721
|
#attemptRecordStore!: AttemptRecordStore;
|
|
3145
3722
|
#activeLogicalRunId: AttemptRunHandle["logicalRunId"] | undefined;
|
|
3723
|
+
readonly #logicalRunIdByAttemptScope = new WeakMap<AttemptScope, AttemptRunHandle["logicalRunId"]>();
|
|
3146
3724
|
#acceptRunHandle(handle: AttemptRunHandle | undefined): void {
|
|
3147
3725
|
// Integration doubles can accept a run without minting a handle; keep the
|
|
3148
3726
|
// previously recorded run id rather than throwing inside the callback.
|
|
3149
3727
|
if (!handle) return;
|
|
3150
3728
|
this.#activeLogicalRunId = handle.logicalRunId;
|
|
3151
3729
|
this.#activeAttemptScope = handle.scope;
|
|
3730
|
+
this.#logicalRunIdByAttemptScope.set(handle.scope, handle.logicalRunId);
|
|
3152
3731
|
}
|
|
3153
3732
|
|
|
3154
3733
|
#turnIndex = 0;
|
|
@@ -3231,6 +3810,7 @@ export class AgentSession {
|
|
|
3231
3810
|
#discoveryMode: "off" | "mcp-only" | "all" = "off";
|
|
3232
3811
|
#discoverableMCPTools = new Map<string, DiscoverableTool>();
|
|
3233
3812
|
#selectedMCPToolNames = new Set<string>();
|
|
3813
|
+
#exactMcpSuspendedTools = new Map<string, { selected: string[]; active: string[] }>();
|
|
3234
3814
|
// Generic tool discovery (covers built-in + MCP + extension when tools.discoveryMode === "all")
|
|
3235
3815
|
#discoverableToolSearchIndex: DiscoverableToolSearchIndex | null = null;
|
|
3236
3816
|
#selectedDiscoveredToolNames = new Set<string>();
|
|
@@ -3305,6 +3885,9 @@ export class AgentSession {
|
|
|
3305
3885
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
3306
3886
|
#postPromptTasksResolve: (() => void) | undefined = undefined;
|
|
3307
3887
|
#postPromptTasksAbortController = new AbortController();
|
|
3888
|
+
readonly #sessionWorkLease: SessionWorkLease = createSessionWorkLease();
|
|
3889
|
+
#postPromptWorkLeases = new Map<Promise<void>, SessionWorkLeaseHandle>();
|
|
3890
|
+
#deferredAgentEndWorkLeases = new Map<symbol, SessionWorkLeaseHandle>();
|
|
3308
3891
|
#runCancellationDomains = new SessionRunCancellationDomainBridge();
|
|
3309
3892
|
#postPromptLeases = new Map<string, RunResourceProducerLease>();
|
|
3310
3893
|
#runResourceLeaseContext = new AsyncLocalStorage<RunResourceProducerLease>();
|
|
@@ -3463,6 +4046,33 @@ export class AgentSession {
|
|
|
3463
4046
|
}
|
|
3464
4047
|
}
|
|
3465
4048
|
|
|
4049
|
+
/**
|
|
4050
|
+
* Reject public work that would enter a session while its identity or
|
|
4051
|
+
* delivery owner is being changed. The only exception is a tracked
|
|
4052
|
+
* submitUserMessage issued by the committed successor hook; that capability
|
|
4053
|
+
* is checked by the caller and is intentionally narrower than the general
|
|
4054
|
+
* successor-hook context.
|
|
4055
|
+
*/
|
|
4056
|
+
#assertExternalSessionIngress(options?: { allowTrackedSuccessor?: boolean; allowCancelAndSubmit?: boolean }): void {
|
|
4057
|
+
this.#assertSessionAdmissionOpen();
|
|
4058
|
+
if (this.#cancelAndSubmitInProgress && options?.allowCancelAndSubmit !== true) {
|
|
4059
|
+
throw Object.assign(new AgentBusyError("Cannot submit work while cancel-and-submit is in progress."), {
|
|
4060
|
+
code: "busy",
|
|
4061
|
+
});
|
|
4062
|
+
}
|
|
4063
|
+
if (
|
|
4064
|
+
(this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) &&
|
|
4065
|
+
!(options?.allowTrackedSuccessor && this.#hasCommittedSuccessorAdmission())
|
|
4066
|
+
) {
|
|
4067
|
+
throw Object.assign(
|
|
4068
|
+
new AgentBusyError(
|
|
4069
|
+
`Cannot submit work while ${this.#sessionTransitionKind ?? "a handoff"} is in progress.`,
|
|
4070
|
+
),
|
|
4071
|
+
{ code: "busy" },
|
|
4072
|
+
);
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
|
|
3466
4076
|
/**
|
|
3467
4077
|
* Reject a turn start while a handoff transition owns the session. Handoff never
|
|
3468
4078
|
* routes its own generation/injection through these turn-start chokepoints, and
|
|
@@ -3480,14 +4090,17 @@ export class AgentSession {
|
|
|
3480
4090
|
|
|
3481
4091
|
/**
|
|
3482
4092
|
* Single, synchronously-acquired mutex for session-identity transitions
|
|
3483
|
-
* (handoff, compact, new/switch/branch/clear, fork, tree navigation). Acquired
|
|
4093
|
+
* (handoff, manual/automatic compact, new/switch/branch/clear, fork, tree navigation). Acquired
|
|
3484
4094
|
* BEFORE any await at each transition's entry and released in its finally, so
|
|
3485
4095
|
* exclusion is symmetric regardless of which transition starts first — an
|
|
3486
4096
|
* operation that began earlier and yielded still owns the lease when a peer
|
|
3487
|
-
* tries to start. Auto-handoff acquires it via handoff()
|
|
3488
|
-
*
|
|
4097
|
+
* tries to start. Auto-handoff acquires it via handoff() while automatic
|
|
4098
|
+
* context-full maintenance acquires it in its own flow, so neither path
|
|
4099
|
+
* exposes an unowned history rewrite.
|
|
3489
4100
|
*/
|
|
3490
4101
|
#sessionTransitionKind: string | undefined;
|
|
4102
|
+
#queuedDeliveryPendingWhileTransition = false;
|
|
4103
|
+
#deferredAutoContinueDuringTransition: (() => void) | undefined;
|
|
3491
4104
|
#coordinatorPersistGeneration = 0;
|
|
3492
4105
|
#coordinatorRescopeBarrier: Promise<void> | undefined;
|
|
3493
4106
|
#releaseCoordinatorRescopeBarrier: (() => void) | undefined;
|
|
@@ -3550,18 +4163,79 @@ export class AgentSession {
|
|
|
3550
4163
|
}
|
|
3551
4164
|
|
|
3552
4165
|
#beginSessionTransition(kind: string): void {
|
|
4166
|
+
if (this.#cancelAndSubmitInProgress) {
|
|
4167
|
+
throw Object.assign(new AgentBusyError(`Cannot start ${kind} during cancel-and-submit.`), { code: "busy" });
|
|
4168
|
+
}
|
|
3553
4169
|
if (this.#sessionTransitionKind !== undefined) {
|
|
3554
4170
|
throw Object.assign(
|
|
3555
|
-
new
|
|
4171
|
+
new AgentBusyError(
|
|
4172
|
+
`Cannot start ${kind} while a ${this.#sessionTransitionKind} transition is in progress.`,
|
|
4173
|
+
),
|
|
3556
4174
|
{ code: "busy" },
|
|
3557
4175
|
);
|
|
3558
4176
|
}
|
|
3559
4177
|
this.#sessionTransitionKind = kind;
|
|
3560
4178
|
this.#coordinatorPersistGeneration += 1;
|
|
4179
|
+
this.#wakeFollowUpReservationTransitionWaiters();
|
|
4180
|
+
}
|
|
4181
|
+
|
|
4182
|
+
async #settleActivePromptForHistoryTransition(): Promise<void> {
|
|
4183
|
+
if (this.#livePromptsInFlight() > 0 || this.agent.state.isStreaming) {
|
|
4184
|
+
await this.abort({ cause: "session_switch" });
|
|
4185
|
+
}
|
|
4186
|
+
const activeAdmission = this.#activeSessionAdmission;
|
|
4187
|
+
if (activeAdmission?.kind === "prompt") {
|
|
4188
|
+
if (this.#livePromptsInFlight() === 0 && !this.agent.state.isStreaming)
|
|
4189
|
+
this.cancelPendingPreflightForTerminalAbort();
|
|
4190
|
+
await activeAdmission.settled.promise;
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
#wakeFollowUpReservationTransitionWaiters(): void {
|
|
4195
|
+
const waiters = [...this.#followUpReservationTransitionWaiters];
|
|
4196
|
+
this.#followUpReservationTransitionWaiters.clear();
|
|
4197
|
+
for (const waiter of waiters) waiter();
|
|
3561
4198
|
}
|
|
3562
4199
|
|
|
3563
4200
|
#endSessionTransition(): void {
|
|
3564
4201
|
this.#sessionTransitionKind = undefined;
|
|
4202
|
+
const deferredAutoContinue = this.#deferredAutoContinueDuringTransition;
|
|
4203
|
+
this.#deferredAutoContinueDuringTransition = undefined;
|
|
4204
|
+
if (!this.#isDisposed && !this.#sessionAdmissionClosing) deferredAutoContinue?.();
|
|
4205
|
+
if (this.#queuedDeliveryPendingWhileTransition) {
|
|
4206
|
+
this.#queuedDeliveryPendingWhileTransition = false;
|
|
4207
|
+
if (!this.#isDisposed && !this.#sessionAdmissionClosing && !this.#cancelAndSubmitInProgress) {
|
|
4208
|
+
this.#scheduleQueuedDelivery();
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
|
|
4213
|
+
#runCommittedSuccessorHook<T>(body: () => Promise<T>): Promise<T> {
|
|
4214
|
+
const admission: SuccessorSessionAdmission = {
|
|
4215
|
+
generation: this.#coordinatorPersistGeneration,
|
|
4216
|
+
sessionId: this.sessionId,
|
|
4217
|
+
capability: Symbol("committed-successor-session-hook"),
|
|
4218
|
+
active: true,
|
|
4219
|
+
};
|
|
4220
|
+
return this.#successorSessionAdmissionContext.run(admission, async () => {
|
|
4221
|
+
try {
|
|
4222
|
+
return await body();
|
|
4223
|
+
} finally {
|
|
4224
|
+
admission.active = false;
|
|
4225
|
+
}
|
|
4226
|
+
});
|
|
4227
|
+
}
|
|
4228
|
+
|
|
4229
|
+
#hasCommittedSuccessorAdmission(): boolean {
|
|
4230
|
+
const admission = this.#successorSessionAdmissionContext.getStore();
|
|
4231
|
+
return (
|
|
4232
|
+
admission?.active === true &&
|
|
4233
|
+
admission.generation === this.#coordinatorPersistGeneration &&
|
|
4234
|
+
admission.sessionId === this.sessionId
|
|
4235
|
+
);
|
|
4236
|
+
}
|
|
4237
|
+
#hasCommittedSuccessorTrackedAdmission(options?: SendUserMessageDispatchOptions): boolean {
|
|
4238
|
+
return options?.trackSubmission === true && this.#hasCommittedSuccessorAdmission();
|
|
3565
4239
|
}
|
|
3566
4240
|
|
|
3567
4241
|
#activateNextSessionAdmission(): void {
|
|
@@ -3722,6 +4396,12 @@ export class AgentSession {
|
|
|
3722
4396
|
code: "busy",
|
|
3723
4397
|
});
|
|
3724
4398
|
}
|
|
4399
|
+
if (kind === "prompt" && this.#sessionTransitionKind !== undefined) {
|
|
4400
|
+
throw Object.assign(
|
|
4401
|
+
new AgentBusyError(`Cannot start a turn while ${this.#sessionTransitionKind} is in progress.`),
|
|
4402
|
+
{ code: "busy" },
|
|
4403
|
+
);
|
|
4404
|
+
}
|
|
3725
4405
|
|
|
3726
4406
|
const entry: SessionAdmissionEntry = {
|
|
3727
4407
|
kind,
|
|
@@ -3765,6 +4445,12 @@ export class AgentSession {
|
|
|
3765
4445
|
code: "busy",
|
|
3766
4446
|
});
|
|
3767
4447
|
}
|
|
4448
|
+
if (kind === "prompt" && this.#sessionTransitionKind !== undefined) {
|
|
4449
|
+
throw Object.assign(
|
|
4450
|
+
new AgentBusyError(`Cannot start a turn while ${this.#sessionTransitionKind} is in progress.`),
|
|
4451
|
+
{ code: "busy" },
|
|
4452
|
+
);
|
|
4453
|
+
}
|
|
3768
4454
|
|
|
3769
4455
|
const release = () => {
|
|
3770
4456
|
releaseEntry();
|
|
@@ -3932,6 +4618,7 @@ export class AgentSession {
|
|
|
3932
4618
|
if (!pending) return undefined;
|
|
3933
4619
|
const hold = Symbol("deferred-agent-end-continuation");
|
|
3934
4620
|
this.#pendingAgentEndContinuationHolds.set(hold, pending);
|
|
4621
|
+
this.#deferredAgentEndWorkLeases.set(hold, this.#sessionWorkLease.acquire());
|
|
3935
4622
|
return hold;
|
|
3936
4623
|
}
|
|
3937
4624
|
|
|
@@ -3943,6 +4630,7 @@ export class AgentSession {
|
|
|
3943
4630
|
this.#pendingAgentEndEmit = pending;
|
|
3944
4631
|
const hold = Symbol("deferred-agent-end-continuation");
|
|
3945
4632
|
this.#pendingAgentEndContinuationHolds.set(hold, pending);
|
|
4633
|
+
this.#deferredAgentEndWorkLeases.set(hold, this.#sessionWorkLease.acquire());
|
|
3946
4634
|
return hold;
|
|
3947
4635
|
}
|
|
3948
4636
|
|
|
@@ -3950,10 +4638,16 @@ export class AgentSession {
|
|
|
3950
4638
|
if (!hold) return undefined;
|
|
3951
4639
|
const pending = this.#pendingAgentEndContinuationHolds.get(hold);
|
|
3952
4640
|
this.#pendingAgentEndContinuationHolds.delete(hold);
|
|
4641
|
+
this.#deferredAgentEndWorkLeases.get(hold)?.release();
|
|
4642
|
+
this.#deferredAgentEndWorkLeases.delete(hold);
|
|
3953
4643
|
if (pending && this.#pendingAgentEndEmit === pending) {
|
|
3954
4644
|
this.#pendingAgentEndEmit = undefined;
|
|
3955
4645
|
for (const [candidate, candidatePending] of this.#pendingAgentEndContinuationHolds) {
|
|
3956
|
-
if (candidatePending === pending)
|
|
4646
|
+
if (candidatePending === pending) {
|
|
4647
|
+
this.#pendingAgentEndContinuationHolds.delete(candidate);
|
|
4648
|
+
this.#deferredAgentEndWorkLeases.get(candidate)?.release();
|
|
4649
|
+
this.#deferredAgentEndWorkLeases.delete(candidate);
|
|
4650
|
+
}
|
|
3957
4651
|
}
|
|
3958
4652
|
}
|
|
3959
4653
|
this.#resolveSessionSettlement();
|
|
@@ -3980,6 +4674,8 @@ export class AgentSession {
|
|
|
3980
4674
|
if (!hold) return;
|
|
3981
4675
|
const pending = this.#pendingAgentEndContinuationHolds.get(hold);
|
|
3982
4676
|
this.#pendingAgentEndContinuationHolds.delete(hold);
|
|
4677
|
+
this.#deferredAgentEndWorkLeases.get(hold)?.release();
|
|
4678
|
+
this.#deferredAgentEndWorkLeases.delete(hold);
|
|
3983
4679
|
this.#restoreDeferredAgentEndAfterContinuationFailure(pending);
|
|
3984
4680
|
}
|
|
3985
4681
|
|
|
@@ -3990,6 +4686,8 @@ export class AgentSession {
|
|
|
3990
4686
|
break;
|
|
3991
4687
|
}
|
|
3992
4688
|
this.#pendingAgentEndContinuationHolds.clear();
|
|
4689
|
+
for (const lease of this.#deferredAgentEndWorkLeases.values()) lease.release();
|
|
4690
|
+
this.#deferredAgentEndWorkLeases.clear();
|
|
3993
4691
|
this.#restoreDeferredAgentEndAfterContinuationFailure(pending);
|
|
3994
4692
|
}
|
|
3995
4693
|
|
|
@@ -4023,9 +4721,11 @@ export class AgentSession {
|
|
|
4023
4721
|
fenceGeneration,
|
|
4024
4722
|
(this.#selectionFenceDeferredContinuations.get(fenceGeneration) ?? 0) + 1,
|
|
4025
4723
|
);
|
|
4724
|
+
const workLease = this.#sessionWorkLease.acquire();
|
|
4026
4725
|
void deferred
|
|
4027
4726
|
.catch(() => {})
|
|
4028
4727
|
.finally(() => {
|
|
4728
|
+
workLease.release();
|
|
4029
4729
|
this.#endSelectionFenceDeferralTracking(fenceGeneration);
|
|
4030
4730
|
});
|
|
4031
4731
|
}
|
|
@@ -4087,12 +4787,46 @@ export class AgentSession {
|
|
|
4087
4787
|
ownEpoch: number,
|
|
4088
4788
|
requesterSignal?: AbortSignal,
|
|
4089
4789
|
admissionSignal?: AbortSignal,
|
|
4790
|
+
transitionGeneration = this.#coordinatorPersistGeneration,
|
|
4090
4791
|
): Promise<void> {
|
|
4091
4792
|
while ([...this.#activeFollowUpReservationEpochs].some(epoch => epoch < ownEpoch)) {
|
|
4092
4793
|
if (requesterSignal?.aborted || admissionSignal?.aborted) throw promptPreflightCancelledError();
|
|
4794
|
+
if (this.#coordinatorPersistGeneration !== transitionGeneration) {
|
|
4795
|
+
throw Object.assign(
|
|
4796
|
+
new AgentBusyError("Cannot submit queued input while a session state transition is in progress."),
|
|
4797
|
+
{
|
|
4798
|
+
code: "busy",
|
|
4799
|
+
},
|
|
4800
|
+
);
|
|
4801
|
+
}
|
|
4093
4802
|
const drained = Promise.withResolvers<void>();
|
|
4094
4803
|
this.#followUpReservationDrainWaiters.add(drained.resolve);
|
|
4095
|
-
|
|
4804
|
+
const abort = Promise.withResolvers<void>();
|
|
4805
|
+
const transition = Promise.withResolvers<void>();
|
|
4806
|
+
const onAbort = () => abort.resolve();
|
|
4807
|
+
requesterSignal?.addEventListener("abort", onAbort, { once: true });
|
|
4808
|
+
admissionSignal?.addEventListener("abort", onAbort, { once: true });
|
|
4809
|
+
this.#followUpReservationTransitionWaiters.add(transition.resolve);
|
|
4810
|
+
// The transition/abort signal can change between the checks above and
|
|
4811
|
+
// registering these waiters. Re-check after registration so a wake that
|
|
4812
|
+
// happened in that gap cannot leave this reservation parked forever.
|
|
4813
|
+
if (requesterSignal?.aborted || admissionSignal?.aborted) abort.resolve();
|
|
4814
|
+
if (this.#coordinatorPersistGeneration !== transitionGeneration) transition.resolve();
|
|
4815
|
+
try {
|
|
4816
|
+
await Promise.race([drained.promise, abort.promise, transition.promise]);
|
|
4817
|
+
if (requesterSignal?.aborted || admissionSignal?.aborted) throw promptPreflightCancelledError();
|
|
4818
|
+
if (this.#coordinatorPersistGeneration !== transitionGeneration) {
|
|
4819
|
+
throw Object.assign(
|
|
4820
|
+
new AgentBusyError("Cannot submit queued input while a session state transition is in progress."),
|
|
4821
|
+
{ code: "busy" },
|
|
4822
|
+
);
|
|
4823
|
+
}
|
|
4824
|
+
} finally {
|
|
4825
|
+
this.#followUpReservationDrainWaiters.delete(drained.resolve);
|
|
4826
|
+
this.#followUpReservationTransitionWaiters.delete(transition.resolve);
|
|
4827
|
+
requesterSignal?.removeEventListener("abort", onAbort);
|
|
4828
|
+
admissionSignal?.removeEventListener("abort", onAbort);
|
|
4829
|
+
}
|
|
4096
4830
|
}
|
|
4097
4831
|
}
|
|
4098
4832
|
|
|
@@ -4346,6 +5080,7 @@ export class AgentSession {
|
|
|
4346
5080
|
// preserves transaction order; terminal publication is the user-visible
|
|
4347
5081
|
// authority and must not be suppressed by a secondary persistence failure.
|
|
4348
5082
|
const terminalPersistence = this.#queueCoordinatorRuntimeStatePersist(pending, true);
|
|
5083
|
+
this.#settleTrackedQueuedInputTerminal(publicationScope);
|
|
4349
5084
|
this.#emit(pending);
|
|
4350
5085
|
void terminalPersistence.then(
|
|
4351
5086
|
() => {
|
|
@@ -4690,7 +5425,8 @@ export class AgentSession {
|
|
|
4690
5425
|
this.#setGuardedAgentTools(this.agent.state.tools);
|
|
4691
5426
|
this.#bindWorkflowGateEmitter();
|
|
4692
5427
|
this.yieldQueue = new YieldQueue({
|
|
4693
|
-
isStreaming: () =>
|
|
5428
|
+
isStreaming: () =>
|
|
5429
|
+
this.isStreaming || this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive,
|
|
4694
5430
|
injectStreaming: message => {
|
|
4695
5431
|
// Mandated boundary comment (corrected turn semantics): turn-scope
|
|
4696
5432
|
// abort blocks only deliveries whose origin is a continuation of the
|
|
@@ -4719,7 +5455,19 @@ export class AgentSession {
|
|
|
4719
5455
|
// #promptWithMessage (resetRetryReplaySafety), which allocates the
|
|
4720
5456
|
// fresh attempt epoch at turn start. The idle injector, which calls
|
|
4721
5457
|
// agent.prompt directly, allocates right before admission.
|
|
4722
|
-
|
|
5458
|
+
const displayText =
|
|
5459
|
+
message.role === "custom"
|
|
5460
|
+
? this.#getCustomMessageTextContent(message)
|
|
5461
|
+
: message.role === "user"
|
|
5462
|
+
? this.#getUserMessageText(message)
|
|
5463
|
+
: "";
|
|
5464
|
+
void this.#queueFollowUpAfterReservation(message, displayText, {
|
|
5465
|
+
createDisplayEntry: false,
|
|
5466
|
+
trackExternalFollowUp: false,
|
|
5467
|
+
}).catch(error => {
|
|
5468
|
+
this.#settleDeliveredOwnedRegistrations([message]);
|
|
5469
|
+
logger.warn("Owned streaming follow-up was rejected", { error: String(error) });
|
|
5470
|
+
});
|
|
4723
5471
|
},
|
|
4724
5472
|
injectIdle: async (messages, signal) => {
|
|
4725
5473
|
// Mandated boundary comment (corrected turn semantics): same origin
|
|
@@ -5193,8 +5941,13 @@ export class AgentSession {
|
|
|
5193
5941
|
if (next && MCPManager.instance() === undefined) MCPManager.setInstance(next);
|
|
5194
5942
|
}
|
|
5195
5943
|
|
|
5196
|
-
/** Swap named custom
|
|
5197
|
-
async replaceNamedCustomTools(
|
|
5944
|
+
/** Swap named custom tools and their optional mandatory MCP selection. */
|
|
5945
|
+
async replaceNamedCustomTools(
|
|
5946
|
+
previousNames: readonly string[],
|
|
5947
|
+
nextTools: readonly CustomTool[],
|
|
5948
|
+
options?: { mandatoryMCPToolNames?: readonly string[]; activateNewTools?: boolean },
|
|
5949
|
+
): Promise<void> {
|
|
5950
|
+
const previousSelectedMCPToolNames = this.getSelectedMCPToolNames();
|
|
5198
5951
|
const previous = new Set(previousNames);
|
|
5199
5952
|
const previousActive = this.getActiveToolNames();
|
|
5200
5953
|
for (const name of previous) this.#toolRegistry.delete(name);
|
|
@@ -5208,11 +5961,21 @@ export class AgentSession {
|
|
|
5208
5961
|
this.#toolRegistry.set(finalTool.name, finalTool);
|
|
5209
5962
|
added.push(finalTool.name);
|
|
5210
5963
|
}
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5964
|
+
if (options?.mandatoryMCPToolNames) {
|
|
5965
|
+
this.#mandatoryMCPToolNames = new Set(
|
|
5966
|
+
options.mandatoryMCPToolNames.map(name => name.toLowerCase()).filter(name => this.#toolRegistry.has(name)),
|
|
5967
|
+
);
|
|
5968
|
+
}
|
|
5969
|
+
this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
|
|
5970
|
+
await this.#applyActiveToolsByName(
|
|
5971
|
+
[
|
|
5972
|
+
...previousActive.filter(name => !previous.has(name)),
|
|
5973
|
+
...added.filter(
|
|
5974
|
+
name => previousActive.includes(name) || (options?.activateNewTools !== false && !previous.has(name)),
|
|
5975
|
+
),
|
|
5976
|
+
],
|
|
5977
|
+
{ previousSelectedMCPToolNames },
|
|
5978
|
+
);
|
|
5216
5979
|
}
|
|
5217
5980
|
|
|
5218
5981
|
/** Best-effort accessor for the active skill's `current_phase` field from
|
|
@@ -5673,6 +6436,7 @@ export class AgentSession {
|
|
|
5673
6436
|
* Does NOT push to the agent's steering/followUp queue — that happens
|
|
5674
6437
|
* separately inside `sendCustomMessage`. */
|
|
5675
6438
|
enqueueCustomMessageDisplay(text: string, mode: "steer" | "followUp"): string {
|
|
6439
|
+
this.#assertExternalSessionIngress();
|
|
5676
6440
|
const tag = `gjc-cmd-${Date.now()}-${++this.#customDisplayTagCounter}`;
|
|
5677
6441
|
const displayText = text.trim();
|
|
5678
6442
|
if (!displayText) return tag;
|
|
@@ -6011,6 +6775,10 @@ export class AgentSession {
|
|
|
6011
6775
|
}
|
|
6012
6776
|
|
|
6013
6777
|
#trackAgentEvent = (event: AgentEvent): Promise<void> => {
|
|
6778
|
+
const eventScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
|
|
6779
|
+
if ((event.type === "agent_start" || event.type === "turn_start") && eventScope !== undefined) {
|
|
6780
|
+
this.#bindAttemptScopeToActiveRun(eventScope);
|
|
6781
|
+
}
|
|
6014
6782
|
this.#agentEventAdmission.set(event, {
|
|
6015
6783
|
scope: this.#activeAttemptScope,
|
|
6016
6784
|
sdkRunToken: this.#activeSdkRunToken,
|
|
@@ -6369,6 +7137,7 @@ export class AgentSession {
|
|
|
6369
7137
|
// re-enter prompt(), so a successor's running transition serializes after it.
|
|
6370
7138
|
// Do not let a lock-hostile sidecar suppress the terminal event itself.
|
|
6371
7139
|
const terminalPersistence = this.#queueCoordinatorRuntimeStatePersist(event);
|
|
7140
|
+
this.#settleTrackedQueuedInputTerminal(attemptScope);
|
|
6372
7141
|
this.#emit(event);
|
|
6373
7142
|
void terminalPersistence;
|
|
6374
7143
|
await this.#emitExtensionEvent(event);
|
|
@@ -6556,16 +7325,23 @@ export class AgentSession {
|
|
|
6556
7325
|
if (userDisplayDequeueAlreadyHandled) this.#displayDequeueAlreadyHandled = undefined;
|
|
6557
7326
|
if (event.type === "message_start" && event.message.role === "user" && !userDisplayDequeueAlreadyHandled) {
|
|
6558
7327
|
const messageText = userMessageText;
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
7328
|
+
const boundIndex = this.#steeringMessages.findIndex(entry => entry.message === event.message);
|
|
7329
|
+
if (boundIndex !== -1) {
|
|
7330
|
+
this.#steeringMessages.splice(boundIndex, 1);
|
|
7331
|
+
} else {
|
|
7332
|
+
const followUpBoundIndex = this.#followUpMessages.findIndex(entry => entry.message === event.message);
|
|
7333
|
+
if (followUpBoundIndex !== -1) this.#followUpMessages.splice(followUpBoundIndex, 1);
|
|
7334
|
+
else if (messageText) {
|
|
7335
|
+
// Legacy display rows without a bound executable message retain text fallback.
|
|
7336
|
+
const steeringIndex = this.#steeringMessages.findIndex(
|
|
7337
|
+
entry => entry.message === undefined && entry.text === messageText,
|
|
7338
|
+
);
|
|
7339
|
+
if (steeringIndex !== -1) this.#steeringMessages.splice(steeringIndex, 1);
|
|
7340
|
+
else {
|
|
7341
|
+
const followUpIndex = this.#followUpMessages.findIndex(
|
|
7342
|
+
entry => entry.message === undefined && entry.text === messageText,
|
|
7343
|
+
);
|
|
7344
|
+
if (followUpIndex !== -1) this.#followUpMessages.splice(followUpIndex, 1);
|
|
6569
7345
|
}
|
|
6570
7346
|
}
|
|
6571
7347
|
}
|
|
@@ -6701,7 +7477,7 @@ export class AgentSession {
|
|
|
6701
7477
|
text: [
|
|
6702
7478
|
"Session transcript reached the managed per-file limit; this result could not be recorded durably.",
|
|
6703
7479
|
committed,
|
|
6704
|
-
|
|
7480
|
+
`To continue, ${SESSION_LIMIT_RECOVERY_ACTIONS}; re-verify the edited file before relying on it.`,
|
|
6705
7481
|
].join("\n"),
|
|
6706
7482
|
},
|
|
6707
7483
|
];
|
|
@@ -7138,6 +7914,10 @@ export class AgentSession {
|
|
|
7138
7914
|
!(event.stopReason === "maintenance" && isContinuingMidRunMaintenanceOutcome(event.maintenanceOutcome))
|
|
7139
7915
|
) {
|
|
7140
7916
|
this.#releaseDeferredSdkFollowUps();
|
|
7917
|
+
if (this.#compactionQueuedDeliveryArmed) {
|
|
7918
|
+
if (this.agent.hasQueuedMessages()) this.#scheduleQueuedDelivery();
|
|
7919
|
+
else this.#compactionQueuedDeliveryArmed = false;
|
|
7920
|
+
}
|
|
7141
7921
|
}
|
|
7142
7922
|
|
|
7143
7923
|
// Check auto-retry and auto-compaction after agent completes
|
|
@@ -7333,6 +8113,31 @@ export class AgentSession {
|
|
|
7333
8113
|
this.#ttsrResumePromise = undefined;
|
|
7334
8114
|
}
|
|
7335
8115
|
|
|
8116
|
+
#dropQueuedTtsrFollowUps(): void {
|
|
8117
|
+
const deferredTtsr = new Set(
|
|
8118
|
+
this.#deferredSdkFollowUps.filter(
|
|
8119
|
+
message => message.role === "custom" && message.customType === "ttsr-injection",
|
|
8120
|
+
),
|
|
8121
|
+
);
|
|
8122
|
+
const queuedTtsr = [
|
|
8123
|
+
...this.agent
|
|
8124
|
+
.snapshotFollowUp()
|
|
8125
|
+
.filter(message => message.role === "custom" && message.customType === "ttsr-injection"),
|
|
8126
|
+
...deferredTtsr,
|
|
8127
|
+
];
|
|
8128
|
+
if (queuedTtsr.length === 0) return;
|
|
8129
|
+
this.agent.removeQueuedMessages(
|
|
8130
|
+
message => message.role === "custom" && message.customType === "ttsr-injection",
|
|
8131
|
+
"followUp",
|
|
8132
|
+
);
|
|
8133
|
+
this.#deferredSdkFollowUps = this.#deferredSdkFollowUps.filter(message => !deferredTtsr.has(message));
|
|
8134
|
+
for (const message of deferredTtsr) this.#deferredFollowUpForceOneAtATime.delete(message);
|
|
8135
|
+
this.#followUpMessages = this.#followUpMessages.filter(
|
|
8136
|
+
entry => entry.message === undefined || !queuedTtsr.includes(entry.message),
|
|
8137
|
+
);
|
|
8138
|
+
this.#resolveTtsrResume();
|
|
8139
|
+
}
|
|
8140
|
+
|
|
7336
8141
|
#ensurePostPromptTasksPromise(): void {
|
|
7337
8142
|
if (this.#postPromptTasksPromise) return;
|
|
7338
8143
|
const { promise, resolve } = Promise.withResolvers<void>();
|
|
@@ -7355,6 +8160,7 @@ export class AgentSession {
|
|
|
7355
8160
|
excludeFromRecovery = false,
|
|
7356
8161
|
): void {
|
|
7357
8162
|
this.#postPromptTasks.add(task);
|
|
8163
|
+
this.#postPromptWorkLeases.set(task, this.#sessionWorkLease.acquire());
|
|
7358
8164
|
this.#postPromptTaskSelectionFenceGenerations.set(task, selectionFenceGeneration);
|
|
7359
8165
|
if (excludeFromRecovery) this.#postPromptTaskRecoveryExcluded.add(task);
|
|
7360
8166
|
this.#ensurePostPromptTasksPromise();
|
|
@@ -7365,6 +8171,8 @@ export class AgentSession {
|
|
|
7365
8171
|
void task
|
|
7366
8172
|
.catch(() => {})
|
|
7367
8173
|
.finally(() => {
|
|
8174
|
+
this.#postPromptWorkLeases.get(task)?.release();
|
|
8175
|
+
this.#postPromptWorkLeases.delete(task);
|
|
7368
8176
|
this.#postPromptTasks.delete(task);
|
|
7369
8177
|
this.#postPromptTaskSelectionFenceGenerations.delete(task);
|
|
7370
8178
|
this.#postPromptTaskRecoveryExcluded.delete(task);
|
|
@@ -7564,6 +8372,12 @@ export class AgentSession {
|
|
|
7564
8372
|
skip("queue_drained");
|
|
7565
8373
|
return false;
|
|
7566
8374
|
}
|
|
8375
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) {
|
|
8376
|
+
if (this.agent.hasQueuedMessages() || this.#deferredSdkFollowUps.length > 0)
|
|
8377
|
+
this.#queuedDeliveryPendingWhileTransition = true;
|
|
8378
|
+
skip("handoff_in_progress");
|
|
8379
|
+
return false;
|
|
8380
|
+
}
|
|
7567
8381
|
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
7568
8382
|
skip("queue_drained");
|
|
7569
8383
|
return false;
|
|
@@ -7703,8 +8517,10 @@ export class AgentSession {
|
|
|
7703
8517
|
error: error.message,
|
|
7704
8518
|
});
|
|
7705
8519
|
}
|
|
7706
|
-
if (!predecessorAccepted)
|
|
8520
|
+
if (!predecessorAccepted) {
|
|
8521
|
+
this.#settleTrackedOwnRunPromotionFailures();
|
|
7707
8522
|
this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
|
|
8523
|
+
}
|
|
7708
8524
|
throw error;
|
|
7709
8525
|
}
|
|
7710
8526
|
} catch (error) {
|
|
@@ -7823,6 +8639,18 @@ export class AgentSession {
|
|
|
7823
8639
|
deferredPredecessorAgentEnd?: AgentSessionEvent,
|
|
7824
8640
|
sdkRunToken?: string,
|
|
7825
8641
|
): void {
|
|
8642
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) {
|
|
8643
|
+
this.#deferredAutoContinueDuringTransition = () =>
|
|
8644
|
+
this.#scheduleAutoContinuePrompt(
|
|
8645
|
+
generation,
|
|
8646
|
+
requireUnfinishedWork,
|
|
8647
|
+
resourceRunId,
|
|
8648
|
+
deferredSelectionFenceGeneration,
|
|
8649
|
+
deferredPredecessorAgentEnd,
|
|
8650
|
+
sdkRunToken,
|
|
8651
|
+
);
|
|
8652
|
+
return;
|
|
8653
|
+
}
|
|
7826
8654
|
const scheduledGeneration = generation;
|
|
7827
8655
|
const scheduledSdkRunToken =
|
|
7828
8656
|
sdkRunToken ??
|
|
@@ -7843,6 +8671,10 @@ export class AgentSession {
|
|
|
7843
8671
|
);
|
|
7844
8672
|
return false;
|
|
7845
8673
|
}
|
|
8674
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) {
|
|
8675
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "session_transition");
|
|
8676
|
+
return false;
|
|
8677
|
+
}
|
|
7846
8678
|
// A same-turn auto-continue of a terminally aborted turn is denied
|
|
7847
8679
|
// (corrected turn semantics); owned-completion deliveries are not
|
|
7848
8680
|
// affected — they flow through followUp as fresh turns.
|
|
@@ -7984,6 +8816,8 @@ export class AgentSession {
|
|
|
7984
8816
|
#abandonPostPromptTasks(): void {
|
|
7985
8817
|
this.#postPromptTasksAbortController.abort();
|
|
7986
8818
|
this.#postPromptTasksAbortController = new AbortController();
|
|
8819
|
+
for (const lease of this.#postPromptWorkLeases.values()) lease.release();
|
|
8820
|
+
this.#postPromptWorkLeases.clear();
|
|
7987
8821
|
this.#postPromptTasks.clear();
|
|
7988
8822
|
this.#postPromptTaskSelectionFenceGenerations.clear();
|
|
7989
8823
|
this.#postPromptTaskRecoveryExcluded.clear();
|
|
@@ -8186,7 +9020,9 @@ export class AgentSession {
|
|
|
8186
9020
|
if (!injection) {
|
|
8187
9021
|
return;
|
|
8188
9022
|
}
|
|
8189
|
-
this
|
|
9023
|
+
const ttsrAbortEpoch = this.#abortEpoch;
|
|
9024
|
+
const ttsrAbortSignal = this.#promptPreflightAbortController.signal;
|
|
9025
|
+
const message: CustomMessage = {
|
|
8190
9026
|
role: "custom",
|
|
8191
9027
|
customType: "ttsr-injection",
|
|
8192
9028
|
content: injection.content,
|
|
@@ -8194,27 +9030,46 @@ export class AgentSession {
|
|
|
8194
9030
|
details: { rules: injection.rules.map(rule => rule.name) },
|
|
8195
9031
|
attribution: "agent",
|
|
8196
9032
|
timestamp: Date.now(),
|
|
8197
|
-
}
|
|
9033
|
+
};
|
|
8198
9034
|
this.#ensureTtsrResumePromise();
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
this.#resolveTtsrResume();
|
|
9035
|
+
void this.#queueFollowUpAfterReservation(
|
|
9036
|
+
message,
|
|
9037
|
+
this.#getCustomMessageTextContent(message),
|
|
9038
|
+
{
|
|
9039
|
+
createDisplayEntry: false,
|
|
9040
|
+
trackExternalFollowUp: false,
|
|
8206
9041
|
},
|
|
8207
|
-
|
|
8208
|
-
|
|
9042
|
+
ttsrAbortSignal,
|
|
9043
|
+
)
|
|
9044
|
+
.then(() => {
|
|
9045
|
+
if (this.#abortEpoch !== ttsrAbortEpoch || ttsrAbortSignal.aborted) {
|
|
8209
9046
|
this.#resolveTtsrResume();
|
|
8210
|
-
return
|
|
9047
|
+
return;
|
|
8211
9048
|
}
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
9049
|
+
// Mark as injected after this custom message is delivered and persisted (handled in message_end).
|
|
9050
|
+
// followUp() only enqueues; resume on the next tick once streaming settles.
|
|
9051
|
+
this.#scheduleAgentContinue({
|
|
9052
|
+
delayMs: 1,
|
|
9053
|
+
generation: this.#promptGeneration,
|
|
9054
|
+
onSkip: () => {
|
|
9055
|
+
this.#resolveTtsrResume();
|
|
9056
|
+
},
|
|
9057
|
+
shouldContinue: () => {
|
|
9058
|
+
if (this.agent.state.isStreaming || !this.agent.hasQueuedMessages()) {
|
|
9059
|
+
this.#resolveTtsrResume();
|
|
9060
|
+
return false;
|
|
9061
|
+
}
|
|
9062
|
+
return true;
|
|
9063
|
+
},
|
|
9064
|
+
onError: () => {
|
|
9065
|
+
this.#resolveTtsrResume();
|
|
9066
|
+
},
|
|
9067
|
+
});
|
|
9068
|
+
})
|
|
9069
|
+
.catch(error => {
|
|
8215
9070
|
this.#resolveTtsrResume();
|
|
8216
|
-
|
|
8217
|
-
|
|
9071
|
+
logger.warn("TTSR follow-up injection was rejected", { error: String(error) });
|
|
9072
|
+
});
|
|
8218
9073
|
}
|
|
8219
9074
|
|
|
8220
9075
|
/** Build TTSR match context for tool call argument deltas. */
|
|
@@ -9036,6 +9891,16 @@ export class AgentSession {
|
|
|
9036
9891
|
}
|
|
9037
9892
|
}
|
|
9038
9893
|
|
|
9894
|
+
#bindAttemptScopeToActiveRun(scope: AttemptScope): void {
|
|
9895
|
+
this.#activeAttemptScope = scope;
|
|
9896
|
+
if (this.#activeLogicalRunId !== undefined) {
|
|
9897
|
+
this.#logicalRunIdByAttemptScope.set(scope, this.#activeLogicalRunId);
|
|
9898
|
+
}
|
|
9899
|
+
if (this.#activeSdkRunToken !== undefined) {
|
|
9900
|
+
this.#sdkRunTokensByAttemptScope.set(scope, this.#activeSdkRunToken);
|
|
9901
|
+
}
|
|
9902
|
+
}
|
|
9903
|
+
|
|
9039
9904
|
/**
|
|
9040
9905
|
* Subscribe to agent events.
|
|
9041
9906
|
* Session persistence is handled internally (saves messages on message_end).
|
|
@@ -9063,6 +9928,7 @@ export class AgentSession {
|
|
|
9063
9928
|
*/
|
|
9064
9929
|
#disconnectFromAgent(): void {
|
|
9065
9930
|
this.#abortActiveMidRunBarriers();
|
|
9931
|
+
this.#compactionQueuedDeliveryArmed = false;
|
|
9066
9932
|
if (this.#unsubscribeAgent) {
|
|
9067
9933
|
this.#unsubscribeAgent();
|
|
9068
9934
|
this.#unsubscribeAgent = undefined;
|
|
@@ -9204,21 +10070,27 @@ export class AgentSession {
|
|
|
9204
10070
|
* Remove all listeners, flush pending writes, and disconnect from agent.
|
|
9205
10071
|
* Call this when completely done with the session.
|
|
9206
10072
|
*/
|
|
9207
|
-
dispose(): Promise<void> {
|
|
10073
|
+
dispose(options: { sessionShutdownReason?: "detached_idle" } = {}): Promise<void> {
|
|
9208
10074
|
this.#evalExecutionDisposing = true;
|
|
9209
10075
|
if (this.#disposeCompleted) return Promise.resolve();
|
|
9210
10076
|
if (this.#disposeTerminalError !== undefined) return Promise.reject(this.#disposeTerminalError);
|
|
9211
10077
|
if (this.#disposeCallerPromise) return this.#disposeCallerPromise;
|
|
9212
10078
|
if (!this.#disposeRunPromise) {
|
|
10079
|
+
this.#sessionShutdownReason = options.sessionShutdownReason;
|
|
9213
10080
|
this.#disposeDeadline = Date.now() + this.#disposeTimeoutMs;
|
|
9214
10081
|
this.#disposeDeadlineExpired = Promise.withResolvers<void>();
|
|
9215
10082
|
this.#disposeDeadlineTimer = setTimeout(() => this.#disposeDeadlineExpired?.resolve(), this.#disposeTimeoutMs);
|
|
9216
10083
|
this.#disposeDeadlineTimer.unref?.();
|
|
9217
10084
|
this.#abortAdmissionEpoch++;
|
|
9218
10085
|
this.#isDisposed = true;
|
|
10086
|
+
// Dispose disconnects the Agent event bridge before bounded abort cleanup;
|
|
10087
|
+
// settle every accepted SDK handle now so consumed submissions cannot wait
|
|
10088
|
+
// forever for an agent_end that teardown deliberately suppresses.
|
|
10089
|
+
this.#terminalizeQueuedSdkWorkForSessionTransition(this.#queuedMessagesForSessionTransition());
|
|
9219
10090
|
// Invalidate every coordinator event admitted before disposal. Handlers may
|
|
9220
10091
|
// still unwind, but their captured generation can no longer enqueue a write.
|
|
9221
10092
|
this.#coordinatorPersistGeneration += 1;
|
|
10093
|
+
this.#wakeFollowUpReservationTransitionWaiters();
|
|
9222
10094
|
this.#disposeAbortController.abort();
|
|
9223
10095
|
// Disposal owns a bounded Agent abort below. Waiting for the active prompt's
|
|
9224
10096
|
// admission here would put that unbounded prompt ahead of the abort budget.
|
|
@@ -9383,7 +10255,10 @@ export class AgentSession {
|
|
|
9383
10255
|
if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
|
|
9384
10256
|
await awaitDisposeStep(
|
|
9385
10257
|
"session shutdown handlers",
|
|
9386
|
-
this.#extensionRunner.emit({
|
|
10258
|
+
this.#extensionRunner.emit({
|
|
10259
|
+
type: "session_shutdown",
|
|
10260
|
+
...(this.#sessionShutdownReason === undefined ? {} : { reason: this.#sessionShutdownReason }),
|
|
10261
|
+
}),
|
|
9387
10262
|
);
|
|
9388
10263
|
}
|
|
9389
10264
|
} catch (error) {
|
|
@@ -9721,6 +10596,24 @@ export class AgentSession {
|
|
|
9721
10596
|
|
|
9722
10597
|
/** Test seam: await all currently admitted coordinator sidecar writes. */
|
|
9723
10598
|
async awaitCoordinatorRuntimeStatePersistenceForTests(): Promise<void> {
|
|
10599
|
+
// A terminal abort can schedule a preserved follow-up as a fresh turn after
|
|
10600
|
+
// the aborted run settles. Join that continuation before observing the
|
|
10601
|
+
// sidecar queue; otherwise teardown can race a rearmed run and wait on its
|
|
10602
|
+
// persistence while the test's manager is already being disposed.
|
|
10603
|
+
const idleStatus = await Promise.race([
|
|
10604
|
+
this.waitForIdle().then(
|
|
10605
|
+
() => ({ status: "settled" as const }),
|
|
10606
|
+
error => ({ status: "error" as const, error }),
|
|
10607
|
+
),
|
|
10608
|
+
Bun.sleep(COORDINATOR_PERSISTENCE_TEST_IDLE_TIMEOUT_MS).then(() => ({ status: "timed_out" as const })),
|
|
10609
|
+
]);
|
|
10610
|
+
if (idleStatus.status === "error") throw idleStatus.error;
|
|
10611
|
+
if (idleStatus.status === "timed_out") {
|
|
10612
|
+
logger.warn("Coordinator persistence test seam timed out waiting for session idle", {
|
|
10613
|
+
sessionId: this.sessionId,
|
|
10614
|
+
timeoutMs: COORDINATOR_PERSISTENCE_TEST_IDLE_TIMEOUT_MS,
|
|
10615
|
+
});
|
|
10616
|
+
}
|
|
9724
10617
|
await this.#coordinatorPersistQueue;
|
|
9725
10618
|
await this.#drainUnbarrieredCoordinatorPersists();
|
|
9726
10619
|
}
|
|
@@ -10068,7 +10961,10 @@ export class AgentSession {
|
|
|
10068
10961
|
reason: FoldReason = "chord",
|
|
10069
10962
|
adapter?: FoldAdapter,
|
|
10070
10963
|
): Promise<ForegroundFoldOutcome> {
|
|
10071
|
-
|
|
10964
|
+
// An explicit adapter (the steer watcher naming its own wait) is validated by
|
|
10965
|
+
// the coordinator's registration-bound identity check; only the implicit
|
|
10966
|
+
// chord / SDK-control path needs a targetable participant to exist.
|
|
10967
|
+
if (!adapter && !this.#foldCoordinator.hasFoldableParticipant()) {
|
|
10072
10968
|
return this.#hasRunningFoldedJob() ? { status: "already_backgrounded" } : { status: "no_active_bash" };
|
|
10073
10969
|
}
|
|
10074
10970
|
try {
|
|
@@ -10106,6 +11002,240 @@ export class AgentSession {
|
|
|
10106
11002
|
return Array.from(this.#toolRegistry.keys());
|
|
10107
11003
|
}
|
|
10108
11004
|
|
|
11005
|
+
/**
|
|
11006
|
+
* Exact MCP controls: stock-workflow transactional successor revocation.
|
|
11007
|
+
*
|
|
11008
|
+
* Serialize an exact-config MCP control against both session mutexes.
|
|
11009
|
+
*
|
|
11010
|
+
* Admission serializes it with prompts and selection changes and fails closed
|
|
11011
|
+
* once dispose has closed admission; the transition lease excludes it from
|
|
11012
|
+
* new/switch/branch/fork/handoff in both directions. Neither side waits for
|
|
11013
|
+
* the other: whichever arrives second is rejected with a busy error, so a
|
|
11014
|
+
* control never observes or mutates state inside a transition window.
|
|
11015
|
+
*/
|
|
11016
|
+
async #withExactMcpControlAdmission<T>(body: () => Promise<T>): Promise<T> {
|
|
11017
|
+
return this.#withSessionAdmission("mcp-control", async () => {
|
|
11018
|
+
this.#beginSessionTransition("mcp-control");
|
|
11019
|
+
try {
|
|
11020
|
+
return await body();
|
|
11021
|
+
} finally {
|
|
11022
|
+
this.#endSessionTransition();
|
|
11023
|
+
}
|
|
11024
|
+
});
|
|
11025
|
+
}
|
|
11026
|
+
|
|
11027
|
+
get hasExactMcpControls(): boolean {
|
|
11028
|
+
return getExactMcpControls(this) !== undefined;
|
|
11029
|
+
}
|
|
11030
|
+
|
|
11031
|
+
/**
|
|
11032
|
+
* Exact-config MCP status for the TUI `/mcp` command.
|
|
11033
|
+
*
|
|
11034
|
+
* Returns `undefined` unless this session holds the exact-config capability,
|
|
11035
|
+
* so a session built outside the root interactive entry point cannot read MCP
|
|
11036
|
+
* state through it. Data comes only from the session-owned tools-only manager
|
|
11037
|
+
* and the call never starts a deferred MCP startup or mutates connections.
|
|
11038
|
+
*/
|
|
11039
|
+
async getExactMcpStatusSnapshot(): Promise<ExactMcpStatusSnapshot | undefined> {
|
|
11040
|
+
const controls = getExactMcpControls(this);
|
|
11041
|
+
if (!controls) return undefined;
|
|
11042
|
+
return this.#withExactMcpControlAdmission(async () => {
|
|
11043
|
+
const manager = this.#ownedMcpManager;
|
|
11044
|
+
const live = new Map<string, ExactMcpServerState>();
|
|
11045
|
+
if (manager) {
|
|
11046
|
+
for (const name of manager.getAllServerNames()) live.set(name, manager.getConnectionStatus(name));
|
|
11047
|
+
}
|
|
11048
|
+
const declared = await this.#readExactMcpDeclaredServers(controls.configPath);
|
|
11049
|
+
const names = declared ? [...declared.keys()] : [...live.keys()];
|
|
11050
|
+
if (names.length === 0) {
|
|
11051
|
+
return {
|
|
11052
|
+
startup: declared ? "no-servers-declared" : "not-started",
|
|
11053
|
+
servers: [],
|
|
11054
|
+
} satisfies ExactMcpStatusSnapshot;
|
|
11055
|
+
}
|
|
11056
|
+
const tools = manager?.getTools() ?? [];
|
|
11057
|
+
const servers = names.map(name => ({
|
|
11058
|
+
name,
|
|
11059
|
+
transport: declared?.get(name) ?? ("unknown" as const),
|
|
11060
|
+
state: manager?.isExactServerSuppressed(name)
|
|
11061
|
+
? ("suspended" as const)
|
|
11062
|
+
: (live.get(name) ?? ("unavailable" as const)),
|
|
11063
|
+
toolCount: tools.reduce((count, tool) => (tool.mcpServerName === name ? count + 1 : count), 0),
|
|
11064
|
+
}));
|
|
11065
|
+
const startup =
|
|
11066
|
+
live.size === 0
|
|
11067
|
+
? "not-started"
|
|
11068
|
+
: servers.some(server => server.state === "connecting")
|
|
11069
|
+
? "in-flight"
|
|
11070
|
+
: "settled";
|
|
11071
|
+
return { startup, servers } satisfies ExactMcpStatusSnapshot;
|
|
11072
|
+
});
|
|
11073
|
+
}
|
|
11074
|
+
|
|
11075
|
+
/**
|
|
11076
|
+
* Apply one session-local exact-config control. Selection persistence is
|
|
11077
|
+
* deliberately bypassed: suppression is runtime state, not user authority.
|
|
11078
|
+
*/
|
|
11079
|
+
async controlExactMcpServer(
|
|
11080
|
+
action: ExactMcpControlAction,
|
|
11081
|
+
name: string,
|
|
11082
|
+
): Promise<ExactMcpControlResult | undefined> {
|
|
11083
|
+
const controls = getExactMcpControls(this);
|
|
11084
|
+
if (!controls) return undefined;
|
|
11085
|
+
return this.#withExactMcpControlAdmission(async () => {
|
|
11086
|
+
const manager = this.#ownedMcpManager;
|
|
11087
|
+
const declared = await this.#readExactMcpDeclaredServers(controls.configPath);
|
|
11088
|
+
const known = declared?.has(name) ?? manager?.getAllServerNames().includes(name) ?? false;
|
|
11089
|
+
if (!known) return { action, name, status: "unknown-server", toolCount: 0 };
|
|
11090
|
+
if (!manager) return { action, name, status: "unavailable", toolCount: 0 };
|
|
11091
|
+
|
|
11092
|
+
const selected = this.getSelectedMCPToolNames();
|
|
11093
|
+
const active = this.getActiveToolNames().filter(toolName => isMCPToolName(toolName));
|
|
11094
|
+
const serverToolNames = new Set(
|
|
11095
|
+
Array.from(this.#toolRegistry.values())
|
|
11096
|
+
.filter(
|
|
11097
|
+
tool =>
|
|
11098
|
+
isMCPBridgeTool(tool) && (tool as AgentTool & { mcpServerName?: string }).mcpServerName === name,
|
|
11099
|
+
)
|
|
11100
|
+
.map(tool => tool.name),
|
|
11101
|
+
);
|
|
11102
|
+
|
|
11103
|
+
const prepared = await manager.prepareExactServerControl(action, name);
|
|
11104
|
+
const result = prepared.result;
|
|
11105
|
+
if (result.status !== "suspended" && result.status !== "resumed" && result.status !== "reconnected") {
|
|
11106
|
+
await prepared.abort();
|
|
11107
|
+
return { action, ...result };
|
|
11108
|
+
}
|
|
11109
|
+
const snapshot = this.#captureExactMcpSessionState();
|
|
11110
|
+
const suspended = this.#exactMcpSuspendedTools.get(name);
|
|
11111
|
+
try {
|
|
11112
|
+
await this.refreshMCPTools(prepared.tools, {
|
|
11113
|
+
selectedMCPToolNames:
|
|
11114
|
+
action === "suspend"
|
|
11115
|
+
? selected.filter(toolName => !serverToolNames.has(toolName))
|
|
11116
|
+
: action === "resume"
|
|
11117
|
+
? [...selected, ...(suspended?.selected ?? [])]
|
|
11118
|
+
: selected,
|
|
11119
|
+
activeMCPToolNames:
|
|
11120
|
+
action === "suspend"
|
|
11121
|
+
? active.filter(toolName => !serverToolNames.has(toolName))
|
|
11122
|
+
: action === "resume"
|
|
11123
|
+
? [...active, ...(suspended?.active ?? [])]
|
|
11124
|
+
: active,
|
|
11125
|
+
persistMCPSelection: false,
|
|
11126
|
+
});
|
|
11127
|
+
await prepared.commit();
|
|
11128
|
+
if (action === "suspend") {
|
|
11129
|
+
this.#exactMcpSuspendedTools.set(name, {
|
|
11130
|
+
selected: selected.filter(toolName => serverToolNames.has(toolName)),
|
|
11131
|
+
active: active.filter(toolName => serverToolNames.has(toolName)),
|
|
11132
|
+
});
|
|
11133
|
+
} else if (action === "resume") {
|
|
11134
|
+
this.#exactMcpSuspendedTools.delete(name);
|
|
11135
|
+
}
|
|
11136
|
+
return { action, ...result };
|
|
11137
|
+
} catch (error) {
|
|
11138
|
+
this.#restoreExactMcpSessionState(snapshot);
|
|
11139
|
+
await prepared.abort();
|
|
11140
|
+
throw error;
|
|
11141
|
+
}
|
|
11142
|
+
});
|
|
11143
|
+
}
|
|
11144
|
+
|
|
11145
|
+
#captureExactMcpSessionState(): ExactMcpSessionStateSnapshot {
|
|
11146
|
+
return {
|
|
11147
|
+
toolRegistry: new Map(this.#toolRegistry),
|
|
11148
|
+
discoverableMCPTools: new Map(this.#discoverableMCPTools),
|
|
11149
|
+
selectedMCPToolNames: new Set(this.#selectedMCPToolNames),
|
|
11150
|
+
selectedDiscoveredToolNames: new Set(this.#selectedDiscoveredToolNames),
|
|
11151
|
+
tools: [...this.agent.state.tools],
|
|
11152
|
+
baseSystemPrompt: this.#baseSystemPrompt,
|
|
11153
|
+
systemPrompt: this.agent.state.systemPrompt,
|
|
11154
|
+
lastAppliedToolSignature: this.#lastAppliedToolSignature,
|
|
11155
|
+
pendingAppliedToolSignature: this.#pendingAppliedToolSignature,
|
|
11156
|
+
defaultModelSelectionMutationRevision: this.#defaultModelSelectionMutationRevision,
|
|
11157
|
+
baseSystemPromptGeneration: this.#baseSystemPromptGeneration,
|
|
11158
|
+
suspendedTools: new Map(this.#exactMcpSuspendedTools),
|
|
11159
|
+
};
|
|
11160
|
+
}
|
|
11161
|
+
|
|
11162
|
+
#restoreExactMcpSessionState(snapshot: ExactMcpSessionStateSnapshot): void {
|
|
11163
|
+
this.#toolRegistry.clear();
|
|
11164
|
+
for (const [name, tool] of snapshot.toolRegistry) this.#toolRegistry.set(name, tool);
|
|
11165
|
+
this.#discoverableMCPTools = snapshot.discoverableMCPTools;
|
|
11166
|
+
this.#selectedMCPToolNames = snapshot.selectedMCPToolNames;
|
|
11167
|
+
this.#selectedDiscoveredToolNames = snapshot.selectedDiscoveredToolNames;
|
|
11168
|
+
// These are the already-prepared predecessor objects from agent.state;
|
|
11169
|
+
// re-preparing them would stack execution guards and break identity.
|
|
11170
|
+
this.agent.setTools(snapshot.tools);
|
|
11171
|
+
this.#baseSystemPrompt = snapshot.baseSystemPrompt;
|
|
11172
|
+
this.agent.setSystemPrompt(snapshot.systemPrompt);
|
|
11173
|
+
this.#lastAppliedToolSignature = snapshot.lastAppliedToolSignature;
|
|
11174
|
+
this.#pendingAppliedToolSignature = snapshot.pendingAppliedToolSignature;
|
|
11175
|
+
this.#defaultModelSelectionMutationRevision = snapshot.defaultModelSelectionMutationRevision;
|
|
11176
|
+
this.#baseSystemPromptGeneration = snapshot.baseSystemPromptGeneration;
|
|
11177
|
+
this.#exactMcpSuspendedTools = snapshot.suspendedTools;
|
|
11178
|
+
this.#invalidateDiscoveryCaches();
|
|
11179
|
+
}
|
|
11180
|
+
|
|
11181
|
+
async #prepareExactMcpControlRetirement(): Promise<PreparedExactMcpRetirement | undefined> {
|
|
11182
|
+
const controls = getExactMcpControls(this);
|
|
11183
|
+
if (!controls) return undefined;
|
|
11184
|
+
const snapshot = this.#captureExactMcpSessionState();
|
|
11185
|
+
const prepared = await this.#ownedMcpManager?.prepareExactServerControlReset();
|
|
11186
|
+
const restoreControls = (): void => attachExactMcpControls(this, controls);
|
|
11187
|
+
try {
|
|
11188
|
+
if (prepared) await this.refreshMCPTools(prepared.tools, { persistMCPSelection: false });
|
|
11189
|
+
return {
|
|
11190
|
+
canCommit: () => prepared?.canCommit() ?? true,
|
|
11191
|
+
commit: async () => {
|
|
11192
|
+
try {
|
|
11193
|
+
await prepared?.commit();
|
|
11194
|
+
this.#exactMcpSuspendedTools.clear();
|
|
11195
|
+
revokeExactMcpControls(this);
|
|
11196
|
+
} catch (error) {
|
|
11197
|
+
this.#restoreExactMcpSessionState(snapshot);
|
|
11198
|
+
restoreControls();
|
|
11199
|
+
await prepared?.abort();
|
|
11200
|
+
throw error;
|
|
11201
|
+
}
|
|
11202
|
+
},
|
|
11203
|
+
abort: async () => {
|
|
11204
|
+
this.#restoreExactMcpSessionState(snapshot);
|
|
11205
|
+
restoreControls();
|
|
11206
|
+
await prepared?.abort();
|
|
11207
|
+
},
|
|
11208
|
+
};
|
|
11209
|
+
} catch (error) {
|
|
11210
|
+
this.#restoreExactMcpSessionState(snapshot);
|
|
11211
|
+
restoreControls();
|
|
11212
|
+
await prepared?.abort();
|
|
11213
|
+
throw error;
|
|
11214
|
+
}
|
|
11215
|
+
}
|
|
11216
|
+
|
|
11217
|
+
/**
|
|
11218
|
+
* Server names and transports the exact config declares, or `undefined` when
|
|
11219
|
+
* the file cannot be read or parsed. Only the name and the transport
|
|
11220
|
+
* discriminator are taken; command, args, url, headers, and env never leave
|
|
11221
|
+
* this method, and a read failure degrades to the manager's own view instead
|
|
11222
|
+
* of surfacing a parse error to the operator.
|
|
11223
|
+
*/
|
|
11224
|
+
async #readExactMcpDeclaredServers(configPath: string): Promise<Map<string, ExactMcpTransport> | undefined> {
|
|
11225
|
+
try {
|
|
11226
|
+
const parsed: unknown = await Bun.file(configPath).json();
|
|
11227
|
+
const servers = (parsed as { mcpServers?: unknown } | null)?.mcpServers;
|
|
11228
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) return undefined;
|
|
11229
|
+
const declared = new Map<string, ExactMcpTransport>();
|
|
11230
|
+
for (const [name, entry] of Object.entries(servers as Record<string, unknown>)) {
|
|
11231
|
+
declared.set(name, exactMcpTransportOf(entry));
|
|
11232
|
+
}
|
|
11233
|
+
return declared;
|
|
11234
|
+
} catch {
|
|
11235
|
+
return undefined;
|
|
11236
|
+
}
|
|
11237
|
+
}
|
|
11238
|
+
|
|
10109
11239
|
#getEditModeSession() {
|
|
10110
11240
|
return {
|
|
10111
11241
|
settings: this.settings,
|
|
@@ -10764,7 +11894,15 @@ export class AgentSession {
|
|
|
10764
11894
|
* Replace MCP tools in the registry and recompute the visible MCP tool set immediately.
|
|
10765
11895
|
* This allows /mcp add/remove/reauth to take effect without restarting the session.
|
|
10766
11896
|
*/
|
|
10767
|
-
async refreshMCPTools(
|
|
11897
|
+
async refreshMCPTools(
|
|
11898
|
+
mcpTools: CustomTool[],
|
|
11899
|
+
options: {
|
|
11900
|
+
selectedMCPToolNames?: string[];
|
|
11901
|
+
activeMCPToolNames?: string[];
|
|
11902
|
+
mandatoryMCPToolNames?: string[];
|
|
11903
|
+
persistMCPSelection?: boolean;
|
|
11904
|
+
} = {},
|
|
11905
|
+
): Promise<void> {
|
|
10768
11906
|
const previousSelectedMCPToolNames = this.getSelectedMCPToolNames();
|
|
10769
11907
|
const existingNames = Array.from(this.#toolRegistry.keys());
|
|
10770
11908
|
for (const name of existingNames) {
|
|
@@ -10796,17 +11934,27 @@ export class AgentSession {
|
|
|
10796
11934
|
}
|
|
10797
11935
|
|
|
10798
11936
|
this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
|
|
11937
|
+
if (options.mandatoryMCPToolNames !== undefined) {
|
|
11938
|
+
this.#mandatoryMCPToolNames = new Set(
|
|
11939
|
+
options.mandatoryMCPToolNames.map(name => name.toLowerCase()).filter(name => this.#toolRegistry.has(name)),
|
|
11940
|
+
);
|
|
11941
|
+
}
|
|
10799
11942
|
this.#pruneSelectedMCPToolNames();
|
|
10800
11943
|
const hasPersistedMCPToolSelection = this.buildDisplaySessionContext().hasPersistedMCPToolSelection;
|
|
10801
|
-
if (
|
|
11944
|
+
if (options.selectedMCPToolNames) {
|
|
11945
|
+
this.#selectedMCPToolNames = new Set(this.#filterSelectableMCPToolNames(options.selectedMCPToolNames));
|
|
11946
|
+
} else if (!hasPersistedMCPToolSelection) {
|
|
10802
11947
|
this.#selectedMCPToolNames = new Set(
|
|
10803
11948
|
this.#resolveConstructorMCPToolSelection() ?? this.#getConfiguredDefaultSelectedMCPToolNames(),
|
|
10804
11949
|
);
|
|
10805
11950
|
}
|
|
10806
|
-
const nextActive = [
|
|
11951
|
+
const nextActive = [
|
|
11952
|
+
...this.#getActiveNonMCPToolNames(),
|
|
11953
|
+
...(options.activeMCPToolNames ?? this.getSelectedMCPToolNames()),
|
|
11954
|
+
];
|
|
10807
11955
|
await this.#applyActiveToolsByName(nextActive, {
|
|
10808
11956
|
previousSelectedMCPToolNames,
|
|
10809
|
-
persistMCPSelection: hasPersistedMCPToolSelection,
|
|
11957
|
+
persistMCPSelection: options.persistMCPSelection ?? hasPersistedMCPToolSelection,
|
|
10810
11958
|
});
|
|
10811
11959
|
}
|
|
10812
11960
|
|
|
@@ -10946,6 +12094,11 @@ export class AgentSession {
|
|
|
10946
12094
|
return this.#postPromptTasks.size > 0;
|
|
10947
12095
|
}
|
|
10948
12096
|
|
|
12097
|
+
/** The host-liveness lease shared by admission, execution, and recovery work. */
|
|
12098
|
+
get sessionWorkLease(): SessionWorkLease {
|
|
12099
|
+
return this.#sessionWorkLease;
|
|
12100
|
+
}
|
|
12101
|
+
|
|
10949
12102
|
/** Stable resource ownership identifier for the active prompt run. */
|
|
10950
12103
|
get activePromptHandle(): string | undefined {
|
|
10951
12104
|
return this.agent.activeResourceRunId;
|
|
@@ -11029,7 +12182,7 @@ export class AgentSession {
|
|
|
11029
12182
|
|
|
11030
12183
|
/** Main startup calls this exactly once, after a strict open returned `kind: "opened"`. */
|
|
11031
12184
|
async continuePersistedHistory(): Promise<void> {
|
|
11032
|
-
this.#
|
|
12185
|
+
this.#assertExternalSessionIngress();
|
|
11033
12186
|
this.#assertRecoveryHydrationPromoted();
|
|
11034
12187
|
this.#removeEphemeralCustomMessages();
|
|
11035
12188
|
|
|
@@ -11072,7 +12225,7 @@ export class AgentSession {
|
|
|
11072
12225
|
// Re-check after the awaited preparation: a handoff can engage during the
|
|
11073
12226
|
// volatile-context/hindsight awaits above and this would otherwise start a
|
|
11074
12227
|
// turn against the session being handed off.
|
|
11075
|
-
this.#
|
|
12228
|
+
this.#assertExternalSessionIngress();
|
|
11076
12229
|
await this.agent.continue({
|
|
11077
12230
|
...this.#managedFallbackPromptOptions(),
|
|
11078
12231
|
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
@@ -11500,7 +12653,26 @@ export class AgentSession {
|
|
|
11500
12653
|
path.join(sessionStateDir(this.sessionManager.getCwd(), sessionId), "workflow-gates.json"),
|
|
11501
12654
|
)
|
|
11502
12655
|
: new MemoryGateStore();
|
|
11503
|
-
|
|
12656
|
+
let emitter: BrokerWorkflowGateEmitter;
|
|
12657
|
+
emitter = new BrokerWorkflowGateEmitter(sessionId, gateStore, {
|
|
12658
|
+
continuationLost: () => {
|
|
12659
|
+
if (
|
|
12660
|
+
!isCurrentWorkflowGateContinuation(
|
|
12661
|
+
this.sessionManager.getSessionId(),
|
|
12662
|
+
sessionId,
|
|
12663
|
+
this.#workflowGateEmitter,
|
|
12664
|
+
emitter,
|
|
12665
|
+
)
|
|
12666
|
+
)
|
|
12667
|
+
return;
|
|
12668
|
+
void this.abort({ cause: "internal", silent: true }).catch(error => {
|
|
12669
|
+
logger.warn("Failed to settle a workflow gate turn after continuation loss", {
|
|
12670
|
+
error: error instanceof Error ? error.message : String(error),
|
|
12671
|
+
});
|
|
12672
|
+
});
|
|
12673
|
+
},
|
|
12674
|
+
});
|
|
12675
|
+
return emitter;
|
|
11504
12676
|
}
|
|
11505
12677
|
|
|
11506
12678
|
/**
|
|
@@ -12061,11 +13233,14 @@ export class AgentSession {
|
|
|
12061
13233
|
options?.images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
|
|
12062
13234
|
if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
|
|
12063
13235
|
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
13236
|
+
if (options?.synthetic !== true && options?.attribution !== "agent")
|
|
13237
|
+
invalidateSessionTitleGeneration(this.sessionManager);
|
|
12064
13238
|
const sdkRunToken = readSdkRunCapability(options?.sdkRunCapability);
|
|
12065
13239
|
const internalOptions: InternalPromptOptions | undefined = options
|
|
12066
13240
|
? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) }
|
|
12067
13241
|
: undefined;
|
|
12068
13242
|
this.#assertRecoveryHydrationPromoted();
|
|
13243
|
+
this.#assertExternalSessionIngress();
|
|
12069
13244
|
const owner = this.#sessionAdmissionContext.getStore();
|
|
12070
13245
|
if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
|
|
12071
13246
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
@@ -12174,7 +13349,7 @@ export class AgentSession {
|
|
|
12174
13349
|
throw new AgentBusyError();
|
|
12175
13350
|
}
|
|
12176
13351
|
if (options.streamingBehavior === "followUp") {
|
|
12177
|
-
await this.#
|
|
13352
|
+
await this.#queueFollowUpTextAfterReservation(expandedText, options?.images, {
|
|
12178
13353
|
forceOneAtATime: options.followUpQueuePolicy === "sequential",
|
|
12179
13354
|
claimsGenuineUserIntent,
|
|
12180
13355
|
});
|
|
@@ -12187,8 +13362,10 @@ export class AgentSession {
|
|
|
12187
13362
|
if (workflowIntentDiff) {
|
|
12188
13363
|
this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
|
|
12189
13364
|
}
|
|
13365
|
+
this.#assertExternalSessionIngress();
|
|
12190
13366
|
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
12191
13367
|
else options?.onPreflightAccepted?.();
|
|
13368
|
+
this.#assertExternalSessionIngress();
|
|
12192
13369
|
return;
|
|
12193
13370
|
}
|
|
12194
13371
|
|
|
@@ -12207,7 +13384,7 @@ export class AgentSession {
|
|
|
12207
13384
|
) {
|
|
12208
13385
|
admission.release();
|
|
12209
13386
|
if (options.streamingBehavior === "followUp") {
|
|
12210
|
-
await this.#
|
|
13387
|
+
await this.#queueFollowUpTextAfterReservation(expandedText, options?.images, {
|
|
12211
13388
|
forceOneAtATime: options.followUpQueuePolicy === "sequential",
|
|
12212
13389
|
claimsGenuineUserIntent,
|
|
12213
13390
|
});
|
|
@@ -12220,8 +13397,10 @@ export class AgentSession {
|
|
|
12220
13397
|
if (workflowIntentDiff) {
|
|
12221
13398
|
this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
|
|
12222
13399
|
}
|
|
13400
|
+
this.#assertExternalSessionIngress();
|
|
12223
13401
|
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
12224
13402
|
else options?.onPreflightAccepted?.();
|
|
13403
|
+
this.#assertExternalSessionIngress();
|
|
12225
13404
|
return;
|
|
12226
13405
|
}
|
|
12227
13406
|
this.#throwIfPromptPreflightCancelled(admissionGeneration, admissionSignal);
|
|
@@ -12409,6 +13588,7 @@ export class AgentSession {
|
|
|
12409
13588
|
const internalOptions: InternalCustomMessageOptions | undefined = options
|
|
12410
13589
|
? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) }
|
|
12411
13590
|
: undefined;
|
|
13591
|
+
this.#assertExternalSessionIngress();
|
|
12412
13592
|
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
12413
13593
|
const textContent =
|
|
12414
13594
|
typeof message.content === "string"
|
|
@@ -12468,9 +13648,11 @@ export class AgentSession {
|
|
|
12468
13648
|
await activationSeed?.rollback();
|
|
12469
13649
|
throw promptPreflightCancelledError();
|
|
12470
13650
|
}
|
|
13651
|
+
this.#assertExternalSessionIngress();
|
|
12471
13652
|
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
12472
13653
|
else options?.onPreflightAccepted?.();
|
|
12473
13654
|
durableAcceptanceCompleted = true;
|
|
13655
|
+
this.#assertExternalSessionIngress();
|
|
12474
13656
|
if (options?.preflightSignal?.aborted) {
|
|
12475
13657
|
await activationSeed?.rollback();
|
|
12476
13658
|
throw promptPreflightCancelledError();
|
|
@@ -12907,10 +14089,12 @@ export class AgentSession {
|
|
|
12907
14089
|
await this.#promptAgentWithIdleRetry(preSubmit, agentPromptOptions, predecessorAgentEndHold, {
|
|
12908
14090
|
signal: preflightSignal,
|
|
12909
14091
|
resourceRunId: this.#runResourceLeaseContext.getStore()?.resourceRunId,
|
|
12910
|
-
onPreflightAccepted: () => {
|
|
14092
|
+
onPreflightAccepted: async () => {
|
|
14093
|
+
this.#assertExternalSessionIngress({ allowCancelAndSubmit: true });
|
|
12911
14094
|
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
12912
|
-
if (options?.onPreflightAcceptCommit)
|
|
12913
|
-
options?.onPreflightAccepted?.();
|
|
14095
|
+
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
14096
|
+
else options?.onPreflightAccepted?.();
|
|
14097
|
+
this.#assertExternalSessionIngress({ allowCancelAndSubmit: true });
|
|
12914
14098
|
},
|
|
12915
14099
|
});
|
|
12916
14100
|
const activeTerminalScope = this.#activeAttemptScope;
|
|
@@ -13169,7 +14353,9 @@ export class AgentSession {
|
|
|
13169
14353
|
images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
|
|
13170
14354
|
if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
|
|
13171
14355
|
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
14356
|
+
invalidateSessionTitleGeneration(this.sessionManager);
|
|
13172
14357
|
this.#assertRecoveryHydrationPromoted();
|
|
14358
|
+
this.#assertExternalSessionIngress({ allowCancelAndSubmit: true });
|
|
13173
14359
|
if (text.startsWith("/")) {
|
|
13174
14360
|
this.#throwIfExtensionCommand(text);
|
|
13175
14361
|
}
|
|
@@ -13178,7 +14364,10 @@ export class AgentSession {
|
|
|
13178
14364
|
if (expandedText.trim().length === 0 && !hasUsableImage)
|
|
13179
14365
|
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
13180
14366
|
assertImagePlaceholdersHavePayload(expandedText, images);
|
|
13181
|
-
await this.#queueSteer(expandedText, images, {
|
|
14367
|
+
await this.#queueSteer(expandedText, images, {
|
|
14368
|
+
claimsGenuineUserIntent: true,
|
|
14369
|
+
allowCancelAndSubmit: true,
|
|
14370
|
+
});
|
|
13182
14371
|
}
|
|
13183
14372
|
|
|
13184
14373
|
/**
|
|
@@ -13193,7 +14382,9 @@ export class AgentSession {
|
|
|
13193
14382
|
images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
|
|
13194
14383
|
if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
|
|
13195
14384
|
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
14385
|
+
invalidateSessionTitleGeneration(this.sessionManager);
|
|
13196
14386
|
this.#assertRecoveryHydrationPromoted();
|
|
14387
|
+
this.#assertExternalSessionIngress({ allowCancelAndSubmit: true });
|
|
13197
14388
|
if (text.startsWith("/")) {
|
|
13198
14389
|
this.#throwIfExtensionCommand(text);
|
|
13199
14390
|
}
|
|
@@ -13202,9 +14393,10 @@ export class AgentSession {
|
|
|
13202
14393
|
if (expandedText.trim().length === 0 && !hasUsableImage)
|
|
13203
14394
|
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
13204
14395
|
assertImagePlaceholdersHavePayload(expandedText, images);
|
|
13205
|
-
await this.#
|
|
14396
|
+
await this.#queueFollowUpTextAfterReservation(expandedText, images, {
|
|
13206
14397
|
forceOneAtATime: options?.followUpQueuePolicy === "sequential",
|
|
13207
14398
|
claimsGenuineUserIntent: true,
|
|
14399
|
+
allowCancelAndSubmit: true,
|
|
13208
14400
|
});
|
|
13209
14401
|
}
|
|
13210
14402
|
|
|
@@ -13217,12 +14409,19 @@ export class AgentSession {
|
|
|
13217
14409
|
options?: {
|
|
13218
14410
|
claimsGenuineUserIntent?: boolean;
|
|
13219
14411
|
onPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
|
|
14412
|
+
onQueued?: (message: AgentMessage) => void;
|
|
14413
|
+
onQueuedAfterAdmission?: (message: AgentMessage, cancelQueued: () => boolean) => void;
|
|
13220
14414
|
external?: boolean;
|
|
13221
14415
|
sdkRunToken?: string;
|
|
13222
14416
|
forceOneAtATime?: boolean;
|
|
14417
|
+
allowDuringSessionTransition?: boolean;
|
|
14418
|
+
allowCancelAndSubmit?: boolean;
|
|
13223
14419
|
},
|
|
13224
|
-
): Promise<
|
|
13225
|
-
this.#
|
|
14420
|
+
): Promise<QueuedFollowUpOwner> {
|
|
14421
|
+
this.#assertExternalSessionIngress({
|
|
14422
|
+
allowTrackedSuccessor: options?.allowDuringSessionTransition,
|
|
14423
|
+
allowCancelAndSubmit: options?.allowCancelAndSubmit,
|
|
14424
|
+
});
|
|
13226
14425
|
assertImagePlaceholdersHavePayload(text, images);
|
|
13227
14426
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
13228
14427
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
@@ -13235,7 +14434,7 @@ export class AgentSession {
|
|
|
13235
14434
|
// in a queue nobody owns.
|
|
13236
14435
|
const admission = this.agent.steer(message, options?.forceOneAtATime ? { forceOneAtATime: true } : undefined);
|
|
13237
14436
|
if (!admission.admitted) {
|
|
13238
|
-
await this.#
|
|
14437
|
+
const queuedFollowUp = await this.#queueFollowUpAfterReservation(message, displayText, {
|
|
13239
14438
|
forceOneAtATime: options?.forceOneAtATime,
|
|
13240
14439
|
claimsGenuineUserIntent: options?.claimsGenuineUserIntent,
|
|
13241
14440
|
onPromoted: options?.onPromoted,
|
|
@@ -13243,16 +14442,21 @@ export class AgentSession {
|
|
|
13243
14442
|
scheduleNonAdmittedWake: false,
|
|
13244
14443
|
onQueued: message => {
|
|
13245
14444
|
if (this.#abortUnwind && !options?.forceOneAtATime) this.#abortUnwindSteerFallbacks.push(message);
|
|
14445
|
+
options?.onQueued?.(message);
|
|
13246
14446
|
},
|
|
14447
|
+
onQueuedAfterAdmission: options?.onQueuedAfterAdmission,
|
|
14448
|
+
allowDuringSessionTransition: options?.allowDuringSessionTransition,
|
|
14449
|
+
allowCancelAndSubmit: options?.allowCancelAndSubmit,
|
|
13247
14450
|
});
|
|
13248
14451
|
this.#scheduleNonAdmittedQueuedContinuation();
|
|
13249
|
-
return;
|
|
14452
|
+
return queuedFollowUp;
|
|
13250
14453
|
}
|
|
13251
14454
|
if (options?.forceOneAtATime) this.#sequentialSteerMessages.add(message);
|
|
13252
14455
|
this.#steeringMessages.push(this.#createQueuedDisplayEntry(displayText, undefined, message));
|
|
13253
14456
|
if (options?.external) {
|
|
13254
14457
|
this.#externalSteerMessages.add(message);
|
|
13255
14458
|
this.#externalSteerAdmissionSeq.set(message, ++this.#steeringAdmissionSeq);
|
|
14459
|
+
this.#queuedAdmissionSeq.set(message, ++this.#queuedAdmissionSequence);
|
|
13256
14460
|
if (options.onPromoted) this.#steerPromotionHooks.set(message, options.onPromoted);
|
|
13257
14461
|
}
|
|
13258
14462
|
if (options?.sdkRunToken) this.#sdkRunTokensByQueuedMessage.set(message, options.sdkRunToken);
|
|
@@ -13260,6 +14464,45 @@ export class AgentSession {
|
|
|
13260
14464
|
const epoch = this.#claimDeepInterviewUserIntent();
|
|
13261
14465
|
this.#deepInterviewGenuineUserMessageEpochs.set(message, epoch);
|
|
13262
14466
|
}
|
|
14467
|
+
options?.onQueued?.(message);
|
|
14468
|
+
const cancelQueuedSteer = (): boolean => {
|
|
14469
|
+
const removedQueues = this.agent.removeQueuedMessages(candidate => candidate === message);
|
|
14470
|
+
const removed = removedQueues.steering > 0 || removedQueues.followUp > 0;
|
|
14471
|
+
if (!removed) return false;
|
|
14472
|
+
this.#steeringMessages = this.#steeringMessages.filter(entry => entry.message !== message);
|
|
14473
|
+
this.#followUpMessages = this.#followUpMessages.filter(entry => entry.message !== message);
|
|
14474
|
+
this.#externalSteerMessages.delete(message);
|
|
14475
|
+
this.#externalFollowUps.delete(message);
|
|
14476
|
+
this.#sequentialSteerMessages.delete(message);
|
|
14477
|
+
this.#fireQueuedRemovalHooks([message]);
|
|
14478
|
+
this.#releaseDeferredSdkFollowUps();
|
|
14479
|
+
return true;
|
|
14480
|
+
};
|
|
14481
|
+
options?.onQueuedAfterAdmission?.(message, cancelQueuedSteer);
|
|
14482
|
+
return { cancel: cancelQueuedSteer };
|
|
14483
|
+
}
|
|
14484
|
+
|
|
14485
|
+
/**
|
|
14486
|
+
* One-shot preflight-abort binding: an invocation cancelled after its queue
|
|
14487
|
+
* admission must cancel the submission it admitted, so an aborted dispatch
|
|
14488
|
+
* can never execute later. Both explicit delivery paths (follow-up and
|
|
14489
|
+
* steer) share this helper so their cancellation semantics cannot diverge
|
|
14490
|
+
* (exact-head review P1).
|
|
14491
|
+
*/
|
|
14492
|
+
#bindPreflightAbortCancellation(
|
|
14493
|
+
signal: AbortSignal | undefined,
|
|
14494
|
+
owner: QueuedFollowUpOwner,
|
|
14495
|
+
state?: TrackedQueuedInput,
|
|
14496
|
+
): void {
|
|
14497
|
+
if (!signal) return;
|
|
14498
|
+
const cancelQueued = () => {
|
|
14499
|
+
if (state) state.submission.cancel();
|
|
14500
|
+
else owner.cancel();
|
|
14501
|
+
};
|
|
14502
|
+
const removeListener = () => signal.removeEventListener("abort", cancelQueued);
|
|
14503
|
+
if (state) state.removePreflightAbortListener = removeListener;
|
|
14504
|
+
signal.addEventListener("abort", cancelQueued, { once: true });
|
|
14505
|
+
if (signal.aborted) cancelQueued();
|
|
13263
14506
|
}
|
|
13264
14507
|
|
|
13265
14508
|
/**
|
|
@@ -13288,7 +14531,12 @@ export class AgentSession {
|
|
|
13288
14531
|
* run that would have polled the queue is gone, so nothing else owns it.
|
|
13289
14532
|
*/
|
|
13290
14533
|
#scheduleQueuedDelivery(delayMs?: number): void {
|
|
13291
|
-
if (this.#cancelAndSubmitInProgress
|
|
14534
|
+
if (this.#cancelAndSubmitInProgress) return;
|
|
14535
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) {
|
|
14536
|
+
this.#queuedDeliveryPendingWhileTransition = true;
|
|
14537
|
+
return;
|
|
14538
|
+
}
|
|
14539
|
+
if (!this.#canDeliverQueuedMessages()) return;
|
|
13292
14540
|
this.#scheduleAgentContinue({
|
|
13293
14541
|
...(delayMs === undefined ? {} : { delayMs }),
|
|
13294
14542
|
shouldContinue: () => this.#canDeliverQueuedMessages() && this.agent.hasQueuedMessages(),
|
|
@@ -13303,109 +14551,201 @@ export class AgentSession {
|
|
|
13303
14551
|
async #queueFollowUp(
|
|
13304
14552
|
text: string,
|
|
13305
14553
|
images?: ImageContent[],
|
|
13306
|
-
options?:
|
|
13307
|
-
forceOneAtATime?: boolean;
|
|
13308
|
-
claimsGenuineUserIntent?: boolean;
|
|
13309
|
-
onPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
|
|
13310
|
-
sdkRunToken?: string;
|
|
13311
|
-
onQueued?: (message: AgentMessage) => void;
|
|
13312
|
-
scheduleNonAdmittedWake?: boolean;
|
|
13313
|
-
},
|
|
14554
|
+
options?: QueueFollowUpOptions,
|
|
13314
14555
|
): Promise<QueuedFollowUpOwner> {
|
|
13315
|
-
this.#
|
|
14556
|
+
this.#assertExternalSessionIngress({
|
|
14557
|
+
allowTrackedSuccessor: options?.allowDuringSessionTransition,
|
|
14558
|
+
allowCancelAndSubmit: options?.allowCancelAndSubmit,
|
|
14559
|
+
});
|
|
14560
|
+
assertImagePlaceholdersHavePayload(text, images);
|
|
14561
|
+
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
14562
|
+
if (images && images.length > 0) content.push(...images);
|
|
14563
|
+
const message = { role: "user" as const, content, attribution: "user" as const, timestamp: Date.now() };
|
|
14564
|
+
return this.#queueFollowUpMessage(message, text || (images && images.length > 0 ? "[Image]" : ""), options);
|
|
14565
|
+
}
|
|
14566
|
+
|
|
14567
|
+
async #queueFollowUpTextAfterReservation(
|
|
14568
|
+
text: string,
|
|
14569
|
+
images?: ImageContent[],
|
|
14570
|
+
options?: QueueFollowUpOptions,
|
|
14571
|
+
): Promise<QueuedFollowUpOwner> {
|
|
14572
|
+
this.#assertExternalSessionIngress({
|
|
14573
|
+
allowTrackedSuccessor: options?.allowDuringSessionTransition,
|
|
14574
|
+
allowCancelAndSubmit: options?.allowCancelAndSubmit,
|
|
14575
|
+
});
|
|
13316
14576
|
assertImagePlaceholdersHavePayload(text, images);
|
|
13317
|
-
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
13318
|
-
const queueWasEmpty = !this.agent.hasQueuedMessages();
|
|
13319
14577
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
13320
14578
|
if (images && images.length > 0) content.push(...images);
|
|
13321
14579
|
const message = { role: "user" as const, content, attribution: "user" as const, timestamp: Date.now() };
|
|
14580
|
+
return this.#queueFollowUpAfterReservation(
|
|
14581
|
+
message,
|
|
14582
|
+
text || (images && images.length > 0 ? "[Image]" : ""),
|
|
14583
|
+
options,
|
|
14584
|
+
);
|
|
14585
|
+
}
|
|
14586
|
+
|
|
14587
|
+
#queueFollowUpMessage(
|
|
14588
|
+
message: AgentMessage,
|
|
14589
|
+
displayText: string,
|
|
14590
|
+
options?: QueueFollowUpOptions,
|
|
14591
|
+
): QueuedFollowUpOwner {
|
|
14592
|
+
const queueWasEmpty = !this.agent.hasQueuedMessages();
|
|
13322
14593
|
options?.onQueued?.(message);
|
|
13323
|
-
|
|
13324
|
-
|
|
13325
|
-
|
|
13326
|
-
|
|
13327
|
-
this.#
|
|
14594
|
+
const displayEntry =
|
|
14595
|
+
options?.createDisplayEntry === false
|
|
14596
|
+
? undefined
|
|
14597
|
+
: this.#createQueuedDisplayEntry(displayText, undefined, message);
|
|
14598
|
+
if (displayEntry) this.#followUpMessages.push(displayEntry);
|
|
14599
|
+
if (options?.trackExternalFollowUp !== false) this.#externalFollowUps.add(message);
|
|
13328
14600
|
if (options?.onPromoted) this.#followUpPromotionHooks.set(message, options.onPromoted);
|
|
14601
|
+
if (options?.sdkRunToken || options?.onQueuedAfterAdmission) {
|
|
14602
|
+
this.#queuedAdmissionSeq.set(message, ++this.#queuedAdmissionSequence);
|
|
14603
|
+
}
|
|
13329
14604
|
if (options?.claimsGenuineUserIntent) {
|
|
13330
14605
|
const epoch = this.#claimDeepInterviewUserIntent();
|
|
13331
14606
|
this.#deepInterviewGenuineUserMessageEpochs.set(message, epoch);
|
|
13332
14607
|
}
|
|
13333
14608
|
if (options?.sdkRunToken) this.#sdkRunTokensByQueuedMessage.set(message, options.sdkRunToken);
|
|
13334
|
-
|
|
13335
|
-
|
|
13336
|
-
|
|
13337
|
-
|
|
13338
|
-
|
|
13339
|
-
// When this is the first queued message and the session is in a resumable
|
|
13340
|
-
// assistant-ended state, schedule an immediate continue so it is delivered
|
|
13341
|
-
// without waiting for the next user turn. A later accepted follow-up must
|
|
13342
|
-
// not start unrelated queued work ahead of it, because that work has a
|
|
13343
|
-
// different cancellation and terminal owner.
|
|
13344
|
-
if (queueWasEmpty) {
|
|
13345
|
-
this.#scheduleQueuedFollowUpContinuation(() =>
|
|
13346
|
-
this.agent.snapshotFollowUp().some(candidate => candidate === message),
|
|
13347
|
-
);
|
|
13348
|
-
if (options?.scheduleNonAdmittedWake !== false) this.#scheduleNonAdmittedQueuedContinuation();
|
|
14609
|
+
// If older deferred work is waiting while the Agent is idle, release
|
|
14610
|
+
// that head before parking this newer message. Otherwise the newer message
|
|
14611
|
+
// would enter the live queue first and invert SDK FIFO ownership.
|
|
14612
|
+
if (!this.agent.state.isStreaming && !this.agent.hasQueuedMessages() && this.#deferredSdkFollowUps.length > 0) {
|
|
14613
|
+
this.#releaseDeferredSdkFollowUps();
|
|
13349
14614
|
}
|
|
13350
|
-
|
|
14615
|
+
const owner: QueuedFollowUpOwner = {
|
|
13351
14616
|
cancel: () => {
|
|
13352
14617
|
const deferredIndex = this.#deferredSdkFollowUps.indexOf(message);
|
|
13353
14618
|
let removed = false;
|
|
13354
14619
|
if (deferredIndex !== -1) {
|
|
13355
14620
|
this.#deferredSdkFollowUps.splice(deferredIndex, 1);
|
|
14621
|
+
this.#forgetDeferredFollowUpMetadata(message);
|
|
13356
14622
|
removed = true;
|
|
13357
14623
|
} else {
|
|
13358
14624
|
removed = this.agent.removeQueuedMessages(candidate => candidate === message).followUp > 0;
|
|
13359
|
-
// This message was already released from the deferred queue; its
|
|
13360
|
-
// scheduled continuation was cancelled before it started. No further
|
|
13361
|
-
// agent_end may arrive to release the next deferred follow-up, so
|
|
13362
|
-
// advance the queue here to keep the next accepted SDK request moving.
|
|
13363
|
-
if (removed) this.#releaseDeferredSdkFollowUps();
|
|
13364
14625
|
}
|
|
13365
14626
|
if (removed) {
|
|
13366
|
-
this.#followUpMessages =
|
|
13367
|
-
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
//
|
|
13371
|
-
|
|
13372
|
-
|
|
14627
|
+
this.#followUpMessages = displayEntry
|
|
14628
|
+
? this.#followUpMessages.filter(entry => entry !== displayEntry)
|
|
14629
|
+
: this.#followUpMessages.filter(entry => entry.message !== message);
|
|
14630
|
+
this.#fireQueuedRemovalHooks([message]);
|
|
14631
|
+
// Either removal site can unblock the NEXT deferred SDK follow-up:
|
|
14632
|
+
// the cancelled entry may itself have been the deferred head, or it
|
|
14633
|
+
// may have been the live work that deferred head was waiting behind.
|
|
14634
|
+
// Without this release the successor stays accepted with no execution
|
|
14635
|
+
// or terminal settlement (exact-head review P1).
|
|
14636
|
+
this.#releaseDeferredSdkFollowUps();
|
|
13373
14637
|
}
|
|
13374
14638
|
return removed;
|
|
13375
14639
|
},
|
|
13376
14640
|
};
|
|
14641
|
+
options?.onQueuedAfterAdmission?.(message, owner.cancel);
|
|
14642
|
+
// Existing deferred work is part of the same FIFO even when the Agent
|
|
14643
|
+
// queue is currently empty. Enqueuing a newer successor directly into the
|
|
14644
|
+
// Agent in that state would let it run before the older deferred request.
|
|
14645
|
+
if (
|
|
14646
|
+
this.#deferredSdkFollowUps.length > 0 ||
|
|
14647
|
+
(options?.sdkRunToken && (this.agent.state.isStreaming || this.agent.hasQueuedMessages()))
|
|
14648
|
+
) {
|
|
14649
|
+
if (options?.forceOneAtATime || options?.sdkRunToken) {
|
|
14650
|
+
this.#deferredFollowUpForceOneAtATime.add(message);
|
|
14651
|
+
}
|
|
14652
|
+
this.#deferredSdkFollowUps.push(message);
|
|
14653
|
+
} else {
|
|
14654
|
+
this.agent.followUp(message, options?.forceOneAtATime ? { forceOneAtATime: true } : undefined);
|
|
14655
|
+
}
|
|
14656
|
+
if (queueWasEmpty) {
|
|
14657
|
+
this.#scheduleQueuedFollowUpContinuation(() =>
|
|
14658
|
+
this.agent.snapshotFollowUp().some(candidate => candidate === message),
|
|
14659
|
+
);
|
|
14660
|
+
if (options?.scheduleNonAdmittedWake !== false) this.#scheduleNonAdmittedQueuedContinuation();
|
|
14661
|
+
}
|
|
14662
|
+
return owner;
|
|
14663
|
+
}
|
|
14664
|
+
|
|
14665
|
+
#forgetDeferredFollowUpMetadata(message: AgentMessage): void {
|
|
14666
|
+
this.#deferredFollowUpForceOneAtATime.delete(message);
|
|
14667
|
+
const batch = this.#deferredFollowUpBatches.get(message);
|
|
14668
|
+
if (!batch) return;
|
|
14669
|
+
const remaining = batch.filter(candidate => this.#deferredSdkFollowUps.includes(candidate));
|
|
14670
|
+
for (const member of batch) this.#deferredFollowUpBatches.delete(member);
|
|
14671
|
+
if (remaining.length > 1) for (const member of remaining) this.#deferredFollowUpBatches.set(member, remaining);
|
|
14672
|
+
}
|
|
14673
|
+
|
|
14674
|
+
async #queueFollowUpAfterReservation(
|
|
14675
|
+
message: AgentMessage,
|
|
14676
|
+
displayText: string,
|
|
14677
|
+
options?: QueueFollowUpOptions,
|
|
14678
|
+
reservationSignal?: AbortSignal,
|
|
14679
|
+
): Promise<QueuedFollowUpOwner> {
|
|
14680
|
+
if (reservationSignal?.aborted) throw promptPreflightCancelledError();
|
|
14681
|
+
const transitionGeneration = this.#coordinatorPersistGeneration;
|
|
14682
|
+
const reservationEpoch = ++this.#followUpReservationEpoch;
|
|
14683
|
+
this.#activeFollowUpReservationEpochs.add(reservationEpoch);
|
|
14684
|
+
const releaseReservation = () => {
|
|
14685
|
+
this.#activeFollowUpReservationEpochs.delete(reservationEpoch);
|
|
14686
|
+
const waiters = [...this.#followUpReservationDrainWaiters];
|
|
14687
|
+
this.#followUpReservationDrainWaiters.clear();
|
|
14688
|
+
for (const waiter of waiters) waiter();
|
|
14689
|
+
};
|
|
14690
|
+
try {
|
|
14691
|
+
if ([...this.#activeFollowUpReservationEpochs].some(epoch => epoch < reservationEpoch)) {
|
|
14692
|
+
await this.#waitForEarlierFollowUpReservations(
|
|
14693
|
+
reservationEpoch,
|
|
14694
|
+
reservationSignal,
|
|
14695
|
+
undefined,
|
|
14696
|
+
transitionGeneration,
|
|
14697
|
+
);
|
|
14698
|
+
}
|
|
14699
|
+
if (reservationSignal?.aborted) throw promptPreflightCancelledError();
|
|
14700
|
+
this.#assertExternalSessionIngress({
|
|
14701
|
+
allowTrackedSuccessor: options?.allowDuringSessionTransition,
|
|
14702
|
+
allowCancelAndSubmit: options?.allowCancelAndSubmit,
|
|
14703
|
+
});
|
|
14704
|
+
return this.#queueFollowUpMessage(message, displayText, options);
|
|
14705
|
+
} finally {
|
|
14706
|
+
releaseReservation();
|
|
14707
|
+
}
|
|
13377
14708
|
}
|
|
13378
|
-
|
|
14709
|
+
|
|
14710
|
+
#releaseDeferredSdkFollowUps(): boolean {
|
|
13379
14711
|
// A deferred SDK follow-up must become the sole first message at the next
|
|
13380
14712
|
// acceptance so its run token is bound to the agent_start. Releasing it
|
|
13381
14713
|
// behind still-queued work reproduces the token-less mid-run consumption
|
|
13382
14714
|
// hazard, so wait for the queue to drain; the next agent_end retries.
|
|
13383
|
-
if (this.agent.hasQueuedMessages()) return;
|
|
13384
|
-
const message = this.#deferredSdkFollowUps
|
|
13385
|
-
if (!message) return;
|
|
13386
|
-
this.
|
|
14715
|
+
if (this.agent.state.isStreaming || this.agent.hasQueuedMessages()) return false;
|
|
14716
|
+
const message = this.#deferredSdkFollowUps[0];
|
|
14717
|
+
if (!message) return false;
|
|
14718
|
+
const batch = this.#deferredFollowUpBatches.get(message);
|
|
14719
|
+
const released =
|
|
14720
|
+
batch && batch.length > 1 && batch.every((candidate, index) => this.#deferredSdkFollowUps[index] === candidate)
|
|
14721
|
+
? [...batch]
|
|
14722
|
+
: [message];
|
|
14723
|
+
const forceOneAtATime = this.#deferredFollowUpForceOneAtATime.has(message);
|
|
14724
|
+
this.#deferredSdkFollowUps.splice(0, released.length);
|
|
14725
|
+
for (const candidate of released) this.#forgetDeferredFollowUpMetadata(candidate);
|
|
14726
|
+
if (released.length > 1) {
|
|
14727
|
+
this.agent.markFollowUpBatch(released);
|
|
14728
|
+
this.agent.restoreFollowUp(released);
|
|
14729
|
+
} else {
|
|
14730
|
+
this.agent.followUp(message, forceOneAtATime ? { forceOneAtATime: true } : undefined);
|
|
14731
|
+
}
|
|
14732
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) {
|
|
14733
|
+
this.#queuedDeliveryPendingWhileTransition = true;
|
|
14734
|
+
return true;
|
|
14735
|
+
}
|
|
13387
14736
|
this.#scheduleAgentContinue({
|
|
13388
|
-
shouldContinue: () => this.#
|
|
14737
|
+
shouldContinue: () => this.#canDeliverQueuedMessages() && this.agent.hasQueuedMessages(),
|
|
13389
14738
|
rescheduleOnBusy: true,
|
|
13390
14739
|
continueQueuedOnly: true,
|
|
13391
14740
|
});
|
|
14741
|
+
return true;
|
|
13392
14742
|
}
|
|
13393
|
-
#canStartDeferredSdkFollowUp(): boolean {
|
|
13394
|
-
if (this.agent.state.isStreaming) return false;
|
|
13395
|
-
if (this.isCompacting) return false;
|
|
13396
|
-
if (this.isBashRunning) return false;
|
|
13397
|
-
if (this.isEvalRunning) return false;
|
|
13398
|
-
if (this.isRetrying) return false;
|
|
13399
|
-
const messages = this.agent.state.messages;
|
|
13400
|
-
const last = messages[messages.length - 1];
|
|
13401
|
-
return last?.role === "assistant" || last?.role === "bashExecution" || last?.role === "pythonExecution";
|
|
13402
|
-
}
|
|
13403
|
-
|
|
13404
14743
|
/**
|
|
13405
14744
|
* Gate for idle-path follow-up auto-continue. See `#queueFollowUp` for rationale.
|
|
13406
14745
|
*/
|
|
13407
14746
|
#canAutoContinueForFollowUp(): boolean {
|
|
13408
14747
|
if (this.#abortUnwind) return false;
|
|
14748
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) return false;
|
|
13409
14749
|
if (this.isStreaming) return false;
|
|
13410
14750
|
if (this.isCompacting) return false;
|
|
13411
14751
|
if (this.isBashRunning) return false;
|
|
@@ -13441,6 +14781,7 @@ export class AgentSession {
|
|
|
13441
14781
|
*/
|
|
13442
14782
|
#canDeliverQueuedMessages(): boolean {
|
|
13443
14783
|
if (this.#abortUnwind) return false;
|
|
14784
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) return false;
|
|
13444
14785
|
if (this.agent.state.isStreaming) return false;
|
|
13445
14786
|
if (this.isRetrying) return false;
|
|
13446
14787
|
if (this.isCompacting) return false;
|
|
@@ -13458,6 +14799,7 @@ export class AgentSession {
|
|
|
13458
14799
|
*/
|
|
13459
14800
|
#canAutoContinueForSteer(): boolean {
|
|
13460
14801
|
if (this.#abortUnwind) return false;
|
|
14802
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) return false;
|
|
13461
14803
|
if (this.agent.state.isStreaming) return false;
|
|
13462
14804
|
if (this.isRetrying) return false;
|
|
13463
14805
|
const messages = this.agent.state.messages;
|
|
@@ -13473,11 +14815,13 @@ export class AgentSession {
|
|
|
13473
14815
|
}
|
|
13474
14816
|
|
|
13475
14817
|
queueDeferredMessage(message: CustomMessage): void {
|
|
13476
|
-
this.#
|
|
14818
|
+
this.#assertExternalSessionIngress({ allowCancelAndSubmit: true });
|
|
14819
|
+
this.#queueHiddenNextTurnMessage(message, true, "turn", true);
|
|
13477
14820
|
}
|
|
13478
14821
|
|
|
13479
14822
|
queueDeferredMessageForTests(message: CustomMessage, triggerTurn = true): void {
|
|
13480
|
-
this.#
|
|
14823
|
+
this.#assertExternalSessionIngress({ allowCancelAndSubmit: true });
|
|
14824
|
+
this.#queueHiddenNextTurnMessage(message, triggerTurn, "turn", true);
|
|
13481
14825
|
}
|
|
13482
14826
|
|
|
13483
14827
|
/** Read-only test seam for the hidden next-turn context queue. */
|
|
@@ -13494,11 +14838,12 @@ export class AgentSession {
|
|
|
13494
14838
|
message: CustomMessage,
|
|
13495
14839
|
triggerTurn: boolean,
|
|
13496
14840
|
origin: "turn" | "external" = "external",
|
|
14841
|
+
allowCancelAndSubmit = false,
|
|
13497
14842
|
): void {
|
|
13498
14843
|
// A hidden next-turn message queued during a handoff transition would be
|
|
13499
14844
|
// dropped when the successor clears predecessor queues; reject it as busy so
|
|
13500
14845
|
// the caller can retry against the settled session.
|
|
13501
|
-
this.#
|
|
14846
|
+
this.#assertExternalSessionIngress({ allowCancelAndSubmit });
|
|
13502
14847
|
this.#pendingNextTurnMessages.push({ message, origin });
|
|
13503
14848
|
if (!triggerTurn) return;
|
|
13504
14849
|
const generation = this.#promptGeneration;
|
|
@@ -13662,7 +15007,7 @@ export class AgentSession {
|
|
|
13662
15007
|
* monitor's registration, so removing it after an intermediate notification
|
|
13663
15008
|
* would let a later scope:"owned" abort miss the monitor and make subsequent
|
|
13664
15009
|
* notifications lose their envelope (review thread P1). */
|
|
13665
|
-
#settleDeliveredOwnedRegistrations(messages: AgentMessage[]): void {
|
|
15010
|
+
#settleDeliveredOwnedRegistrations(messages: readonly AgentMessage[]): void {
|
|
13666
15011
|
// Use the SESSION-OWNED manager first: the process-global instance is
|
|
13667
15012
|
// the last-created session, so checking this session's delivery against
|
|
13668
15013
|
// another session's manager (which lacks the same local job id) would
|
|
@@ -13771,6 +15116,7 @@ export class AgentSession {
|
|
|
13771
15116
|
},
|
|
13772
15117
|
): Promise<void> {
|
|
13773
15118
|
this.#assertRecoveryHydrationPromoted();
|
|
15119
|
+
this.#assertExternalSessionIngress();
|
|
13774
15120
|
const appMessage: CustomMessage<T> = {
|
|
13775
15121
|
role: "custom",
|
|
13776
15122
|
customType: message.customType,
|
|
@@ -13817,7 +15163,11 @@ export class AgentSession {
|
|
|
13817
15163
|
this.#settleDeliveredOwnedRegistrations([appMessage]);
|
|
13818
15164
|
return;
|
|
13819
15165
|
}
|
|
13820
|
-
this
|
|
15166
|
+
await this.#queueFollowUpAfterReservation(appMessage, this.#getCustomMessageTextContent(appMessage), {
|
|
15167
|
+
forceOneAtATime: sequential !== undefined,
|
|
15168
|
+
createDisplayEntry: false,
|
|
15169
|
+
trackExternalFollowUp: false,
|
|
15170
|
+
});
|
|
13821
15171
|
if (this.#abortUnwind && !sequential) this.#abortUnwindSteerFallbacks.push(appMessage);
|
|
13822
15172
|
// The chip now describes follow-up work: keep its mode and identity
|
|
13823
15173
|
// aligned with where the executable message actually landed.
|
|
@@ -13843,10 +15193,11 @@ export class AgentSession {
|
|
|
13843
15193
|
return;
|
|
13844
15194
|
}
|
|
13845
15195
|
|
|
13846
|
-
this
|
|
13847
|
-
|
|
13848
|
-
|
|
13849
|
-
|
|
15196
|
+
await this.#queueFollowUpAfterReservation(appMessage, this.#getCustomMessageTextContent(appMessage), {
|
|
15197
|
+
forceOneAtATime: options?.followUpQueuePolicy === "sequential",
|
|
15198
|
+
createDisplayEntry: false,
|
|
15199
|
+
trackExternalFollowUp: false,
|
|
15200
|
+
});
|
|
13850
15201
|
this.#bindCustomDisplayEntry(appMessage, "followUp");
|
|
13851
15202
|
// The session can report streaming while no agent loop owns the queue
|
|
13852
15203
|
// (post-prompt unwind): without a scheduled delivery a cron/monitor
|
|
@@ -13952,13 +15303,22 @@ export class AgentSession {
|
|
|
13952
15303
|
purgeQueuedCustomMessages(predicate: (message: CustomMessage) => boolean): PurgeQueuedCustomMessagesResult {
|
|
13953
15304
|
const isMatch = (m: AgentMessage): boolean => m.role === "custom" && predicate(m as CustomMessage);
|
|
13954
15305
|
const removedTags = new Set<string>();
|
|
13955
|
-
|
|
15306
|
+
const steeringBefore = this.agent.snapshotSteering();
|
|
15307
|
+
const followUpBefore = this.agent.snapshotFollowUp();
|
|
15308
|
+
const deferredBefore = [...this.#deferredSdkFollowUps];
|
|
15309
|
+
for (const m of [...steeringBefore, ...followUpBefore, ...deferredBefore]) {
|
|
13956
15310
|
if (isMatch(m)) {
|
|
13957
15311
|
const tag = readPendingDisplayTag((m as CustomMessage).details);
|
|
13958
15312
|
if (tag) removedTags.add(tag);
|
|
13959
15313
|
}
|
|
13960
15314
|
}
|
|
13961
15315
|
const agentRemoved = this.agent.removeQueuedMessages(isMatch);
|
|
15316
|
+
const removedAgentMessages = [...steeringBefore, ...followUpBefore].filter(isMatch);
|
|
15317
|
+
const removedDeferred = deferredBefore.filter(isMatch);
|
|
15318
|
+
if (removedDeferred.length > 0) {
|
|
15319
|
+
this.#deferredSdkFollowUps = this.#deferredSdkFollowUps.filter(message => !isMatch(message));
|
|
15320
|
+
for (const message of removedDeferred) this.#forgetDeferredFollowUpMetadata(message);
|
|
15321
|
+
}
|
|
13962
15322
|
const beforeNext = this.#pendingNextTurnMessages.length;
|
|
13963
15323
|
for (const entry of this.#pendingNextTurnMessages) {
|
|
13964
15324
|
if (predicate(entry.message)) {
|
|
@@ -13968,6 +15328,7 @@ export class AgentSession {
|
|
|
13968
15328
|
}
|
|
13969
15329
|
this.#pendingNextTurnMessages = this.#pendingNextTurnMessages.filter(entry => !predicate(entry.message));
|
|
13970
15330
|
const pendingNextTurn = beforeNext - this.#pendingNextTurnMessages.length;
|
|
15331
|
+
this.#fireQueuedRemovalHooks([...removedAgentMessages, ...removedDeferred]);
|
|
13971
15332
|
let displaySteering = 0;
|
|
13972
15333
|
let displayFollowUp = 0;
|
|
13973
15334
|
if (removedTags.size > 0) {
|
|
@@ -13978,6 +15339,7 @@ export class AgentSession {
|
|
|
13978
15339
|
this.#followUpMessages = this.#followUpMessages.filter(e => !(e.tag && removedTags.has(e.tag)));
|
|
13979
15340
|
displayFollowUp = beforeF - this.#followUpMessages.length;
|
|
13980
15341
|
}
|
|
15342
|
+
if (agentRemoved.total > 0 || removedDeferred.length > 0) this.#releaseDeferredSdkFollowUps();
|
|
13981
15343
|
return {
|
|
13982
15344
|
agentSteering: agentRemoved.steering,
|
|
13983
15345
|
agentFollowUp: agentRemoved.followUp,
|
|
@@ -13997,26 +15359,45 @@ export class AgentSession {
|
|
|
13997
15359
|
*/
|
|
13998
15360
|
async sendUserMessage(
|
|
13999
15361
|
content: string | (TextContent | ImageContent)[],
|
|
14000
|
-
options?:
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
14004
|
-
|
|
14005
|
-
onPreflightAcceptCommit?: () => void | Promise<void>;
|
|
14006
|
-
/** Fired when a queued submission (steering or follow-up) is promoted to its own run (SDK ownership correlation). */
|
|
14007
|
-
onQueuedPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
|
|
14008
|
-
/** Internal dispatch disposition used before actual queue consumption. */
|
|
14009
|
-
onDispatchDisposition?: (promotion: { startsOwnRun: boolean }) => void;
|
|
14010
|
-
preflightSignal?: AbortSignal;
|
|
14011
|
-
sdkRunCapability?: unknown;
|
|
14012
|
-
},
|
|
15362
|
+
options?: SendUserMessageOptions,
|
|
15363
|
+
): Promise<void>;
|
|
15364
|
+
async sendUserMessage(
|
|
15365
|
+
content: string | (TextContent | ImageContent)[],
|
|
15366
|
+
options?: SendUserMessageOptions,
|
|
14013
15367
|
): Promise<void> {
|
|
15368
|
+
assertLegacySendUserMessageOptions(options);
|
|
15369
|
+
await this.#sendUserMessage(content, options);
|
|
15370
|
+
}
|
|
15371
|
+
|
|
15372
|
+
/**
|
|
15373
|
+
* Queue one steer or follow-up and return a stable lifecycle handle for that
|
|
15374
|
+
* exact submission. Unlike `sendUserMessage`, this method resolves when the
|
|
15375
|
+
* queue admission receipt is available; use the returned promises to await
|
|
15376
|
+
* execution and the owning terminal boundary.
|
|
15377
|
+
*/
|
|
15378
|
+
async submitUserMessage(
|
|
15379
|
+
content: string | (TextContent | ImageContent)[],
|
|
15380
|
+
options: TrackedSendUserMessageOptions,
|
|
15381
|
+
): Promise<QueuedInputSubmission> {
|
|
15382
|
+
assertTrackedSendUserMessageOptions(options);
|
|
15383
|
+
const submission = await this.#sendUserMessage(content, options);
|
|
15384
|
+
if (submission === undefined) throw new Error("Tracked user message did not produce a submission handle.");
|
|
15385
|
+
return submission;
|
|
15386
|
+
}
|
|
15387
|
+
|
|
15388
|
+
async #sendUserMessage(
|
|
15389
|
+
content: string | (TextContent | ImageContent)[],
|
|
15390
|
+
options?: SendUserMessageDispatchOptions,
|
|
15391
|
+
): Promise<undefined | QueuedInputSubmission> {
|
|
15392
|
+
assertQueuedInputQueuePolicy(options?.queuePolicy);
|
|
14014
15393
|
const sdkRunToken = readSdkRunCapability(options?.sdkRunCapability);
|
|
15394
|
+
const submissionTransitionGeneration = this.#coordinatorPersistGeneration;
|
|
14015
15395
|
const internalOptions = options ? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) } : undefined;
|
|
14016
15396
|
this.#assertRecoveryHydrationPromoted();
|
|
15397
|
+
const trackedSuccessorAdmission = this.#hasCommittedSuccessorTrackedAdmission(options);
|
|
15398
|
+
this.#assertExternalSessionIngress({ allowTrackedSuccessor: trackedSuccessorAdmission });
|
|
14017
15399
|
const owner = this.#sessionAdmissionContext.getStore();
|
|
14018
|
-
if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
|
|
14019
|
-
this.#assertSessionAdmissionOpen();
|
|
15400
|
+
if (owner && !owner.released && !trackedSuccessorAdmission) throw this.#sessionAdmissionBusyError();
|
|
14020
15401
|
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
14021
15402
|
if (typeof content !== "string" && !Array.isArray(content)) {
|
|
14022
15403
|
throw Object.assign(new Error("sendUserMessage requires string or content-array content."), {
|
|
@@ -14054,6 +15435,16 @@ export class AgentSession {
|
|
|
14054
15435
|
images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
|
|
14055
15436
|
if (text.trim().length === 0 && !hasUsableImage)
|
|
14056
15437
|
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
15438
|
+
if (options?.trackSubmission === true && options.deliverAs === undefined)
|
|
15439
|
+
throw Object.assign(new Error("trackSubmission requires deliverAs: steer or followUp."), {
|
|
15440
|
+
code: "invalid_input",
|
|
15441
|
+
});
|
|
15442
|
+
let trackedQueuedInput: TrackedQueuedInput | undefined;
|
|
15443
|
+
let onTrackedQueued: ((message: AgentMessage, cancelQueued: () => boolean) => void) | undefined;
|
|
15444
|
+
const onQueuedPromoted = (promotion: { startsOwnRun?: boolean; removed?: boolean }): void => {
|
|
15445
|
+
if (trackedQueuedInput) this.#handleTrackedQueuedInputPromotion(trackedQueuedInput, promotion);
|
|
15446
|
+
options?.onQueuedPromoted?.(promotion);
|
|
15447
|
+
};
|
|
14057
15448
|
|
|
14058
15449
|
let admissionSignal = options?.preflightSignal
|
|
14059
15450
|
? AbortSignal.any([this.#promptPreflightAbortController.signal, options.preflightSignal])
|
|
@@ -14120,7 +15511,19 @@ export class AgentSession {
|
|
|
14120
15511
|
await awaitPromptInvocationPreflight(this.#selectionFenceTail, admissionSignal);
|
|
14121
15512
|
}
|
|
14122
15513
|
const assertPreflightStillOpen = () => {
|
|
14123
|
-
this.#
|
|
15514
|
+
this.#assertExternalSessionIngress({ allowTrackedSuccessor: trackedSuccessorAdmission });
|
|
15515
|
+
if (
|
|
15516
|
+
(this.#sessionTransitionKind !== undefined ||
|
|
15517
|
+
this.#coordinatorPersistGeneration !== submissionTransitionGeneration) &&
|
|
15518
|
+
!this.#hasCommittedSuccessorTrackedAdmission(options)
|
|
15519
|
+
) {
|
|
15520
|
+
throw Object.assign(
|
|
15521
|
+
new AgentBusyError(
|
|
15522
|
+
`Cannot submit queued input while ${this.#sessionTransitionKind ?? "a session state transition"} is in progress.`,
|
|
15523
|
+
),
|
|
15524
|
+
{ code: "busy" },
|
|
15525
|
+
);
|
|
15526
|
+
}
|
|
14124
15527
|
if (
|
|
14125
15528
|
options?.preflightSignal?.aborted ||
|
|
14126
15529
|
this.#promptPreflightCancellationGeneration !== preflightCancellationGeneration
|
|
@@ -14139,33 +15542,69 @@ export class AgentSession {
|
|
|
14139
15542
|
followUpReservationEpoch,
|
|
14140
15543
|
options?.preflightSignal,
|
|
14141
15544
|
admissionSignal,
|
|
15545
|
+
submissionTransitionGeneration,
|
|
14142
15546
|
);
|
|
14143
15547
|
}
|
|
15548
|
+
assertPreflightStillOpen();
|
|
14144
15549
|
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
14145
15550
|
assertPreflightStillOpen();
|
|
15551
|
+
if (options?.trackSubmission === true) {
|
|
15552
|
+
trackedQueuedInput = this.#createTrackedQueuedInput(deliverAs, options.queuePolicy ?? "respect-mode");
|
|
15553
|
+
onTrackedQueued = (message: AgentMessage, cancelQueued: () => boolean) =>
|
|
15554
|
+
this.#admitTrackedQueuedInput(trackedQueuedInput!, message, cancelQueued);
|
|
15555
|
+
}
|
|
14146
15556
|
const queuedFollowUp = await this.#queueFollowUp(text, images, {
|
|
14147
15557
|
claimsGenuineUserIntent: true,
|
|
14148
|
-
forceOneAtATime: Boolean(options?.
|
|
14149
|
-
onPromoted:
|
|
15558
|
+
forceOneAtATime: Boolean(options?.queuedAtDispatch || options?.queuePolicy === "sequential"),
|
|
15559
|
+
onPromoted: onQueuedPromoted,
|
|
14150
15560
|
sdkRunToken: internalOptions?.sdkRunToken,
|
|
15561
|
+
onQueuedAfterAdmission: onTrackedQueued,
|
|
15562
|
+
allowDuringSessionTransition: this.#hasCommittedSuccessorTrackedAdmission(options),
|
|
14151
15563
|
});
|
|
14152
|
-
|
|
14153
|
-
|
|
14154
|
-
|
|
14155
|
-
|
|
14156
|
-
|
|
15564
|
+
this.#bindPreflightAbortCancellation(options?.preflightSignal, queuedFollowUp, trackedQueuedInput);
|
|
15565
|
+
try {
|
|
15566
|
+
assertPreflightStillOpen();
|
|
15567
|
+
options?.onPreflightAccepted?.();
|
|
15568
|
+
assertPreflightStillOpen();
|
|
15569
|
+
} catch (error) {
|
|
15570
|
+
if (trackedQueuedInput) this.#settleTrackedAdmissionCallbackFailure(trackedQueuedInput, queuedFollowUp);
|
|
15571
|
+
else queuedFollowUp.cancel();
|
|
15572
|
+
throw error;
|
|
15573
|
+
}
|
|
15574
|
+
return trackedQueuedInput?.submission;
|
|
14157
15575
|
}
|
|
14158
15576
|
if (deliverAs === "steer") {
|
|
14159
15577
|
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
14160
15578
|
assertPreflightStillOpen();
|
|
14161
|
-
|
|
15579
|
+
if (options?.trackSubmission === true) {
|
|
15580
|
+
trackedQueuedInput = this.#createTrackedQueuedInput(deliverAs, options.queuePolicy ?? "respect-mode");
|
|
15581
|
+
onTrackedQueued = (message: AgentMessage, cancelQueued: () => boolean) =>
|
|
15582
|
+
this.#admitTrackedQueuedInput(trackedQueuedInput!, message, cancelQueued);
|
|
15583
|
+
}
|
|
15584
|
+
const queuedSteer = await this.#queueSteer(text, images, {
|
|
14162
15585
|
claimsGenuineUserIntent: true,
|
|
14163
|
-
onPromoted:
|
|
15586
|
+
onPromoted: onQueuedPromoted,
|
|
14164
15587
|
external: true,
|
|
14165
15588
|
sdkRunToken: internalOptions?.sdkRunToken,
|
|
15589
|
+
forceOneAtATime: options?.queuePolicy === "sequential",
|
|
15590
|
+
onQueuedAfterAdmission: onTrackedQueued,
|
|
15591
|
+
allowDuringSessionTransition: this.#hasCommittedSuccessorTrackedAdmission(options),
|
|
14166
15592
|
});
|
|
14167
|
-
|
|
14168
|
-
|
|
15593
|
+
// An explicit steer is admitted to the live loop immediately, so an
|
|
15594
|
+
// aborted invocation must cancel the exact submission it admitted:
|
|
15595
|
+
// otherwise the steer stays executable after its caller gave up
|
|
15596
|
+
// (exact-head review P1).
|
|
15597
|
+
this.#bindPreflightAbortCancellation(options?.preflightSignal, queuedSteer, trackedQueuedInput);
|
|
15598
|
+
try {
|
|
15599
|
+
assertPreflightStillOpen();
|
|
15600
|
+
options?.onPreflightAccepted?.();
|
|
15601
|
+
assertPreflightStillOpen();
|
|
15602
|
+
} catch (error) {
|
|
15603
|
+
if (trackedQueuedInput) this.#settleTrackedAdmissionCallbackFailure(trackedQueuedInput, queuedSteer);
|
|
15604
|
+
else queuedSteer.cancel();
|
|
15605
|
+
throw error;
|
|
15606
|
+
}
|
|
15607
|
+
return trackedQueuedInput?.submission;
|
|
14169
15608
|
}
|
|
14170
15609
|
|
|
14171
15610
|
// No explicit delivery mode: only a live stream makes prompt() throw
|
|
@@ -14225,6 +15664,13 @@ export class AgentSession {
|
|
|
14225
15664
|
preflightSignal: options?.preflightSignal,
|
|
14226
15665
|
} as InternalPromptOptions);
|
|
14227
15666
|
} finally {
|
|
15667
|
+
// A preflight fence or queue admission error can occur after the tracked
|
|
15668
|
+
// state is allocated but before a queue callback binds its exact message.
|
|
15669
|
+
// No handle escaped in that case, so discard the unreachable state rather
|
|
15670
|
+
// than retaining an unresolved submission forever.
|
|
15671
|
+
if (trackedQueuedInput?.message === undefined) {
|
|
15672
|
+
if (trackedQueuedInput) this.#settleTrackedQueuedInputRemoved(trackedQueuedInput, "removed");
|
|
15673
|
+
}
|
|
14228
15674
|
releaseFollowUpReservation();
|
|
14229
15675
|
}
|
|
14230
15676
|
}
|
|
@@ -14346,15 +15792,36 @@ export class AgentSession {
|
|
|
14346
15792
|
const deferredIndex = this.#deferredSdkFollowUps.indexOf(removedMessage);
|
|
14347
15793
|
if (deferredIndex !== -1) {
|
|
14348
15794
|
this.#deferredSdkFollowUps.splice(deferredIndex, 1);
|
|
15795
|
+
this.#forgetDeferredFollowUpMetadata(removedMessage);
|
|
14349
15796
|
} else {
|
|
14350
15797
|
this.agent.removeQueuedMessages(candidate => candidate === removedMessage);
|
|
14351
15798
|
}
|
|
14352
15799
|
} else if (index >= 0) {
|
|
14353
|
-
// Legacy entries without an identity link
|
|
14354
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
15800
|
+
// Legacy entries without an identity link use a content/tag lookup. The
|
|
15801
|
+
// visible index is not an Agent-queue index because hidden messages may
|
|
15802
|
+
// occupy executable slots ahead of the row.
|
|
15803
|
+
const executable = resolvedMode === "steer" ? this.agent.snapshotSteering() : this.agent.snapshotFollowUp();
|
|
15804
|
+
const legacyIndex = executable.findIndex(candidate => {
|
|
15805
|
+
if (entry?.tag !== undefined && candidate.role === "custom") {
|
|
15806
|
+
return readPendingDisplayTag(candidate.details) === entry.tag;
|
|
15807
|
+
}
|
|
15808
|
+
if (entry?.text === undefined) return false;
|
|
15809
|
+
if (candidate.role === "user") return this.#getUserMessageText(candidate) === entry.text;
|
|
15810
|
+
if (candidate.role === "custom") return this.#getCustomMessageTextContent(candidate) === entry.text;
|
|
15811
|
+
return false;
|
|
15812
|
+
});
|
|
15813
|
+
if (legacyIndex !== -1) {
|
|
15814
|
+
removedMessage =
|
|
15815
|
+
resolvedMode === "steer"
|
|
15816
|
+
? this.agent.removeSteerAt(legacyIndex)
|
|
15817
|
+
: this.agent.removeFollowUpAt(legacyIndex);
|
|
15818
|
+
}
|
|
15819
|
+
}
|
|
15820
|
+
// Removing a deferred SDK follow-up — or the live follow-up it was waiting
|
|
15821
|
+
// behind — must release the NEXT deferred submission, exactly like the
|
|
15822
|
+
// cancellation path does, or it stays accepted with no execution or
|
|
15823
|
+
// terminal settlement (exact-head review P1).
|
|
15824
|
+
if (removedMessage !== undefined) this.#releaseDeferredSdkFollowUps();
|
|
14358
15825
|
// The removed message left its queue WITHOUT consumption: its accepted SDK
|
|
14359
15826
|
// submission must terminalize boundedly, not strand accepted forever
|
|
14360
15827
|
// (#4668 review P1).
|
|
@@ -14705,11 +16172,13 @@ export class AgentSession {
|
|
|
14705
16172
|
| undefined;
|
|
14706
16173
|
|
|
14707
16174
|
if (this.#extensionRunner && savedCompactionEntry) {
|
|
14708
|
-
await this.#
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
16175
|
+
await this.#runCommittedSuccessorHook(() =>
|
|
16176
|
+
this.#extensionRunner!.emit({
|
|
16177
|
+
type: "session_compact",
|
|
16178
|
+
compactionEntry: savedCompactionEntry,
|
|
16179
|
+
fromExtension: fromExtension ?? false,
|
|
16180
|
+
}),
|
|
16181
|
+
);
|
|
14713
16182
|
}
|
|
14714
16183
|
|
|
14715
16184
|
return savedCompactionEntry;
|
|
@@ -14797,6 +16266,7 @@ export class AgentSession {
|
|
|
14797
16266
|
this.#promptPreflightCancellationGeneration++;
|
|
14798
16267
|
this.#promptPreflightAbortController.abort();
|
|
14799
16268
|
this.#promptPreflightAbortController = new AbortController();
|
|
16269
|
+
this.#dropQueuedTtsrFollowUps();
|
|
14800
16270
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
14801
16271
|
if (options?.preserveCompaction) this.#autoCompactionAbortController?.abort();
|
|
14802
16272
|
else this.abortCompaction();
|
|
@@ -14872,6 +16342,7 @@ export class AgentSession {
|
|
|
14872
16342
|
this.#promptPreflightCancellationGeneration++;
|
|
14873
16343
|
this.#promptPreflightAbortController.abort();
|
|
14874
16344
|
this.#promptPreflightAbortController = new AbortController();
|
|
16345
|
+
this.#dropQueuedTtsrFollowUps();
|
|
14875
16346
|
this.#drainTerminalOwnedYieldEntries();
|
|
14876
16347
|
if (options?.preserveCompaction !== true) this.abortCompaction();
|
|
14877
16348
|
const overlappingPostPromptDrain = this.#cancelPostPromptTasks();
|
|
@@ -15019,6 +16490,7 @@ export class AgentSession {
|
|
|
15019
16490
|
this.#promptPreflightCancellationGeneration++;
|
|
15020
16491
|
this.#promptPreflightAbortController.abort();
|
|
15021
16492
|
this.#promptPreflightAbortController = new AbortController();
|
|
16493
|
+
this.#dropQueuedTtsrFollowUps();
|
|
15022
16494
|
}
|
|
15023
16495
|
/**
|
|
15024
16496
|
* Capture the steering admission snapshot at abort ADMISSION: the host
|
|
@@ -15105,6 +16577,31 @@ export class AgentSession {
|
|
|
15105
16577
|
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
15106
16578
|
return AsyncJobManager.endpointIdOf(manager) ?? this.sessionManager.getSessionId() ?? "local";
|
|
15107
16579
|
}
|
|
16580
|
+
/**
|
|
16581
|
+
* Tool call ids the run resource ledger currently holds for `handle` (#5637).
|
|
16582
|
+
*
|
|
16583
|
+
* The authoritative answer to "is a tool running?": AgentLoop reserves the
|
|
16584
|
+
* `kind: "tool"` lease SYNCHRONOUSLY inside its dispatch loop, before the
|
|
16585
|
+
* tool's `execute` is invoked, and the lease settles when the call really
|
|
16586
|
+
* ends — whereas `tool_execution_start` only reaches an extension after an
|
|
16587
|
+
* asynchronous fanout. Strictly read-only: it takes a snapshot and never
|
|
16588
|
+
* claims, reserves, seals or quarantines. An unknown run reads as empty,
|
|
16589
|
+
* matching `RunResourceLedger.pending`.
|
|
16590
|
+
*/
|
|
16591
|
+
pendingToolExecutions(handle: string): readonly string[] {
|
|
16592
|
+
const ids = new Set<string>();
|
|
16593
|
+
for (const entry of this.agent.resourceLedger.pending(handle)) {
|
|
16594
|
+
if (entry.kind !== "tool") continue;
|
|
16595
|
+
// AgentLoop labels a tool reservation `<toolName>:<toolCallId>`, and
|
|
16596
|
+
// registers the reservation and its tracked task under the SAME label, so
|
|
16597
|
+
// the set also collapses that pair to one id. A label without a separator
|
|
16598
|
+
// is passed through rather than dropped: over-reporting a running tool
|
|
16599
|
+
// only costs a bounded wait, under-reporting kills it mid-write.
|
|
16600
|
+
const separator = entry.label.indexOf(":");
|
|
16601
|
+
ids.add(separator < 0 ? entry.label : entry.label.slice(separator + 1));
|
|
16602
|
+
}
|
|
16603
|
+
return [...ids];
|
|
16604
|
+
}
|
|
15108
16605
|
async abortPromptAndWait(
|
|
15109
16606
|
handle: string,
|
|
15110
16607
|
options: {
|
|
@@ -15237,16 +16734,21 @@ export class AgentSession {
|
|
|
15237
16734
|
// Purged ordinary follow-ups leave without consumption: terminalize
|
|
15238
16735
|
// their accepted SDK submissions boundedly (#4668 review P1).
|
|
15239
16736
|
this.#fireQueuedRemovalHooks(followUpBeforePurge.filter(purgedByAbort));
|
|
15240
|
-
// Mirror the follow-up purge in the display list
|
|
15241
|
-
//
|
|
15242
|
-
|
|
15243
|
-
const
|
|
15244
|
-
|
|
15245
|
-
|
|
15246
|
-
|
|
15247
|
-
|
|
15248
|
-
|
|
15249
|
-
|
|
16737
|
+
// Mirror the follow-up purge in the display list by bound message identity,
|
|
16738
|
+
// not by Agent-array position. Deferred SDK follow-ups and tag-only custom
|
|
16739
|
+
// entries are not represented one-for-one in followUpBeforePurge.
|
|
16740
|
+
const keptFollowUps = new Set([...this.agent.snapshotFollowUp(), ...this.#deferredSdkFollowUps]);
|
|
16741
|
+
const keptCustomTags = new Set(
|
|
16742
|
+
[...keptFollowUps].flatMap(message => {
|
|
16743
|
+
if (message.role !== "custom") return [];
|
|
16744
|
+
const tag = readPendingDisplayTag(message.details);
|
|
16745
|
+
return tag === undefined ? [] : [tag];
|
|
16746
|
+
}),
|
|
16747
|
+
);
|
|
16748
|
+
this.#followUpMessages = this.#followUpMessages.filter(entry => {
|
|
16749
|
+
if (entry.message !== undefined) return keptFollowUps.has(entry.message);
|
|
16750
|
+
return entry.tag !== undefined && keptCustomTags.has(entry.tag);
|
|
16751
|
+
});
|
|
15250
16752
|
}
|
|
15251
16753
|
// Do NOT advance the attempt epoch here: the terminal scope must stay
|
|
15252
16754
|
// keyed to the aborted epoch so #isTurnContinuationBlocked (which
|
|
@@ -15260,6 +16762,7 @@ export class AgentSession {
|
|
|
15260
16762
|
this.#promptPreflightCancellationGeneration++;
|
|
15261
16763
|
this.#promptPreflightAbortController.abort();
|
|
15262
16764
|
this.#promptPreflightAbortController = new AbortController();
|
|
16765
|
+
this.#dropQueuedTtsrFollowUps();
|
|
15263
16766
|
}
|
|
15264
16767
|
const aborted = this.#runCancellationDomains.abort(handle);
|
|
15265
16768
|
if (!aborted.ok) {
|
|
@@ -15376,6 +16879,7 @@ export class AgentSession {
|
|
|
15376
16879
|
const queueSnapshot = this.agent.snapshotQueues();
|
|
15377
16880
|
const steeringDisplaySnapshot = [...this.#steeringMessages];
|
|
15378
16881
|
const followUpDisplaySnapshot = [...this.#followUpMessages];
|
|
16882
|
+
const deferredFollowUpSnapshot = [...this.#deferredSdkFollowUps];
|
|
15379
16883
|
const pendingNextTurnSnapshot = [...this.#pendingNextTurnMessages];
|
|
15380
16884
|
const additionsSince = <T>(current: readonly T[], baseline: readonly T[]): T[] => {
|
|
15381
16885
|
const remaining = new Map<T, number>();
|
|
@@ -15387,7 +16891,7 @@ export class AgentSession {
|
|
|
15387
16891
|
return false;
|
|
15388
16892
|
});
|
|
15389
16893
|
};
|
|
15390
|
-
|
|
16894
|
+
let selected = (() => {
|
|
15391
16895
|
if (options?.queuedEntryId === undefined) return undefined;
|
|
15392
16896
|
const [mode, sequenceText] = options.queuedEntryId.split(":");
|
|
15393
16897
|
const sequence = Number(sequenceText);
|
|
@@ -15395,21 +16899,63 @@ export class AgentSession {
|
|
|
15395
16899
|
const displays = mode === "steer" ? steeringDisplaySnapshot : followUpDisplaySnapshot;
|
|
15396
16900
|
const index = displays.findIndex(entry => entry.sequence === sequence);
|
|
15397
16901
|
if (index === -1) return undefined;
|
|
16902
|
+
const display = displays[index]!;
|
|
16903
|
+
const queue = mode === "steer" ? queueSnapshot.steering : queueSnapshot.followUp;
|
|
16904
|
+
const message =
|
|
16905
|
+
display.message ??
|
|
16906
|
+
queue.find(candidate => {
|
|
16907
|
+
if (candidate.role !== "custom" || display.tag === undefined) return false;
|
|
16908
|
+
return readPendingDisplayTag(candidate.details) === display.tag;
|
|
16909
|
+
});
|
|
16910
|
+
const deferred =
|
|
16911
|
+
mode === "followUp" && message !== undefined && deferredFollowUpSnapshot.includes(message);
|
|
15398
16912
|
return {
|
|
15399
|
-
display
|
|
15400
|
-
message
|
|
16913
|
+
display,
|
|
16914
|
+
message,
|
|
15401
16915
|
mode,
|
|
15402
|
-
|
|
16916
|
+
deferred,
|
|
15403
16917
|
};
|
|
15404
16918
|
})();
|
|
16919
|
+
let cancelledSelection: typeof selected;
|
|
16920
|
+
const selectedPromotionHook = selected?.message
|
|
16921
|
+
? (this.#followUpPromotionHooks.get(selected.message) ?? this.#steerPromotionHooks.get(selected.message))
|
|
16922
|
+
: undefined;
|
|
16923
|
+
if (selected?.deferred && selected.message !== undefined) {
|
|
16924
|
+
const selectedDeferredIndex = this.#deferredSdkFollowUps.indexOf(selected.message);
|
|
16925
|
+
if (selectedDeferredIndex !== -1) {
|
|
16926
|
+
this.#deferredSdkFollowUps.splice(selectedDeferredIndex, 1);
|
|
16927
|
+
}
|
|
16928
|
+
}
|
|
16929
|
+
const revalidateSelected = (): void => {
|
|
16930
|
+
if (selected === undefined || selected.message === undefined) return;
|
|
16931
|
+
const stillDisplayed =
|
|
16932
|
+
this.#steeringMessages.includes(selected.display) ||
|
|
16933
|
+
this.#followUpMessages.includes(selected.display);
|
|
16934
|
+
// An abort temporarily removes steering from Agent's live queue and
|
|
16935
|
+
// returns it in the agent_end disowned batch; the display mirror is the
|
|
16936
|
+
// stable ownership marker across that boundary. A successful external
|
|
16937
|
+
// cancellation removes the display entry as part of the same operation.
|
|
16938
|
+
if (!stillDisplayed) {
|
|
16939
|
+
cancelledSelection = selected;
|
|
16940
|
+
selected = undefined;
|
|
16941
|
+
}
|
|
16942
|
+
};
|
|
15405
16943
|
let runAccepted = false;
|
|
15406
16944
|
const restore = () => {
|
|
15407
16945
|
const queues = this.agent.snapshotQueues();
|
|
15408
16946
|
const queueBaseline = [...queueSnapshot.steering, ...queueSnapshot.followUp];
|
|
15409
16947
|
const displayBaseline = [...steeringDisplaySnapshot, ...followUpDisplaySnapshot];
|
|
16948
|
+
const cancelledMessage = cancelledSelection?.message;
|
|
16949
|
+
const cancelledDisplay = cancelledSelection?.display;
|
|
15410
16950
|
this.agent.restoreQueues({
|
|
15411
|
-
steering: [
|
|
15412
|
-
|
|
16951
|
+
steering: [
|
|
16952
|
+
...queueSnapshot.steering.filter(message => message !== cancelledMessage),
|
|
16953
|
+
...additionsSince(queues.steering, queueBaseline).filter(message => message !== cancelledMessage),
|
|
16954
|
+
],
|
|
16955
|
+
followUp: [
|
|
16956
|
+
...queueSnapshot.followUp.filter(message => message !== cancelledMessage),
|
|
16957
|
+
...additionsSince(queues.followUp, queueBaseline).filter(message => message !== cancelledMessage),
|
|
16958
|
+
],
|
|
15413
16959
|
});
|
|
15414
16960
|
this.#pendingNextTurnMessages = [
|
|
15415
16961
|
...pendingNextTurnSnapshot,
|
|
@@ -15418,13 +16964,23 @@ export class AgentSession {
|
|
|
15418
16964
|
this.#cancelAndSubmitPendingNextTurnDrained ? [] : pendingNextTurnSnapshot,
|
|
15419
16965
|
),
|
|
15420
16966
|
];
|
|
16967
|
+
this.#deferredSdkFollowUps = [
|
|
16968
|
+
...deferredFollowUpSnapshot.filter(message => message !== cancelledMessage),
|
|
16969
|
+
...additionsSince(this.#deferredSdkFollowUps, deferredFollowUpSnapshot).filter(
|
|
16970
|
+
message => message !== cancelledMessage,
|
|
16971
|
+
),
|
|
16972
|
+
];
|
|
15421
16973
|
this.#steeringMessages = [
|
|
15422
|
-
...steeringDisplaySnapshot,
|
|
15423
|
-
...additionsSince(this.#steeringMessages, displayBaseline)
|
|
16974
|
+
...steeringDisplaySnapshot.filter(entry => entry !== cancelledDisplay),
|
|
16975
|
+
...additionsSince(this.#steeringMessages, displayBaseline).filter(
|
|
16976
|
+
entry => entry !== cancelledDisplay,
|
|
16977
|
+
),
|
|
15424
16978
|
];
|
|
15425
16979
|
this.#followUpMessages = [
|
|
15426
|
-
...followUpDisplaySnapshot,
|
|
15427
|
-
...additionsSince(this.#followUpMessages, displayBaseline)
|
|
16980
|
+
...followUpDisplaySnapshot.filter(entry => entry !== cancelledDisplay),
|
|
16981
|
+
...additionsSince(this.#followUpMessages, displayBaseline).filter(
|
|
16982
|
+
entry => entry !== cancelledDisplay,
|
|
16983
|
+
),
|
|
15428
16984
|
];
|
|
15429
16985
|
};
|
|
15430
16986
|
try {
|
|
@@ -15436,6 +16992,7 @@ export class AgentSession {
|
|
|
15436
16992
|
? await this.#cancelAndSubmitAbortOutcomeProviderForTests()
|
|
15437
16993
|
: await this.#abortWithOutcome({ cause: "user_interrupt", timeoutMs: 5_000 });
|
|
15438
16994
|
this.#disownedSteeringDisposition = undefined;
|
|
16995
|
+
revalidateSelected();
|
|
15439
16996
|
if (outcome.kind !== "settled") {
|
|
15440
16997
|
restore();
|
|
15441
16998
|
if (outcome.kind === "error") {
|
|
@@ -15456,22 +17013,21 @@ export class AgentSession {
|
|
|
15456
17013
|
};
|
|
15457
17014
|
const steeringDisplaysDuringWindow = additionsSince(this.#steeringMessages, steeringDisplaySnapshot);
|
|
15458
17015
|
const followUpDisplaysDuringWindow = additionsSince(this.#followUpMessages, followUpDisplaySnapshot);
|
|
15459
|
-
const
|
|
15460
|
-
const
|
|
15461
|
-
|
|
15462
|
-
|
|
15463
|
-
|
|
15464
|
-
|
|
15465
|
-
|
|
15466
|
-
(_, index) => selected.mode !== "followUp" || index !== selected.index,
|
|
15467
|
-
),
|
|
15468
|
-
]
|
|
17016
|
+
const committedSelection = selected;
|
|
17017
|
+
const selectedMessage = committedSelection?.message;
|
|
17018
|
+
const reclassifiedSteering = committedSelection
|
|
17019
|
+
? queueSnapshot.steering.filter(message => message !== selectedMessage)
|
|
17020
|
+
: [];
|
|
17021
|
+
const heldFollowUp = committedSelection
|
|
17022
|
+
? [...reclassifiedSteering, ...queueSnapshot.followUp.filter(message => message !== selectedMessage)]
|
|
15469
17023
|
: [];
|
|
15470
17024
|
let heldQueueRestored = false;
|
|
15471
17025
|
const restoreHeldQueue = () => {
|
|
15472
|
-
if (!
|
|
17026
|
+
if (!committedSelection || heldQueueRestored) return;
|
|
15473
17027
|
heldQueueRestored = true;
|
|
15474
17028
|
const current = this.agent.snapshotQueues();
|
|
17029
|
+
this.#markSteeringAsFollowUpPolicy(reclassifiedSteering);
|
|
17030
|
+
this.#markTrackedFollowUpSequential(heldFollowUp);
|
|
15475
17031
|
this.agent.restoreQueues({
|
|
15476
17032
|
steering: current.steering,
|
|
15477
17033
|
followUp: [...heldFollowUp, ...current.followUp],
|
|
@@ -15481,25 +17037,49 @@ export class AgentSession {
|
|
|
15481
17037
|
// restored once the new run is accepted (below), so the Agent
|
|
15482
17038
|
// admits them into a live run instead of re-labelling them as
|
|
15483
17039
|
// follow-ups drained turn after turn. Follow-ups keep their queue.
|
|
15484
|
-
const
|
|
15485
|
-
const
|
|
17040
|
+
const cancelledMessage = cancelledSelection?.message;
|
|
17041
|
+
const cancelledDisplay = cancelledSelection?.display;
|
|
17042
|
+
const heldSteering = committedSelection
|
|
17043
|
+
? []
|
|
17044
|
+
: queueSnapshot.steering.filter(message => message !== cancelledMessage);
|
|
17045
|
+
const heldSteeringDisplays = committedSelection
|
|
17046
|
+
? []
|
|
17047
|
+
: steeringDisplaySnapshot.filter(entry => entry !== cancelledDisplay);
|
|
15486
17048
|
this.agent.restoreQueues({
|
|
15487
|
-
steering:
|
|
15488
|
-
|
|
15489
|
-
|
|
15490
|
-
|
|
17049
|
+
steering: queuedDuringWindow.steering.filter(
|
|
17050
|
+
message => message !== selectedMessage && message !== cancelledMessage,
|
|
17051
|
+
),
|
|
17052
|
+
followUp: committedSelection
|
|
17053
|
+
? queuedDuringWindow.followUp.filter(message => message !== selectedMessage)
|
|
17054
|
+
: [
|
|
17055
|
+
...queueSnapshot.followUp.filter(message => message !== cancelledMessage),
|
|
17056
|
+
...queuedDuringWindow.followUp.filter(message => message !== cancelledMessage),
|
|
17057
|
+
],
|
|
15491
17058
|
});
|
|
15492
17059
|
this.#steeringMessages = steeringDisplaysDuringWindow;
|
|
15493
17060
|
// With a selected entry the held (unselected) messages are re-queued
|
|
15494
17061
|
// as follow-ups by restoreHeldQueue once the new run is accepted;
|
|
15495
17062
|
// mirror them in the display in the same order.
|
|
15496
|
-
this.#followUpMessages =
|
|
17063
|
+
this.#followUpMessages = committedSelection
|
|
15497
17064
|
? [
|
|
15498
|
-
...steeringDisplaySnapshot.filter(entry => entry !==
|
|
15499
|
-
...followUpDisplaySnapshot.filter(entry => entry !==
|
|
17065
|
+
...steeringDisplaySnapshot.filter(entry => entry !== committedSelection.display),
|
|
17066
|
+
...followUpDisplaySnapshot.filter(entry => entry !== committedSelection.display),
|
|
15500
17067
|
...followUpDisplaysDuringWindow,
|
|
15501
17068
|
]
|
|
15502
|
-
: [
|
|
17069
|
+
: [
|
|
17070
|
+
...followUpDisplaySnapshot.filter(entry => entry !== cancelledDisplay),
|
|
17071
|
+
...followUpDisplaysDuringWindow.filter(entry => entry !== cancelledDisplay),
|
|
17072
|
+
];
|
|
17073
|
+
if (committedSelection?.deferred && selectedMessage !== undefined) {
|
|
17074
|
+
this.#deferredSdkFollowUps = this.#deferredSdkFollowUps.filter(
|
|
17075
|
+
message => message !== selectedMessage,
|
|
17076
|
+
);
|
|
17077
|
+
}
|
|
17078
|
+
if (selectedMessage !== undefined && selectedPromotionHook !== undefined) {
|
|
17079
|
+
if (committedSelection?.mode === "steer")
|
|
17080
|
+
this.#steerPromotionHooks.set(selectedMessage, selectedPromotionHook);
|
|
17081
|
+
else this.#followUpPromotionHooks.set(selectedMessage, selectedPromotionHook);
|
|
17082
|
+
}
|
|
15503
17083
|
const message = selectedMessage ?? {
|
|
15504
17084
|
role: "user" as const,
|
|
15505
17085
|
content: [{ type: "text" as const, text }],
|
|
@@ -15517,7 +17097,7 @@ export class AgentSession {
|
|
|
15517
17097
|
: text;
|
|
15518
17098
|
await this.refreshGjcSubskillTools();
|
|
15519
17099
|
if (message.role === "custom") await this.#syncSkillPromptActiveStateSafely(message, true);
|
|
15520
|
-
if (
|
|
17100
|
+
if (committedSelection) {
|
|
15521
17101
|
const displayTag = message.role === "custom" ? readPendingDisplayTag(message.details) : undefined;
|
|
15522
17102
|
if (displayTag) this.#displayDequeueAlreadyHandled = { role: "custom", tag: displayTag };
|
|
15523
17103
|
else if (message.role === "user")
|
|
@@ -15529,7 +17109,7 @@ export class AgentSession {
|
|
|
15529
17109
|
resetRetryReplaySafety: true,
|
|
15530
17110
|
sdkRunToken: selectedSdkRunToken,
|
|
15531
17111
|
skipInitialSteeringPoll: heldSteering.length > 0,
|
|
15532
|
-
onRunAccepted: () => {
|
|
17112
|
+
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
15533
17113
|
runAccepted = true;
|
|
15534
17114
|
if (heldSteering.length > 0) {
|
|
15535
17115
|
// Re-admit the aborted turn's steers into the replacement run the
|
|
@@ -15541,10 +17121,17 @@ export class AgentSession {
|
|
|
15541
17121
|
this.agent.restoreSteering(heldSteering);
|
|
15542
17122
|
this.#steeringMessages = [...heldSteeringDisplays, ...this.#steeringMessages];
|
|
15543
17123
|
}
|
|
15544
|
-
if (
|
|
15545
|
-
|
|
15546
|
-
this.#
|
|
15547
|
-
|
|
17124
|
+
if (committedSelection) {
|
|
17125
|
+
this.#fireQueuedPromotionHooks([message], { startsOwnRun: true });
|
|
17126
|
+
this.#settleTrackedOwnRunPromotions(handle);
|
|
17127
|
+
}
|
|
17128
|
+
if (committedSelection) {
|
|
17129
|
+
this.#steeringMessages = this.#steeringMessages.filter(
|
|
17130
|
+
entry => entry !== committedSelection.display,
|
|
17131
|
+
);
|
|
17132
|
+
this.#followUpMessages = this.#followUpMessages.filter(
|
|
17133
|
+
entry => entry !== committedSelection.display,
|
|
17134
|
+
);
|
|
15548
17135
|
}
|
|
15549
17136
|
},
|
|
15550
17137
|
});
|
|
@@ -15559,6 +17146,7 @@ export class AgentSession {
|
|
|
15559
17146
|
if (runAccepted) {
|
|
15560
17147
|
return { kind: "submitted" };
|
|
15561
17148
|
}
|
|
17149
|
+
revalidateSelected();
|
|
15562
17150
|
this.#displayDequeueAlreadyHandled = undefined;
|
|
15563
17151
|
restore();
|
|
15564
17152
|
logger.error("Cancel-and-submit prompt failed before run acceptance", { cause });
|
|
@@ -15659,17 +17247,23 @@ export class AgentSession {
|
|
|
15659
17247
|
const noLeasePreviousSessionIdentity = this.sessionManager.getSessionId();
|
|
15660
17248
|
const noLeasePreviousSessionFile = this.sessionManager.getSessionFile();
|
|
15661
17249
|
const prepared = await this.sessionManager.prepareNewSession(options);
|
|
17250
|
+
let exactRetirement: PreparedExactMcpRetirement | undefined;
|
|
15662
17251
|
try {
|
|
15663
17252
|
// Last fallible gate while public getters still show the predecessor (#3138).
|
|
15664
17253
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
15665
17254
|
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
17255
|
+
exactRetirement = await this.#prepareExactMcpControlRetirement();
|
|
17256
|
+
if (exactRetirement && !exactRetirement.canCommit())
|
|
17257
|
+
throw new Error("Exact MCP control retirement changed before session adoption");
|
|
15666
17258
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
17259
|
+
await exactRetirement?.commit();
|
|
15667
17260
|
// Endpoint identity committed to the successor: re-register the
|
|
15668
17261
|
// manager so post-transition lineage bindings resolve and owned
|
|
15669
17262
|
// aborts classify in the successor session (review thread P1).
|
|
15670
17263
|
this.#rekeyJobManagerForSessionIdentity(noLeasePreviousSessionIdentity, noLeasePreviousSessionFile);
|
|
15671
17264
|
await this.#runToolSessionTransitionCleanups();
|
|
15672
17265
|
} catch (error) {
|
|
17266
|
+
await exactRetirement?.abort();
|
|
15673
17267
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
15674
17268
|
}
|
|
15675
17269
|
this.setTodoPhases([]);
|
|
@@ -15735,17 +17329,23 @@ export class AgentSession {
|
|
|
15735
17329
|
throw new Error("Owned async jobs did not settle before session replacement.");
|
|
15736
17330
|
}
|
|
15737
17331
|
const prepared = await this.sessionManager.prepareNewSession(options);
|
|
17332
|
+
let exactRetirement: PreparedExactMcpRetirement | undefined;
|
|
15738
17333
|
try {
|
|
15739
17334
|
// Last fallible gate while public getters still show the predecessor (#3138).
|
|
15740
17335
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
15741
17336
|
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
17337
|
+
exactRetirement = await this.#prepareExactMcpControlRetirement();
|
|
17338
|
+
if (exactRetirement && !exactRetirement.canCommit())
|
|
17339
|
+
throw new Error("Exact MCP control retirement changed before session adoption");
|
|
15742
17340
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
17341
|
+
await exactRetirement?.commit();
|
|
15743
17342
|
// Endpoint identity committed to the successor: re-register the
|
|
15744
17343
|
// manager so post-transition lineage bindings resolve and owned
|
|
15745
17344
|
// aborts classify in the successor session (review thread P1).
|
|
15746
17345
|
this.#rekeyJobManagerForSessionIdentity(previousSessionIdentity, previousSessionIdentityFile);
|
|
15747
17346
|
await this.#runToolSessionTransitionCleanups();
|
|
15748
17347
|
} catch (error) {
|
|
17348
|
+
await exactRetirement?.abort();
|
|
15749
17349
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
15750
17350
|
}
|
|
15751
17351
|
this.#disconnectFromAgent();
|
|
@@ -15851,11 +17451,13 @@ export class AgentSession {
|
|
|
15851
17451
|
this.#reconnectToAgent();
|
|
15852
17452
|
this.#resetIrcRosterDeliveryState();
|
|
15853
17453
|
if (this.#extensionRunner) {
|
|
15854
|
-
await this.#
|
|
15855
|
-
|
|
15856
|
-
|
|
15857
|
-
|
|
15858
|
-
|
|
17454
|
+
await this.#runCommittedSuccessorHook(() =>
|
|
17455
|
+
this.#extensionRunner!.emit({
|
|
17456
|
+
type: "session_switch",
|
|
17457
|
+
reason: "new",
|
|
17458
|
+
previousSessionFile,
|
|
17459
|
+
}),
|
|
17460
|
+
);
|
|
15859
17461
|
} else {
|
|
15860
17462
|
}
|
|
15861
17463
|
}
|
|
@@ -15923,6 +17525,17 @@ export class AgentSession {
|
|
|
15923
17525
|
const previousSessionFile = this.sessionFile;
|
|
15924
17526
|
const previousWorkflowGateSessionId = this.sessionId;
|
|
15925
17527
|
const previousSessionIdentity = this.sessionManager.getSessionId();
|
|
17528
|
+
const predecessorQueuedSdkWork = this.#queuedMessagesForSessionTransition();
|
|
17529
|
+
const settleForkPredecessorWork = (): void => {
|
|
17530
|
+
this.#terminalizeQueuedSdkWorkForSessionTransition(predecessorQueuedSdkWork);
|
|
17531
|
+
this.#deferredSdkFollowUps = [];
|
|
17532
|
+
this.#pendingNextTurnMessages = [];
|
|
17533
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
17534
|
+
this.agent.clearAllQueues();
|
|
17535
|
+
this.#steeringMessages = [];
|
|
17536
|
+
this.#followUpMessages = [];
|
|
17537
|
+
this.#resetActiveSdkRunOwnership();
|
|
17538
|
+
};
|
|
15926
17539
|
|
|
15927
17540
|
// Emit session_before_switch event with reason "fork" (can be cancelled)
|
|
15928
17541
|
if (this.#extensionRunner?.hasHandlers("session_before_switch")) {
|
|
@@ -15936,6 +17549,10 @@ export class AgentSession {
|
|
|
15936
17549
|
}
|
|
15937
17550
|
}
|
|
15938
17551
|
|
|
17552
|
+
this.#settleTrackedQueuedInputsBeforeAgentDisconnect();
|
|
17553
|
+
this.#disconnectFromAgent();
|
|
17554
|
+
await this.#settleActivePromptForHistoryTransition();
|
|
17555
|
+
|
|
15939
17556
|
// Flush current session to ensure all entries are written
|
|
15940
17557
|
await this.sessionManager.flush();
|
|
15941
17558
|
|
|
@@ -15951,6 +17568,8 @@ export class AgentSession {
|
|
|
15951
17568
|
"copy-retain",
|
|
15952
17569
|
this.settings.get("sessionMemory.mode"),
|
|
15953
17570
|
);
|
|
17571
|
+
let exactRetirement: PreparedExactMcpRetirement | undefined;
|
|
17572
|
+
let adopted = false;
|
|
15954
17573
|
try {
|
|
15955
17574
|
await initializeLocalRoot({
|
|
15956
17575
|
getArtifactsDir: () => forkedManager.getArtifactsDir(),
|
|
@@ -15960,7 +17579,15 @@ export class AgentSession {
|
|
|
15960
17579
|
});
|
|
15961
17580
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
15962
17581
|
this.#assertJobManagerEndpointAdmission(forkedManager.getSessionId(), forkedManager.getSessionFile());
|
|
17582
|
+
exactRetirement = await this.#prepareExactMcpControlRetirement();
|
|
17583
|
+
if (exactRetirement && !exactRetirement.canCommit())
|
|
17584
|
+
throw new Error("Exact MCP control retirement changed before session adoption");
|
|
17585
|
+
this.sessionManager = forkedManager;
|
|
17586
|
+
adopted = true;
|
|
17587
|
+
await exactRetirement?.commit();
|
|
15963
17588
|
} catch (error) {
|
|
17589
|
+
await exactRetirement?.abort();
|
|
17590
|
+
if (adopted) this.sessionManager = previousManager;
|
|
15964
17591
|
const forkedFile = forkedManager.getSessionFile();
|
|
15965
17592
|
const cleanupErrors: unknown[] = [];
|
|
15966
17593
|
try {
|
|
@@ -15989,8 +17616,8 @@ export class AgentSession {
|
|
|
15989
17616
|
}
|
|
15990
17617
|
throw error;
|
|
15991
17618
|
}
|
|
15992
|
-
this.sessionManager = forkedManager;
|
|
15993
17619
|
this.#rekeyJobManagerForSessionIdentity(previousSessionIdentity, previousSessionFile);
|
|
17620
|
+
settleForkPredecessorWork();
|
|
15994
17621
|
try {
|
|
15995
17622
|
await previousManager.close();
|
|
15996
17623
|
} catch (error) {
|
|
@@ -16004,16 +17631,23 @@ export class AgentSession {
|
|
|
16004
17631
|
// public manager getters remain bound to the predecessor.
|
|
16005
17632
|
const prepared = await this.sessionManager.prepareFork();
|
|
16006
17633
|
if (!prepared) return false;
|
|
17634
|
+
let exactRetirement: PreparedExactMcpRetirement | undefined;
|
|
16007
17635
|
try {
|
|
16008
17636
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
16009
17637
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
16010
17638
|
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
17639
|
+
exactRetirement = await this.#prepareExactMcpControlRetirement();
|
|
17640
|
+
if (exactRetirement && !exactRetirement.canCommit())
|
|
17641
|
+
throw new Error("Exact MCP control retirement changed before session adoption");
|
|
16011
17642
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
17643
|
+
await exactRetirement?.commit();
|
|
16012
17644
|
// Fork commits a successor endpoint identity; re-register the
|
|
16013
17645
|
// manager under it (review thread P1).
|
|
16014
17646
|
this.#rekeyJobManagerForSessionIdentity(previousSessionIdentity, previousSessionFile);
|
|
17647
|
+
settleForkPredecessorWork();
|
|
16015
17648
|
await this.#runToolSessionTransitionCleanups();
|
|
16016
17649
|
} catch (error) {
|
|
17650
|
+
await exactRetirement?.abort();
|
|
16017
17651
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
16018
17652
|
}
|
|
16019
17653
|
}
|
|
@@ -16025,15 +17659,18 @@ export class AgentSession {
|
|
|
16025
17659
|
|
|
16026
17660
|
// Emit session_switch event with reason "fork" to hooks
|
|
16027
17661
|
if (this.#extensionRunner) {
|
|
16028
|
-
await this.#
|
|
16029
|
-
|
|
16030
|
-
|
|
16031
|
-
|
|
16032
|
-
|
|
17662
|
+
await this.#runCommittedSuccessorHook(() =>
|
|
17663
|
+
this.#extensionRunner!.emit({
|
|
17664
|
+
type: "session_switch",
|
|
17665
|
+
reason: "fork",
|
|
17666
|
+
previousSessionFile,
|
|
17667
|
+
}),
|
|
17668
|
+
);
|
|
16033
17669
|
}
|
|
16034
17670
|
|
|
16035
17671
|
return true;
|
|
16036
17672
|
} finally {
|
|
17673
|
+
this.#reconnectToAgent();
|
|
16037
17674
|
this.#endSessionTransition();
|
|
16038
17675
|
}
|
|
16039
17676
|
}
|
|
@@ -16142,15 +17779,42 @@ export class AgentSession {
|
|
|
16142
17779
|
await this.refreshBaseSystemPrompt();
|
|
16143
17780
|
}
|
|
16144
17781
|
|
|
16145
|
-
/** Resolver intent only for assignments owned by
|
|
17782
|
+
/** Resolver intent only for default assignments owned by an active or runtime-recovered durable profile. */
|
|
16146
17783
|
#persistedModelProfileAliasIntent(role: string): { aliasIntent: "preset-equivalent" } | undefined {
|
|
16147
|
-
|
|
16148
|
-
const
|
|
17784
|
+
const runtimeDefaultIdentity = this.#defaultFallbackController?.chain;
|
|
17785
|
+
const profileName =
|
|
17786
|
+
this.#activeModelProfile ??
|
|
17787
|
+
(role === "default" &&
|
|
17788
|
+
runtimeDefaultIdentity?.origin === "runtime" &&
|
|
17789
|
+
runtimeDefaultIdentity.identity === this.settings.get("modelProfile.default")
|
|
17790
|
+
? runtimeDefaultIdentity.identity
|
|
17791
|
+
: undefined);
|
|
17792
|
+
if (!profileName) return undefined;
|
|
17793
|
+
const profile = this.#modelRegistry.getModelProfile?.(profileName);
|
|
16149
17794
|
if (!profile) return undefined;
|
|
16150
17795
|
const bindings = resolveProfileBindings(profile);
|
|
17796
|
+
const configuredDefault = role === "default" ? this.getConfiguredModelChain("default")?.[0] : undefined;
|
|
17797
|
+
const profileDefault = Array.isArray(bindings.defaultSelector)
|
|
17798
|
+
? bindings.defaultSelector[0]
|
|
17799
|
+
: bindings.defaultSelector;
|
|
17800
|
+
const configuredDefaultIdentity = configuredDefault
|
|
17801
|
+
? splitSelectorThinkingSuffix(configuredDefault).selector.trim().toLowerCase()
|
|
17802
|
+
: undefined;
|
|
17803
|
+
const profileDefaultIdentity = profileDefault
|
|
17804
|
+
? splitSelectorThinkingSuffix(profileDefault).selector.trim().toLowerCase()
|
|
17805
|
+
: undefined;
|
|
17806
|
+
const configuredDefaultModel = configuredDefaultIdentity
|
|
17807
|
+
? parseModelString(configuredDefaultIdentity)
|
|
17808
|
+
: undefined;
|
|
17809
|
+
const profileDefaultModel = profileDefaultIdentity ? parseModelString(profileDefaultIdentity) : undefined;
|
|
17810
|
+
const ownsConfiguredDefault =
|
|
17811
|
+
configuredDefaultModel && profileDefaultModel
|
|
17812
|
+
? configuredDefaultModel.provider === profileDefaultModel.provider &&
|
|
17813
|
+
configuredDefaultModel.id === profileDefaultModel.id
|
|
17814
|
+
: profileDefaultIdentity !== undefined && configuredDefaultIdentity === profileDefaultIdentity;
|
|
16151
17815
|
const owned =
|
|
16152
17816
|
role === "default"
|
|
16153
|
-
?
|
|
17817
|
+
? runtimeDefaultIdentity?.origin === "runtime" || ownsConfiguredDefault
|
|
16154
17818
|
: Object.hasOwn(bindings.modelRoles, role) || Object.hasOwn(bindings.agentModelOverrides, role);
|
|
16155
17819
|
return owned ? { aliasIntent: "preset-equivalent" } : undefined;
|
|
16156
17820
|
}
|
|
@@ -16172,9 +17836,10 @@ export class AgentSession {
|
|
|
16172
17836
|
* configured `modelBindings` (also installed into these two override slots
|
|
16173
17837
|
* once at startup) are not profile-owned and must survive the transition.
|
|
16174
17838
|
*/
|
|
16175
|
-
#resetSessionScopedModelProfileState(): void {
|
|
17839
|
+
#resetSessionScopedModelProfileState(options?: { preserveDefaultConfiguredChain?: boolean; force?: boolean }): void {
|
|
16176
17840
|
const persistedProfile = this.settings.get("modelProfile.default");
|
|
16177
|
-
if (persistedProfile !== undefined && persistedProfile === this.getActiveModelProfile())
|
|
17841
|
+
if (!options?.force && persistedProfile !== undefined && persistedProfile === this.getActiveModelProfile())
|
|
17842
|
+
return;
|
|
16178
17843
|
const hadInstalledKeys =
|
|
16179
17844
|
this.#activeProfileInstalledRoles.size > 0 || this.#activeProfileInstalledAgentOverrides.size > 0;
|
|
16180
17845
|
if (hadInstalledKeys) {
|
|
@@ -16197,7 +17862,7 @@ export class AgentSession {
|
|
|
16197
17862
|
this.#modelRegistry.reapplyConfiguredModelBindings(this.settings);
|
|
16198
17863
|
}
|
|
16199
17864
|
const defaultChain = getSessionContextForInternalRead(this.sessionManager).configuredModelChains.default;
|
|
16200
|
-
if (defaultChain && defaultChain.identity !== undefined) {
|
|
17865
|
+
if (!options?.preserveDefaultConfiguredChain && defaultChain && defaultChain.identity !== undefined) {
|
|
16201
17866
|
this.setConfiguredModelChain("default", [], "user-selection");
|
|
16202
17867
|
}
|
|
16203
17868
|
this.setActiveModelProfile(undefined);
|
|
@@ -16439,11 +18104,36 @@ export class AgentSession {
|
|
|
16439
18104
|
* must not mutate the persisted configured intent.
|
|
16440
18105
|
*/
|
|
16441
18106
|
setDefaultFallbackRuntimeModel(selector: string): void {
|
|
18107
|
+
this.installRecoveredDefaultFallbackChain([selector], undefined, 0, []);
|
|
18108
|
+
}
|
|
18109
|
+
|
|
18110
|
+
/** Install a runtime-only durable fallback without changing saved session intent. */
|
|
18111
|
+
installRecoveredDefaultFallbackChain(
|
|
18112
|
+
entries: readonly string[],
|
|
18113
|
+
identity: string | undefined,
|
|
18114
|
+
activeIndex: number,
|
|
18115
|
+
skips: Array<{ selector: string; reason: string }>,
|
|
18116
|
+
options?: { emitResolutionEvent?: boolean },
|
|
18117
|
+
): void {
|
|
16442
18118
|
this.#defaultFallbackController = new FallbackChainController(
|
|
16443
|
-
{ role: "default", entries: [
|
|
18119
|
+
{ role: "default", entries: [...entries], origin: "runtime", identity, explicitHead: true },
|
|
16444
18120
|
this.settings.get("fallback.maxAttempts"),
|
|
16445
18121
|
);
|
|
16446
18122
|
this.#defaultFallbackExhaustedLastTurn = false;
|
|
18123
|
+
this.#seedDefaultFallbackResolutionForController(
|
|
18124
|
+
this.#defaultFallbackController,
|
|
18125
|
+
activeIndex,
|
|
18126
|
+
skips,
|
|
18127
|
+
options?.emitResolutionEvent ?? true,
|
|
18128
|
+
);
|
|
18129
|
+
}
|
|
18130
|
+
|
|
18131
|
+
markStartupRecoveryBindingsRequired(): void {
|
|
18132
|
+
this.#startupRecoveryBindingsRequired = true;
|
|
18133
|
+
}
|
|
18134
|
+
|
|
18135
|
+
hasRecoveredDefaultFallbackChain(): boolean {
|
|
18136
|
+
return this.#startupRecoveryBindingsRequired || this.#defaultFallbackController?.chain.origin === "runtime";
|
|
16447
18137
|
}
|
|
16448
18138
|
|
|
16449
18139
|
/**
|
|
@@ -16451,9 +18141,18 @@ export class AgentSession {
|
|
|
16451
18141
|
* The configured chain's role, origin, and identity are retained by the controller.
|
|
16452
18142
|
*/
|
|
16453
18143
|
seedDefaultFallbackResolution(activeIndex: number, skips: Array<{ selector: string; reason: string }>): void {
|
|
16454
|
-
|
|
18144
|
+
this.#seedDefaultFallbackResolutionForController(this.#defaultFallbackChain(), activeIndex, skips);
|
|
18145
|
+
}
|
|
18146
|
+
|
|
18147
|
+
/** Seed an already-selected runtime controller without legacy-chain materialization. */
|
|
18148
|
+
#seedDefaultFallbackResolutionForController(
|
|
18149
|
+
controller: FallbackChainController,
|
|
18150
|
+
activeIndex: number,
|
|
18151
|
+
skips: Array<{ selector: string; reason: string }>,
|
|
18152
|
+
emitResolutionEvent = true,
|
|
18153
|
+
): void {
|
|
16455
18154
|
controller.seedResolution(activeIndex, skips);
|
|
16456
|
-
this.#emitResolutionFallbackSwitch(controller);
|
|
18155
|
+
if (emitResolutionEvent) this.#emitResolutionFallbackSwitch(controller);
|
|
16457
18156
|
}
|
|
16458
18157
|
|
|
16459
18158
|
getDefaultFallbackRuntimeState(): DefaultFallbackRuntimeState {
|
|
@@ -16531,6 +18230,7 @@ export class AgentSession {
|
|
|
16531
18230
|
reason?: TemporaryModelReason;
|
|
16532
18231
|
providerSessionScope?: TemporaryProviderSessionScope;
|
|
16533
18232
|
signal?: AbortSignal;
|
|
18233
|
+
onMutationStarted?: () => void;
|
|
16534
18234
|
},
|
|
16535
18235
|
// biome-ignore lint/suspicious/noConfusingVoidType: Existing session adapters return Promise<void>; a scope is optional.
|
|
16536
18236
|
): Promise<TemporaryProviderSessionScope | void> {
|
|
@@ -16548,6 +18248,7 @@ export class AgentSession {
|
|
|
16548
18248
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
16549
18249
|
}
|
|
16550
18250
|
if (suppliedScope && this.#temporaryProviderSessionScopes.at(-1)?.token !== suppliedScope) return;
|
|
18251
|
+
options?.onMutationStarted?.();
|
|
16551
18252
|
|
|
16552
18253
|
const isTemporaryOperation = options?.cause === undefined || options.cause === "temporary-operation";
|
|
16553
18254
|
const autoCreateScope = isTemporaryOperation && !suppliedScope;
|
|
@@ -16599,13 +18300,18 @@ export class AgentSession {
|
|
|
16599
18300
|
model: Model | undefined,
|
|
16600
18301
|
thinkingLevel: ThinkingLevel | undefined,
|
|
16601
18302
|
): Promise<void> {
|
|
18303
|
+
const previousEditMode = this.#resolveActiveEditMode();
|
|
16602
18304
|
if (model) {
|
|
16603
|
-
|
|
16604
|
-
|
|
18305
|
+
// Restoring a captured live model is compensation, not a new selection:
|
|
18306
|
+
// the old credential may have disappeared after the forward mutation.
|
|
18307
|
+
this.#setModelAuthoritatively(model, "rollback");
|
|
18308
|
+
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
|
|
18309
|
+
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
18310
|
+
} else {
|
|
18311
|
+
this.#clearActiveRetryFallback();
|
|
18312
|
+
this.#setModelWithProviderSessionReset(undefined);
|
|
18313
|
+
this.#syncAppendOnlyContext(undefined);
|
|
16605
18314
|
}
|
|
16606
|
-
const previousEditMode = this.#resolveActiveEditMode();
|
|
16607
|
-
this.#clearActiveRetryFallback();
|
|
16608
|
-
this.#setModelWithProviderSessionReset(undefined);
|
|
16609
18315
|
this.setThinkingLevel(thinkingLevel);
|
|
16610
18316
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
16611
18317
|
}
|
|
@@ -17942,6 +19648,14 @@ export class AgentSession {
|
|
|
17942
19648
|
*/
|
|
17943
19649
|
async compact(customInstructions?: string, options?: CompactOptions): Promise<CompactionResult> {
|
|
17944
19650
|
this.#assertSessionAdmissionOpen();
|
|
19651
|
+
// Automatic context maintenance owns the transition lease while its
|
|
19652
|
+
// post-append hooks run. A manual request must join behind that owner so
|
|
19653
|
+
// it can cancel/wait for the automatic work rather than racing its lease.
|
|
19654
|
+
if (this.#sessionTransitionKind === "auto-compaction") {
|
|
19655
|
+
this.abortCompaction();
|
|
19656
|
+
await this.#waitForAutoCompactionCompletions();
|
|
19657
|
+
await this.#cancelPostPromptTasks();
|
|
19658
|
+
}
|
|
17945
19659
|
// Serialize with every other session-identity transition via the shared lease
|
|
17946
19660
|
// (bidirectional mutual exclusion with handoff/new/switch/branch/clear/fork/
|
|
17947
19661
|
// navigateTree). Released in the outer finally below.
|
|
@@ -17954,11 +19668,66 @@ export class AgentSession {
|
|
|
17954
19668
|
}
|
|
17955
19669
|
const compactionAbortController = new AbortController();
|
|
17956
19670
|
this.#compactionAbortController = compactionAbortController;
|
|
19671
|
+
const steeringBeforeCompaction = this.agent.snapshotSteering();
|
|
19672
|
+
const steeringDisplaysBeforeCompaction = [...this.#steeringMessages];
|
|
19673
|
+
const trackedSteersBeforeCompaction = new Map(
|
|
19674
|
+
[...this.#trackedQueuedInputs.values()]
|
|
19675
|
+
.filter(state => state.message !== undefined && steeringBeforeCompaction.includes(state.message))
|
|
19676
|
+
.map(state => [state.message as AgentMessage, state] as const),
|
|
19677
|
+
);
|
|
19678
|
+
const followUpBeforeCompaction = this.agent.snapshotFollowUp();
|
|
19679
|
+
const followUpDisplaysBeforeCompaction = [...this.#followUpMessages];
|
|
19680
|
+
const trackedFollowUpsBeforeCompaction = new Map(
|
|
19681
|
+
[...this.#trackedQueuedInputs.values()]
|
|
19682
|
+
.filter(state => state.message !== undefined && followUpBeforeCompaction.includes(state.message))
|
|
19683
|
+
.map(state => [state.message as AgentMessage, state] as const),
|
|
19684
|
+
);
|
|
19685
|
+
// Compaction intentionally preserves still-queued inputs, but it drops the
|
|
19686
|
+
// Agent event bridge before aborting the active run. Settle submissions that
|
|
19687
|
+
// already crossed the queue boundary now; they cannot receive agent_end later.
|
|
19688
|
+
this.#settleTrackedQueuedInputsBeforeAgentDisconnect();
|
|
17957
19689
|
this.#disconnectFromAgent();
|
|
17958
19690
|
try {
|
|
17959
|
-
|
|
19691
|
+
try {
|
|
19692
|
+
await this.abort({ cause: "compaction", preserveCompaction: true });
|
|
19693
|
+
} finally {
|
|
19694
|
+
const steeringAfterAbort = new Set(this.agent.snapshotSteering());
|
|
19695
|
+
const missingSteering = steeringBeforeCompaction.filter(message => !steeringAfterAbort.has(message));
|
|
19696
|
+
const restorableSteering: AgentMessage[] = [];
|
|
19697
|
+
for (const message of missingSteering) {
|
|
19698
|
+
const trackedState = trackedSteersBeforeCompaction.get(message);
|
|
19699
|
+
if (trackedState && (trackedState.terminalSettled || trackedState.cancelRequested)) continue;
|
|
19700
|
+
restorableSteering.push(message);
|
|
19701
|
+
}
|
|
19702
|
+
if (restorableSteering.length > 0) this.agent.restoreSteering(restorableSteering);
|
|
19703
|
+
this.#reconcileCompactionQueueDisplay(steeringDisplaysBeforeCompaction, "steering");
|
|
19704
|
+
const followUpAfterAbort = new Set(this.agent.snapshotFollowUp());
|
|
19705
|
+
const missingFollowUp = followUpBeforeCompaction.filter(message => !followUpAfterAbort.has(message));
|
|
19706
|
+
const restorableFollowUp: AgentMessage[] = [];
|
|
19707
|
+
for (const message of missingFollowUp) {
|
|
19708
|
+
const trackedState = trackedFollowUpsBeforeCompaction.get(message);
|
|
19709
|
+
if (trackedState) {
|
|
19710
|
+
const state = trackedState;
|
|
19711
|
+
if (state && !state.terminalSettled) this.#settleTrackedQueuedInputRemoved(state, "removed");
|
|
19712
|
+
continue;
|
|
19713
|
+
}
|
|
19714
|
+
restorableFollowUp.push(message);
|
|
19715
|
+
}
|
|
19716
|
+
if (restorableFollowUp.length > 0) this.agent.restoreFollowUp(restorableFollowUp);
|
|
19717
|
+
this.#reconcileCompactionQueueDisplay(followUpDisplaysBeforeCompaction, "followUp");
|
|
19718
|
+
// The disconnected bridge cannot run finishAttempt for the aborted
|
|
19719
|
+
// predecessor. Do not let its SDK token or attempt identity bleed into
|
|
19720
|
+
// the first ordinary successor prompt.
|
|
19721
|
+
this.#resetActiveSdkRunOwnership();
|
|
19722
|
+
}
|
|
17960
19723
|
} catch (error) {
|
|
17961
19724
|
this.#compactionAbortController = undefined;
|
|
19725
|
+
if (!this.#isDisposed && !this.#sessionAdmissionClosing) {
|
|
19726
|
+
this.#reconnectToAgent();
|
|
19727
|
+
const releasedDeferred = this.#releaseDeferredSdkFollowUps();
|
|
19728
|
+
this.#compactionQueuedDeliveryArmed = this.agent.hasQueuedMessages();
|
|
19729
|
+
if (!releasedDeferred && this.#compactionQueuedDeliveryArmed) this.#scheduleQueuedDelivery();
|
|
19730
|
+
}
|
|
17962
19731
|
throw error;
|
|
17963
19732
|
}
|
|
17964
19733
|
await this.#waitForAutoCompactionCompletions();
|
|
@@ -18106,7 +19875,12 @@ export class AgentSession {
|
|
|
18106
19875
|
if (this.#compactionAbortController === compactionAbortController) {
|
|
18107
19876
|
this.#compactionAbortController = undefined;
|
|
18108
19877
|
}
|
|
18109
|
-
if (!this.#isDisposed && !this.#sessionAdmissionClosing)
|
|
19878
|
+
if (!this.#isDisposed && !this.#sessionAdmissionClosing) {
|
|
19879
|
+
this.#reconnectToAgent();
|
|
19880
|
+
const releasedDeferred = this.#releaseDeferredSdkFollowUps();
|
|
19881
|
+
this.#compactionQueuedDeliveryArmed = this.agent.hasQueuedMessages();
|
|
19882
|
+
if (!releasedDeferred && this.#compactionQueuedDeliveryArmed) this.#scheduleQueuedDelivery();
|
|
19883
|
+
}
|
|
18110
19884
|
}
|
|
18111
19885
|
} finally {
|
|
18112
19886
|
completion.resolve();
|
|
@@ -18350,6 +20124,7 @@ export class AgentSession {
|
|
|
18350
20124
|
let savedPath: string | undefined;
|
|
18351
20125
|
let committed = false;
|
|
18352
20126
|
let prepared: PreparedNewSession | undefined;
|
|
20127
|
+
let exactRetirement: PreparedExactMcpRetirement | undefined;
|
|
18353
20128
|
try {
|
|
18354
20129
|
// Prepare successor entries, persistence, display state, and gate
|
|
18355
20130
|
// construction without publishing manager identity. Managed local-root
|
|
@@ -18375,9 +20150,13 @@ export class AgentSession {
|
|
|
18375
20150
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
18376
20151
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
18377
20152
|
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
20153
|
+
exactRetirement = await this.#prepareExactMcpControlRetirement();
|
|
20154
|
+
if (exactRetirement && !exactRetirement.canCommit())
|
|
20155
|
+
throw new Error("Exact MCP control retirement changed before session adoption");
|
|
18378
20156
|
|
|
18379
20157
|
// --- Commit boundary: synchronous adoption is the sole identity publication.
|
|
18380
20158
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
20159
|
+
await exactRetirement?.commit();
|
|
18381
20160
|
// Handoff commits a successor endpoint identity; re-register the
|
|
18382
20161
|
// manager under it (review thread P1).
|
|
18383
20162
|
this.#rekeyJobManagerForSessionIdentity(rollbackSessionState.sessionId, rollbackSessionState.sessionFile);
|
|
@@ -18387,6 +20166,7 @@ export class AgentSession {
|
|
|
18387
20166
|
...rollbackAgentSteeringQueue,
|
|
18388
20167
|
...rollbackAgentFollowUpQueue,
|
|
18389
20168
|
...rollbackDeferredSdkFollowUps,
|
|
20169
|
+
...rollbackPendingNextTurnMessages.map(entry => entry.message),
|
|
18390
20170
|
]);
|
|
18391
20171
|
this.#resetActiveSdkRunOwnership();
|
|
18392
20172
|
this.agent.reset();
|
|
@@ -18437,11 +20217,13 @@ export class AgentSession {
|
|
|
18437
20217
|
// errors are isolated by ExtensionRunner and must not roll back the
|
|
18438
20218
|
// already-committed switch.
|
|
18439
20219
|
if (this.#extensionRunner) {
|
|
18440
|
-
await this.#
|
|
18441
|
-
|
|
18442
|
-
|
|
18443
|
-
|
|
18444
|
-
|
|
20220
|
+
await this.#runCommittedSuccessorHook(() =>
|
|
20221
|
+
this.#extensionRunner!.emit({
|
|
20222
|
+
type: "session_switch",
|
|
20223
|
+
reason: "new",
|
|
20224
|
+
previousSessionFile,
|
|
20225
|
+
}),
|
|
20226
|
+
);
|
|
18445
20227
|
}
|
|
18446
20228
|
|
|
18447
20229
|
return { document: handoffText, savedPath };
|
|
@@ -18458,6 +20240,7 @@ export class AgentSession {
|
|
|
18458
20240
|
// failure is non-destructive. Predecessor gate emitter, provider
|
|
18459
20241
|
// sessions, async jobs, IRC/plan bookkeeping, and injection signatures
|
|
18460
20242
|
// were never mutated before commit, so they survive intact.
|
|
20243
|
+
await exactRetirement?.abort();
|
|
18461
20244
|
await this.sessionManager.restoreRollbackState(rollbackSessionState);
|
|
18462
20245
|
this.#syncAgentSessionId(rollbackSessionState.sessionId);
|
|
18463
20246
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
@@ -19884,33 +21667,79 @@ export class AgentSession {
|
|
|
19884
21667
|
addCandidate(this.#resolveRoleModelFull(role, availableModels, currentModel).model);
|
|
19885
21668
|
}
|
|
19886
21669
|
|
|
19887
|
-
// Last-resort fallback: the largest-context model that shares the ACTIVE
|
|
19888
|
-
// model's provider. Scoping this to the current provider keeps auto-
|
|
19889
|
-
// compaction on the user's configured/custom route instead of silently
|
|
19890
|
-
// defaulting to an unrelated provider (e.g. a stray OpenAI credential
|
|
19891
|
-
// with no remaining credit) just because it happens to be in the bundled
|
|
19892
|
-
// catalog. Cross-provider compaction stays possible, but only when the
|
|
19893
|
-
// user opts in explicitly via modelRoles (handled by the loop above).
|
|
19894
|
-
const fallbackProvider = currentModel?.provider;
|
|
19895
|
-
const sortedByContext = [...availableModels]
|
|
19896
|
-
.filter(model => fallbackProvider === undefined || model.provider === fallbackProvider)
|
|
19897
|
-
.sort((a, b) => b.contextWindow - a.contextWindow);
|
|
19898
|
-
for (const model of sortedByContext) {
|
|
19899
|
-
if (!seen.has(this.#getModelKey(model))) {
|
|
19900
|
-
addCandidate(model);
|
|
19901
|
-
break;
|
|
19902
|
-
}
|
|
19903
|
-
}
|
|
19904
|
-
|
|
19905
21670
|
return candidates;
|
|
19906
21671
|
}
|
|
21672
|
+
#compactionCandidateSource(candidate: Model): string {
|
|
21673
|
+
const currentModel = this.model;
|
|
21674
|
+
if (currentModel && modelsAreEqual(candidate, currentModel)) return "active session model";
|
|
21675
|
+
for (const role of MODEL_ROLE_IDS) {
|
|
21676
|
+
const roleModel = this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), currentModel).model;
|
|
21677
|
+
if (roleModel && modelsAreEqual(candidate, roleModel)) return `modelRoles.${role}`;
|
|
21678
|
+
}
|
|
21679
|
+
return "configured compaction fallback";
|
|
21680
|
+
}
|
|
21681
|
+
#compactionFailureReason(error: unknown): string {
|
|
21682
|
+
const message = sanitizeCompactionCandidateFailureMessage(error instanceof Error ? error.message : String(error));
|
|
21683
|
+
if (/stream stalled while waiting for the next event/i.test(message)) {
|
|
21684
|
+
return "SSE stream stalled while waiting for the next event";
|
|
21685
|
+
}
|
|
21686
|
+
if (/timed? out while waiting for the first event/i.test(message)) {
|
|
21687
|
+
return "SSE stream timed out while waiting for the first event";
|
|
21688
|
+
}
|
|
21689
|
+
if (/auth_unavailable|no auth available|credential/i.test(message)) {
|
|
21690
|
+
return "candidate credentials unavailable";
|
|
21691
|
+
}
|
|
21692
|
+
return message.length > 160 ? `${message.slice(0, 157)}...` : message;
|
|
21693
|
+
}
|
|
21694
|
+
async #emitCompactionModelSubstitution(from: Model, to: Model, reason: string): Promise<void> {
|
|
21695
|
+
const fromKey = this.#getModelKey(from);
|
|
21696
|
+
const toKey = this.#getModelKey(to);
|
|
21697
|
+
const source = this.#compactionCandidateSource(to);
|
|
21698
|
+
try {
|
|
21699
|
+
await this.#emitSessionEvent({
|
|
21700
|
+
type: "notice",
|
|
21701
|
+
level: "warning",
|
|
21702
|
+
source: "compaction-recovery",
|
|
21703
|
+
message: `Compaction recovery switched model from ${fromKey} to ${toKey} (${source}) after ${reason}.`,
|
|
21704
|
+
});
|
|
21705
|
+
} catch (error) {
|
|
21706
|
+
logger.warn("Failed to publish compaction model substitution", {
|
|
21707
|
+
from: fromKey,
|
|
21708
|
+
to: toKey,
|
|
21709
|
+
error: String(error),
|
|
21710
|
+
});
|
|
21711
|
+
}
|
|
21712
|
+
}
|
|
21713
|
+
async #resolveCompactionCandidate(
|
|
21714
|
+
candidate: Model,
|
|
21715
|
+
previousCandidate: Model | undefined,
|
|
21716
|
+
previousFailure: unknown,
|
|
21717
|
+
): Promise<{ apiKey: string; authError?: undefined } | { apiKey: undefined; authError: Error }> {
|
|
21718
|
+
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.credentialSessionId);
|
|
21719
|
+
if (!apiKey) return { apiKey: undefined, authError: this.#buildCompactionAuthError(candidate) };
|
|
21720
|
+
if (previousCandidate) {
|
|
21721
|
+
await this.#emitCompactionModelSubstitution(
|
|
21722
|
+
previousCandidate,
|
|
21723
|
+
candidate,
|
|
21724
|
+
this.#compactionFailureReason(previousFailure),
|
|
21725
|
+
);
|
|
21726
|
+
}
|
|
21727
|
+
return { apiKey, authError: undefined };
|
|
21728
|
+
}
|
|
19907
21729
|
#isCompactionAuthFailure(error: unknown): boolean {
|
|
19908
21730
|
if (!(error instanceof Error)) return false;
|
|
21731
|
+
if (
|
|
21732
|
+
/stream stalled while waiting for the next event|timed? out while waiting for the first event/i.test(
|
|
21733
|
+
error.message,
|
|
21734
|
+
)
|
|
21735
|
+
) {
|
|
21736
|
+
return false;
|
|
21737
|
+
}
|
|
19909
21738
|
return /auth_unavailable|no auth available/i.test(error.message);
|
|
19910
21739
|
}
|
|
19911
21740
|
|
|
19912
|
-
#buildCompactionAuthError(): Error {
|
|
19913
|
-
const currentModel = this.model;
|
|
21741
|
+
#buildCompactionAuthError(candidate?: Model): Error {
|
|
21742
|
+
const currentModel = candidate ?? this.model;
|
|
19914
21743
|
if (!currentModel) {
|
|
19915
21744
|
return new Error(
|
|
19916
21745
|
"Compaction requires a model with usable credentials, but no authenticated compaction model is available.",
|
|
@@ -19960,10 +21789,20 @@ export class AgentSession {
|
|
|
19960
21789
|
): Promise<CompactionResult> {
|
|
19961
21790
|
const candidates = this.#getCompactionModelCandidates(this.#modelRegistry.getAvailable());
|
|
19962
21791
|
const telemetry = resolveTelemetry(this.agent.telemetry, this.sessionId);
|
|
21792
|
+
let previousCandidate: Model | undefined;
|
|
21793
|
+
let previousFailure: unknown;
|
|
19963
21794
|
|
|
19964
21795
|
for (const candidate of candidates) {
|
|
19965
|
-
const apiKey = await this.#
|
|
19966
|
-
|
|
21796
|
+
const { apiKey, authError } = await this.#resolveCompactionCandidate(
|
|
21797
|
+
candidate,
|
|
21798
|
+
previousCandidate,
|
|
21799
|
+
previousFailure,
|
|
21800
|
+
);
|
|
21801
|
+
if (apiKey === undefined) {
|
|
21802
|
+
previousCandidate = candidate;
|
|
21803
|
+
previousFailure = authError;
|
|
21804
|
+
continue;
|
|
21805
|
+
}
|
|
19967
21806
|
|
|
19968
21807
|
try {
|
|
19969
21808
|
return await compact(preparation, candidate, apiKey, customInstructions, signal, {
|
|
@@ -19981,10 +21820,12 @@ export class AgentSession {
|
|
|
19981
21820
|
if (!this.#isCompactionAuthFailure(error)) {
|
|
19982
21821
|
throw error;
|
|
19983
21822
|
}
|
|
21823
|
+
previousCandidate = candidate;
|
|
21824
|
+
previousFailure = this.#buildCompactionAuthError(candidate);
|
|
19984
21825
|
}
|
|
19985
21826
|
}
|
|
19986
21827
|
|
|
19987
|
-
throw this.#buildCompactionAuthError();
|
|
21828
|
+
throw previousFailure instanceof Error ? previousFailure : this.#buildCompactionAuthError();
|
|
19988
21829
|
}
|
|
19989
21830
|
|
|
19990
21831
|
async #prepareCompactionFromHooks(
|
|
@@ -20144,6 +21985,15 @@ export class AgentSession {
|
|
|
20144
21985
|
let action: "context-full" | "handoff" =
|
|
20145
21986
|
compactionSettings.strategy === "handoff" && reason !== "overflow" ? "handoff" : "context-full";
|
|
20146
21987
|
const continueAfterMaintenance = options?.continueAfterMaintenance !== false;
|
|
21988
|
+
let ownsAutoCompactionTransition = false;
|
|
21989
|
+
const acquireAutoCompactionTransition = (): boolean => {
|
|
21990
|
+
if (ownsAutoCompactionTransition) return true;
|
|
21991
|
+
if (this.#sessionTransitionKind !== undefined || this.#cancelAndSubmitInProgress) return false;
|
|
21992
|
+
this.#beginSessionTransition("auto-compaction");
|
|
21993
|
+
ownsAutoCompactionTransition = true;
|
|
21994
|
+
return true;
|
|
21995
|
+
};
|
|
21996
|
+
if (action === "context-full" && !acquireAutoCompactionTransition()) return { kind: "skipped" };
|
|
20147
21997
|
// Register the controller before the observable start event so an abort from
|
|
20148
21998
|
// a local subscriber, extension, dispose, or caller signal owns this run.
|
|
20149
21999
|
this.#autoCompactionAbortController?.abort();
|
|
@@ -20168,12 +22018,21 @@ export class AgentSession {
|
|
|
20168
22018
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
20169
22019
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
20170
22020
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
20171
|
-
|
|
20172
|
-
|
|
20173
|
-
|
|
20174
|
-
|
|
22021
|
+
// Start the workflow projection in parallel with the synchronous compaction
|
|
22022
|
+
// preparation. Overflow recovery often has no eligible history after the
|
|
22023
|
+
// failed assistant is removed; waiting for this filesystem projection before
|
|
22024
|
+
// discovering that no-op would delay the terminal auto_compaction_end event.
|
|
22025
|
+
// The promise is still awaited below so zero-progress tracking remains a
|
|
22026
|
+
// completed side effect for every compaction observation.
|
|
22027
|
+
const compactionStateSnapshotPromise = this.#compactionStateSnapshot({
|
|
22028
|
+
trackWorkflowRecoveryProgress: true,
|
|
22029
|
+
});
|
|
20175
22030
|
|
|
20176
22031
|
if (compactionSettings.strategy === "handoff" && reason !== "overflow") {
|
|
22032
|
+
await compactionStateSnapshotPromise;
|
|
22033
|
+
if (autoCompactionSignal.aborted || this.#isDisposed || this.#promptGeneration !== generation) {
|
|
22034
|
+
return await emitAborted();
|
|
22035
|
+
}
|
|
20177
22036
|
const handoffFocus = AUTO_HANDOFF_THRESHOLD_FOCUS;
|
|
20178
22037
|
const handoffResult = await this.handoff(handoffFocus, {
|
|
20179
22038
|
autoTriggered: true,
|
|
@@ -20196,6 +22055,7 @@ export class AgentSession {
|
|
|
20196
22055
|
reason,
|
|
20197
22056
|
});
|
|
20198
22057
|
action = "context-full";
|
|
22058
|
+
if (!acquireAutoCompactionTransition()) return { kind: "skipped" };
|
|
20199
22059
|
}
|
|
20200
22060
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
20201
22061
|
|
|
@@ -20218,28 +22078,14 @@ export class AgentSession {
|
|
|
20218
22078
|
}
|
|
20219
22079
|
|
|
20220
22080
|
if (!this.model) {
|
|
20221
|
-
await
|
|
20222
|
-
|
|
20223
|
-
action,
|
|
20224
|
-
result: undefined,
|
|
20225
|
-
aborted: false,
|
|
20226
|
-
willRetry: false,
|
|
20227
|
-
skipped: true,
|
|
20228
|
-
});
|
|
20229
|
-
return { kind: "skipped" };
|
|
22081
|
+
await compactionStateSnapshotPromise;
|
|
22082
|
+
throw new Error("No model selected");
|
|
20230
22083
|
}
|
|
20231
22084
|
|
|
20232
22085
|
const availableModels = this.#modelRegistry.getAvailable();
|
|
20233
22086
|
if (availableModels.length === 0) {
|
|
20234
|
-
await
|
|
20235
|
-
|
|
20236
|
-
action,
|
|
20237
|
-
result: undefined,
|
|
20238
|
-
aborted: false,
|
|
20239
|
-
willRetry: false,
|
|
20240
|
-
skipped: true,
|
|
20241
|
-
});
|
|
20242
|
-
return { kind: "skipped" };
|
|
22087
|
+
await compactionStateSnapshotPromise;
|
|
22088
|
+
throw this.#buildCompactionAuthError(this.model);
|
|
20243
22089
|
}
|
|
20244
22090
|
|
|
20245
22091
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
@@ -20287,6 +22133,7 @@ export class AgentSession {
|
|
|
20287
22133
|
skipped: true,
|
|
20288
22134
|
continuationSkipReason,
|
|
20289
22135
|
});
|
|
22136
|
+
await compactionStateSnapshotPromise;
|
|
20290
22137
|
if (overflowNoopWouldReplay) {
|
|
20291
22138
|
if (continueAfterMaintenance && this.agent.hasQueuedMessages()) {
|
|
20292
22139
|
this.#scheduleAgentContinue({
|
|
@@ -20327,6 +22174,11 @@ export class AgentSession {
|
|
|
20327
22174
|
return { kind: "skipped" };
|
|
20328
22175
|
}
|
|
20329
22176
|
|
|
22177
|
+
const compactionStateSnapshot = await compactionStateSnapshotPromise;
|
|
22178
|
+
if (autoCompactionSignal.aborted || this.#isDisposed || this.#promptGeneration !== generation) {
|
|
22179
|
+
return await emitAborted();
|
|
22180
|
+
}
|
|
22181
|
+
|
|
20330
22182
|
let hookCompaction: CompactionResult | undefined;
|
|
20331
22183
|
let fromExtension = false;
|
|
20332
22184
|
let preserveData: Record<string, unknown> | undefined;
|
|
@@ -20421,14 +22273,29 @@ export class AgentSession {
|
|
|
20421
22273
|
const telemetry = resolveTelemetry(this.agent.telemetry, this.sessionId);
|
|
20422
22274
|
let compactResult: CompactionResult | undefined;
|
|
20423
22275
|
let lastError: unknown;
|
|
20424
|
-
// Every candidate that was tried and failed, in order.
|
|
20425
|
-
//
|
|
20426
|
-
//
|
|
22276
|
+
// Every candidate that was tried and failed, in order. Keep the first
|
|
22277
|
+
// failure visible, and retain candidate-specific diagnostics for explicit
|
|
22278
|
+
// role fallbacks instead of collapsing the chain to its final error.
|
|
20427
22279
|
const candidateFailures: Array<{ model: string; message: string }> = [];
|
|
22280
|
+
let previousCandidate: Model | undefined;
|
|
22281
|
+
let previousFailure: unknown;
|
|
20428
22282
|
|
|
20429
22283
|
for (const candidate of candidates) {
|
|
20430
|
-
const apiKey = await this.#
|
|
20431
|
-
|
|
22284
|
+
const { apiKey, authError } = await this.#resolveCompactionCandidate(
|
|
22285
|
+
candidate,
|
|
22286
|
+
previousCandidate,
|
|
22287
|
+
previousFailure,
|
|
22288
|
+
);
|
|
22289
|
+
if (apiKey === undefined) {
|
|
22290
|
+
lastError = authError;
|
|
22291
|
+
previousCandidate = candidate;
|
|
22292
|
+
previousFailure = authError;
|
|
22293
|
+
candidateFailures.push({
|
|
22294
|
+
model: `${candidate.provider}/${candidate.id}`,
|
|
22295
|
+
message: authError.message,
|
|
22296
|
+
});
|
|
22297
|
+
continue;
|
|
22298
|
+
}
|
|
20432
22299
|
|
|
20433
22300
|
let attempt = 0;
|
|
20434
22301
|
while (true) {
|
|
@@ -20457,8 +22324,14 @@ export class AgentSession {
|
|
|
20457
22324
|
|
|
20458
22325
|
const message = error instanceof Error ? error.message : String(error);
|
|
20459
22326
|
if (this.#isCompactionAuthFailure(error)) {
|
|
20460
|
-
|
|
20461
|
-
|
|
22327
|
+
const authError = this.#buildCompactionAuthError(candidate);
|
|
22328
|
+
lastError = authError;
|
|
22329
|
+
previousCandidate = candidate;
|
|
22330
|
+
previousFailure = authError;
|
|
22331
|
+
candidateFailures.push({
|
|
22332
|
+
model: `${candidate.provider}/${candidate.id}`,
|
|
22333
|
+
message: authError.message,
|
|
22334
|
+
});
|
|
20462
22335
|
break;
|
|
20463
22336
|
}
|
|
20464
22337
|
const retryAfterMs = this.#parseRetryAfterMsFromError(message);
|
|
@@ -20471,6 +22344,8 @@ export class AgentSession {
|
|
|
20471
22344
|
isUsageLimitError(message));
|
|
20472
22345
|
if (!shouldRetry) {
|
|
20473
22346
|
lastError = error;
|
|
22347
|
+
previousCandidate = candidate;
|
|
22348
|
+
previousFailure = error;
|
|
20474
22349
|
candidateFailures.push({ model: `${candidate.provider}/${candidate.id}`, message });
|
|
20475
22350
|
break;
|
|
20476
22351
|
}
|
|
@@ -20496,6 +22371,8 @@ export class AgentSession {
|
|
|
20496
22371
|
model: `${candidate.provider}/${candidate.id}`,
|
|
20497
22372
|
});
|
|
20498
22373
|
lastError = error;
|
|
22374
|
+
previousCandidate = candidate;
|
|
22375
|
+
previousFailure = error;
|
|
20499
22376
|
candidateFailures.push({ model: `${candidate.provider}/${candidate.id}`, message });
|
|
20500
22377
|
break; // Exit retry loop, continue to next candidate
|
|
20501
22378
|
}
|
|
@@ -20643,6 +22520,7 @@ export class AgentSession {
|
|
|
20643
22520
|
if (this.#autoCompactionAbortController === autoCompactionAbortController) {
|
|
20644
22521
|
this.#autoCompactionAbortController = undefined;
|
|
20645
22522
|
}
|
|
22523
|
+
if (ownsAutoCompactionTransition) this.#endSessionTransition();
|
|
20646
22524
|
}
|
|
20647
22525
|
}
|
|
20648
22526
|
|
|
@@ -20911,6 +22789,11 @@ export class AgentSession {
|
|
|
20911
22789
|
if (message.errorMessage?.startsWith("Managed fallback retried the escaped non-ASCII")) return "terminal";
|
|
20912
22790
|
if (message.stopReason !== "error") return "none";
|
|
20913
22791
|
if (message.errorKind === "provider_safety_stop") return "terminal";
|
|
22792
|
+
// A decode loop is deterministic for the submitted context: replaying the
|
|
22793
|
+
// identical conversation re-trips the guard and re-bills the full context.
|
|
22794
|
+
// Without this the message carries no transport facts and would fall
|
|
22795
|
+
// through to "unknown", which is admitted for bounded retry (#5627).
|
|
22796
|
+
if (message.errorCode === REPETITION_GUARD_ERROR_CODE) return "terminal";
|
|
20914
22797
|
if (message.errorKind === "local_snapshot_failure") return "local_snapshot";
|
|
20915
22798
|
if (message.errorKind === "local_buffer_overflow") return "local_buffer_overflow";
|
|
20916
22799
|
if (this.#isTypedFirstEventTimeout(message)) return "first_event_timeout";
|
|
@@ -21459,6 +23342,10 @@ export class AgentSession {
|
|
|
21459
23342
|
if (classifyContextOverflow(message, transportFailure, this.model?.contextWindow ?? 0)) return undefined;
|
|
21460
23343
|
const transport = classifyFallbackTrigger(transportFailure ?? { status: message.errorStatus });
|
|
21461
23344
|
if (transport.class !== "other") return transport;
|
|
23345
|
+
// HTTP/2 observations explain a terminal failure; they do not authorize replay.
|
|
23346
|
+
if (transportFailure?.http2RstCode !== undefined || transportFailure?.nativeErrorCode !== undefined) {
|
|
23347
|
+
return undefined;
|
|
23348
|
+
}
|
|
21462
23349
|
// Managed fallback receives authoritative transport facts from the request
|
|
21463
23350
|
// boundary. Once those facts classify as other, error prose must not upgrade
|
|
21464
23351
|
// the failure into an unbounded transient or quota retry.
|
|
@@ -23744,8 +25631,16 @@ export class AgentSession {
|
|
|
23744
25631
|
const previousPendingNextTurnMessages = [...this.#pendingNextTurnMessages];
|
|
23745
25632
|
const previousScheduledHiddenNextTurnGeneration = this.#scheduledHiddenNextTurnGeneration;
|
|
23746
25633
|
const previousModel = this.model;
|
|
25634
|
+
const previousDefaultFallbackRuntimeState = this.getDefaultFallbackRuntimeState();
|
|
23747
25635
|
const previousThinkingLevel = this.#thinkingLevel;
|
|
23748
25636
|
const previousActiveModelProfile = this.#activeModelProfile;
|
|
25637
|
+
const previousModelRolesOverride = structuredClone(this.settings.getOverride("modelRoles"));
|
|
25638
|
+
const previousAgentModelOverridesOverride = structuredClone(
|
|
25639
|
+
this.settings.getOverride("task.agentModelOverrides"),
|
|
25640
|
+
);
|
|
25641
|
+
const previousActiveProfileInstalledRoles = new Map(this.#activeProfileInstalledRoles);
|
|
25642
|
+
const previousActiveProfileInstalledAgentOverrides = new Map(this.#activeProfileInstalledAgentOverrides);
|
|
25643
|
+
const previousPreProfileModel = this.#preProfileModel;
|
|
23749
25644
|
const previousServiceTier = this.agent.serviceTier;
|
|
23750
25645
|
const previousSelectedMCPToolNames = new Set(this.#selectedMCPToolNames);
|
|
23751
25646
|
const previousTools = [...this.agent.state.tools];
|
|
@@ -23754,6 +25649,7 @@ export class AgentSession {
|
|
|
23754
25649
|
const previousAgentSteeringQueue = this.agent.snapshotSteering();
|
|
23755
25650
|
const previousAgentFollowUpQueue = this.agent.snapshotFollowUp();
|
|
23756
25651
|
const previousDeferredSdkFollowUps = [...this.#deferredSdkFollowUps];
|
|
25652
|
+
const previousTrackedQueuedInputs = [...this.#trackedQueuedInputs.values()];
|
|
23757
25653
|
const previousActiveSdkRunToken = this.#activeSdkRunToken;
|
|
23758
25654
|
const previousActiveAttemptScope = this.#activeAttemptScope;
|
|
23759
25655
|
const previousActiveLogicalRunId = this.#activeLogicalRunId;
|
|
@@ -23766,7 +25662,11 @@ export class AgentSession {
|
|
|
23766
25662
|
? this.#suspendWorkflowGateEmitter(previousSessionState.sessionId)
|
|
23767
25663
|
: undefined;
|
|
23768
25664
|
let unavailableDefaultChainMessage: string | undefined;
|
|
25665
|
+
let recoveredDefaultChainMessage: string | undefined;
|
|
25666
|
+
let recoveredThinkingLevel: ThinkingLevel | undefined;
|
|
25667
|
+
let durableDefaultRecoveryError: string | undefined;
|
|
23769
25668
|
let transitionCleanupCommitted = false;
|
|
25669
|
+
let exactRetirement: PreparedExactMcpRetirement | undefined;
|
|
23770
25670
|
|
|
23771
25671
|
try {
|
|
23772
25672
|
await this.sessionManager.setSessionFile(sessionPath, {
|
|
@@ -23822,12 +25722,6 @@ export class AgentSession {
|
|
|
23822
25722
|
|
|
23823
25723
|
const resumeModelBehavior = this.settings.get("session.resumeModelBehavior");
|
|
23824
25724
|
const configuredDefaultChain = sessionContext.configuredModelChains.default;
|
|
23825
|
-
const settingsDefaultEntries = normalizeModelSelectorValue(this.settings.getModelRole("default"));
|
|
23826
|
-
const defaultEntries =
|
|
23827
|
-
resumeModelBehavior === "useCurrentDefault"
|
|
23828
|
-
? settingsDefaultEntries
|
|
23829
|
-
: (configuredDefaultChain?.entries ??
|
|
23830
|
-
(sessionContext.models.default ? [sessionContext.models.default] : []));
|
|
23831
25725
|
const profileDefinitions = this.#modelRegistry.getModelProfiles?.() ?? new Map<string, unknown>();
|
|
23832
25726
|
const configuredProfileName = this.settings.get("modelProfile.default");
|
|
23833
25727
|
const configuredProfileIdentity = configuredProfileName
|
|
@@ -23839,18 +25733,45 @@ export class AgentSession {
|
|
|
23839
25733
|
const liveProfileIdentity = previousActiveModelProfile
|
|
23840
25734
|
? resolveModelProfileName(previousActiveModelProfile, profileDefinitions)
|
|
23841
25735
|
: undefined;
|
|
23842
|
-
|
|
25736
|
+
let targetActiveModelProfile =
|
|
23843
25737
|
resumeModelBehavior === "useCurrentDefault"
|
|
23844
|
-
?
|
|
23845
|
-
?
|
|
23846
|
-
: configuredProfileIdentity && profileDefinitions.has(configuredProfileIdentity)
|
|
25738
|
+
? switchingToDifferentSession
|
|
25739
|
+
? configuredProfileIdentity && profileDefinitions.has(configuredProfileIdentity)
|
|
23847
25740
|
? configuredProfileIdentity
|
|
23848
25741
|
: undefined
|
|
25742
|
+
: liveProfileIdentity && profileDefinitions.has(liveProfileIdentity)
|
|
25743
|
+
? liveProfileIdentity
|
|
25744
|
+
: configuredProfileIdentity && profileDefinitions.has(configuredProfileIdentity)
|
|
25745
|
+
? configuredProfileIdentity
|
|
25746
|
+
: undefined
|
|
23849
25747
|
: configuredDefaultChain?.origin === "profile-activation" &&
|
|
23850
25748
|
persistedProfileIdentity &&
|
|
23851
25749
|
profileDefinitions.has(persistedProfileIdentity)
|
|
23852
25750
|
? persistedProfileIdentity
|
|
23853
25751
|
: undefined;
|
|
25752
|
+
// Keep a durable profile's existing runtime layer when the successor
|
|
25753
|
+
// resolves to that same profile. A cross-file transition has no separate
|
|
25754
|
+
// profile activation to reinstall its role and delegation bindings. Any
|
|
25755
|
+
// session-only or different predecessor profile must still be removed
|
|
25756
|
+
// before resolving the successor's default chain.
|
|
25757
|
+
const retainsDurableProfileLayer =
|
|
25758
|
+
switchingToDifferentSession &&
|
|
25759
|
+
targetActiveModelProfile !== undefined &&
|
|
25760
|
+
targetActiveModelProfile === configuredProfileIdentity &&
|
|
25761
|
+
liveProfileIdentity === targetActiveModelProfile;
|
|
25762
|
+
let targetProfileRuntimeInstalled = !switchingToDifferentSession || retainsDurableProfileLayer;
|
|
25763
|
+
if (switchingToDifferentSession && !retainsDurableProfileLayer)
|
|
25764
|
+
this.#resetSessionScopedModelProfileState({
|
|
25765
|
+
preserveDefaultConfiguredChain: true,
|
|
25766
|
+
force: true,
|
|
25767
|
+
});
|
|
25768
|
+
const settingsDefaultEntries = normalizeModelSelectorValue(this.settings.getModelRole("default"));
|
|
25769
|
+
const defaultEntries =
|
|
25770
|
+
resumeModelBehavior === "useCurrentDefault"
|
|
25771
|
+
? settingsDefaultEntries
|
|
25772
|
+
: (configuredDefaultChain?.entries ??
|
|
25773
|
+
(sessionContext.models.default ? [sessionContext.models.default] : []));
|
|
25774
|
+
this.#activeModelProfile = targetActiveModelProfile;
|
|
23854
25775
|
this.#defaultFallbackController = undefined;
|
|
23855
25776
|
if (defaultEntries.length > 0) {
|
|
23856
25777
|
const resolution = await resolveModelChainWithAuth(
|
|
@@ -23864,14 +25785,77 @@ export class AgentSession {
|
|
|
23864
25785
|
...(this.#persistedModelProfileAliasIntent("default") ?? {}),
|
|
23865
25786
|
},
|
|
23866
25787
|
);
|
|
23867
|
-
|
|
25788
|
+
let controller = this.#defaultFallbackChain();
|
|
23868
25789
|
this.seedDefaultFallbackResolution(resolution.activeIndex, resolution.skips);
|
|
23869
|
-
|
|
23870
|
-
|
|
23871
|
-
|
|
25790
|
+
let resolvedModel = resolution.model;
|
|
25791
|
+
if (!resolvedModel) {
|
|
25792
|
+
if (resumeModelBehavior === "keepSessionModel") {
|
|
25793
|
+
try {
|
|
25794
|
+
const recovery = await resolveMissingSessionModelRecovery({
|
|
25795
|
+
modelRegistry: this.#modelRegistry,
|
|
25796
|
+
settings: this.settings,
|
|
25797
|
+
defaultEntries,
|
|
25798
|
+
skips: resolution.skips,
|
|
25799
|
+
savedDefault: sessionContext.models.default,
|
|
25800
|
+
credentialSessionId: this.credentialSessionId,
|
|
25801
|
+
...(this.#persistedModelProfileAliasIntent("default") ?? {}),
|
|
25802
|
+
});
|
|
25803
|
+
if (recovery?.model) {
|
|
25804
|
+
const installedProfileKeys =
|
|
25805
|
+
this.#activeProfileInstalledRoles.size > 0 ||
|
|
25806
|
+
this.#activeProfileInstalledAgentOverrides.size > 0;
|
|
25807
|
+
const recoveryNeedsBindings =
|
|
25808
|
+
switchingToDifferentSession ||
|
|
25809
|
+
targetActiveModelProfile !== recovery.profileName ||
|
|
25810
|
+
!installedProfileKeys;
|
|
25811
|
+
if (recoveryNeedsBindings) {
|
|
25812
|
+
if (!switchingToDifferentSession)
|
|
25813
|
+
this.#resetSessionScopedModelProfileState({
|
|
25814
|
+
preserveDefaultConfiguredChain: true,
|
|
25815
|
+
force: true,
|
|
25816
|
+
});
|
|
25817
|
+
await applyModelProfileRuntimeBindings({
|
|
25818
|
+
session: this,
|
|
25819
|
+
modelRegistry: this.#modelRegistry,
|
|
25820
|
+
settings: this.settings,
|
|
25821
|
+
profileName: recovery.profileName,
|
|
25822
|
+
});
|
|
25823
|
+
targetActiveModelProfile = recovery.profileName;
|
|
25824
|
+
targetProfileRuntimeInstalled = true;
|
|
25825
|
+
}
|
|
25826
|
+
this.installRecoveredDefaultFallbackChain(
|
|
25827
|
+
recovery.entries,
|
|
25828
|
+
recovery.profileName,
|
|
25829
|
+
recovery.activeIndex,
|
|
25830
|
+
recovery.skips,
|
|
25831
|
+
{ emitResolutionEvent: false },
|
|
25832
|
+
);
|
|
25833
|
+
controller = this.#defaultFallbackChain(false);
|
|
25834
|
+
resolvedModel = recovery.model;
|
|
25835
|
+
recoveredThinkingLevel = recovery.explicitThinkingLevel ? recovery.thinkingLevel : undefined;
|
|
25836
|
+
recoveredDefaultChainMessage =
|
|
25837
|
+
"Saved session model is no longer registered; restored the durable default preset instead.";
|
|
25838
|
+
} else if (recovery) {
|
|
25839
|
+
durableDefaultRecoveryError = "durable default preset resolution failed";
|
|
25840
|
+
}
|
|
25841
|
+
} catch (error) {
|
|
25842
|
+
// The saved chain remains authoritative on recovery failure, but the
|
|
25843
|
+
// durable preset's diagnostic is actionable and must not disappear.
|
|
25844
|
+
logger.warn("Failed to recover saved session model through durable default", {
|
|
25845
|
+
error: safeErrorDescription(error),
|
|
25846
|
+
});
|
|
25847
|
+
durableDefaultRecoveryError = "durable default preset resolution failed";
|
|
25848
|
+
}
|
|
25849
|
+
}
|
|
25850
|
+
if (!resolvedModel) {
|
|
25851
|
+
unavailableDefaultChainMessage = this.#fallbackExhaustionError(controller);
|
|
25852
|
+
if (durableDefaultRecoveryError)
|
|
25853
|
+
unavailableDefaultChainMessage += `; durable default preset recovery failed: ${durableDefaultRecoveryError}`;
|
|
25854
|
+
throw new Error(unavailableDefaultChainMessage);
|
|
25855
|
+
}
|
|
23872
25856
|
}
|
|
23873
|
-
if (!this.model || !modelsAreEqual(this.model,
|
|
23874
|
-
this.#setModelAuthoritatively(
|
|
25857
|
+
if (!this.model || !modelsAreEqual(this.model, resolvedModel)) {
|
|
25858
|
+
this.#setModelAuthoritatively(resolvedModel, "restore");
|
|
23875
25859
|
}
|
|
23876
25860
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
23877
25861
|
// No thinking-level write here: the recompute below is the single
|
|
@@ -23879,6 +25863,11 @@ export class AgentSession {
|
|
|
23879
25863
|
// would append a stray thinking_level_change entry that flips
|
|
23880
25864
|
// hasThinkingEntry and changes what the recompute restores.
|
|
23881
25865
|
}
|
|
25866
|
+
// The saved chain may need its profile identity for alias resolution, but
|
|
25867
|
+
// a cross-file transition must not advertise that profile after its
|
|
25868
|
+
// runtime role layer was removed. Otherwise delegation prompt state and
|
|
25869
|
+
// actual role routing diverge.
|
|
25870
|
+
if (!targetProfileRuntimeInstalled) this.#activeModelProfile = undefined;
|
|
23882
25871
|
|
|
23883
25872
|
const hasThinkingEntry = this.sessionManager
|
|
23884
25873
|
.getBranch()
|
|
@@ -23887,10 +25876,17 @@ export class AgentSession {
|
|
|
23887
25876
|
.getBranch()
|
|
23888
25877
|
.some(entry => entry.type === "service_tier_change");
|
|
23889
25878
|
const defaultThinkingLevel = this.settings.get("defaultThinkingLevel");
|
|
25879
|
+
const hasExplicitDefaultThinkingLevel = this.settings.has("defaultThinkingLevel");
|
|
23890
25880
|
const configuredServiceTier = this.settings.get("serviceTier");
|
|
23891
|
-
const
|
|
23892
|
-
|
|
23893
|
-
|
|
25881
|
+
const sessionThinkingLevel = sessionContext.thinkingLevel as ThinkingLevel | undefined;
|
|
25882
|
+
const recoveredModelThinkingLevel =
|
|
25883
|
+
recoveredDefaultChainMessage && !hasExplicitDefaultThinkingLevel
|
|
25884
|
+
? this.model?.thinking?.defaultLevel
|
|
25885
|
+
: undefined;
|
|
25886
|
+
const persistedThinkingLevel =
|
|
25887
|
+
hasThinkingEntry && sessionThinkingLevel !== ThinkingLevel.Inherit
|
|
25888
|
+
? sessionThinkingLevel
|
|
25889
|
+
: (recoveredThinkingLevel ?? recoveredModelThinkingLevel ?? defaultThinkingLevel);
|
|
23894
25890
|
const nextThinkingLevel = resolveThinkingLevelForModel(
|
|
23895
25891
|
this.model,
|
|
23896
25892
|
persistedThinkingLevel === ThinkingLevel.Inherit
|
|
@@ -23910,16 +25906,15 @@ export class AgentSession {
|
|
|
23910
25906
|
: configuredServiceTier === "none"
|
|
23911
25907
|
? undefined
|
|
23912
25908
|
: configuredServiceTier;
|
|
23913
|
-
// Switching to another session file must not carry the predecessor's
|
|
23914
|
-
// profile marker or role overrides into the successor; the successor's
|
|
23915
|
-
// own configured model is restored above.
|
|
23916
|
-
if (switchingToDifferentSession) this.#resetSessionScopedModelProfileState();
|
|
23917
25909
|
// Establish the successor's durable session identity only after every
|
|
23918
25910
|
// restored state facet is live. Identity-bound extension hooks run below.
|
|
23919
25911
|
await this.sessionManager.ensureOnDisk();
|
|
23920
25912
|
if (!switchingToDifferentSession) await this.#initializeLocalRootForLoadedSession();
|
|
23921
25913
|
|
|
23922
25914
|
if (switchingToDifferentSession) {
|
|
25915
|
+
// Profile cleanup changed the active role layer; refresh eager task
|
|
25916
|
+
// delegation and its base prompt before reconnecting the successor.
|
|
25917
|
+
await this.syncEagerDelegation();
|
|
23923
25918
|
// The local:// migration gate for this successor already ran above,
|
|
23924
25919
|
// before the identity was published (#2797 / #2925).
|
|
23925
25920
|
this.#resetHindsightConversationTrackingIfHindsight();
|
|
@@ -23931,6 +25926,11 @@ export class AgentSession {
|
|
|
23931
25926
|
previousSessionState.sessionId,
|
|
23932
25927
|
previousSessionState.sessionFile,
|
|
23933
25928
|
);
|
|
25929
|
+
exactRetirement = await this.#prepareExactMcpControlRetirement();
|
|
25930
|
+
if (exactRetirement && !exactRetirement.canCommit())
|
|
25931
|
+
throw new Error("Exact MCP control retirement changed before session switch commit");
|
|
25932
|
+
await exactRetirement?.commit();
|
|
25933
|
+
transitionCleanupCommitted = true;
|
|
23934
25934
|
let ownerShutdownSettled = true;
|
|
23935
25935
|
if (ownerShutdownManager && ownerShutdownLease && ownerId) {
|
|
23936
25936
|
try {
|
|
@@ -23959,7 +25959,18 @@ export class AgentSession {
|
|
|
23959
25959
|
);
|
|
23960
25960
|
}
|
|
23961
25961
|
}
|
|
23962
|
-
|
|
25962
|
+
// The successor identity is committed. Settle consumed tracked inputs
|
|
25963
|
+
// before reconnecting the successor or running session-switch hooks,
|
|
25964
|
+
// either of which can outlive the predecessor event bridge.
|
|
25965
|
+
this.#terminalizeQueuedSdkWorkForSessionTransition(
|
|
25966
|
+
[
|
|
25967
|
+
...previousAgentSteeringQueue,
|
|
25968
|
+
...previousAgentFollowUpQueue,
|
|
25969
|
+
...previousDeferredSdkFollowUps,
|
|
25970
|
+
...previousPendingNextTurnMessages.map(entry => entry.message),
|
|
25971
|
+
],
|
|
25972
|
+
previousTrackedQueuedInputs,
|
|
25973
|
+
);
|
|
23963
25974
|
// Different files may intentionally carry the same copied session id; pathname transition is the commit signal.
|
|
23964
25975
|
ownerShutdownTransitionCommitted = true;
|
|
23965
25976
|
if (ownerShutdownSettled) {
|
|
@@ -23982,22 +25993,39 @@ export class AgentSession {
|
|
|
23982
25993
|
// messages, model state, MCP selections, the agent subscription, and
|
|
23983
25994
|
// session-scoped tool cleanup are complete.
|
|
23984
25995
|
if (this.#extensionRunner) {
|
|
23985
|
-
await this.#
|
|
23986
|
-
|
|
23987
|
-
|
|
23988
|
-
|
|
23989
|
-
|
|
23990
|
-
|
|
25996
|
+
await this.#runCommittedSuccessorHook(() =>
|
|
25997
|
+
this.#extensionRunner!.emit({
|
|
25998
|
+
type: "session_switch",
|
|
25999
|
+
reason: "resume",
|
|
26000
|
+
previousSessionFile,
|
|
26001
|
+
...(options?.transition ? { transition: options.transition } : {}),
|
|
26002
|
+
}),
|
|
26003
|
+
);
|
|
23991
26004
|
}
|
|
23992
|
-
this.#terminalizeQueuedSdkWorkForSessionTransition(
|
|
23993
|
-
|
|
23994
|
-
|
|
23995
|
-
|
|
23996
|
-
|
|
23997
|
-
|
|
26005
|
+
this.#terminalizeQueuedSdkWorkForSessionTransition(
|
|
26006
|
+
[
|
|
26007
|
+
...previousAgentSteeringQueue,
|
|
26008
|
+
...previousAgentFollowUpQueue,
|
|
26009
|
+
...previousDeferredSdkFollowUps,
|
|
26010
|
+
...previousPendingNextTurnMessages.map(entry => entry.message),
|
|
26011
|
+
],
|
|
26012
|
+
previousTrackedQueuedInputs,
|
|
26013
|
+
);
|
|
26014
|
+
const predecessorDeferred = new Set(previousDeferredSdkFollowUps);
|
|
26015
|
+
this.#deferredSdkFollowUps = this.#deferredSdkFollowUps.filter(
|
|
26016
|
+
message => !predecessorDeferred.has(message),
|
|
26017
|
+
);
|
|
26018
|
+
if (recoveredDefaultChainMessage) this.#emitResolutionFallbackSwitch(this.#defaultFallbackChain(false));
|
|
26019
|
+
if (recoveredDefaultChainMessage) this.emitNotice("warning", recoveredDefaultChainMessage, "fallback");
|
|
23998
26020
|
return true;
|
|
23999
26021
|
} catch (error) {
|
|
24000
26022
|
if (transitionCleanupCommitted) throw error;
|
|
26023
|
+
let exactRetirementAbortError: unknown;
|
|
26024
|
+
try {
|
|
26025
|
+
await exactRetirement?.abort();
|
|
26026
|
+
} catch (abortError) {
|
|
26027
|
+
exactRetirementAbortError = abortError;
|
|
26028
|
+
}
|
|
24001
26029
|
// The switch never committed: rotate the manager's endpoint
|
|
24002
26030
|
// registration back to the predecessor before restoring it
|
|
24003
26031
|
// (review thread P1 — the map key must track the session id).
|
|
@@ -24027,9 +26055,22 @@ export class AgentSession {
|
|
|
24027
26055
|
`Session switch rollback aborted: predecessor endpoint "${previousSessionState.sessionId}" is no longer owned by this session.`,
|
|
24028
26056
|
);
|
|
24029
26057
|
}
|
|
24030
|
-
|
|
24031
|
-
|
|
26058
|
+
let sessionRestoreError: unknown;
|
|
26059
|
+
try {
|
|
26060
|
+
await this.sessionManager.restoreRollbackState(previousSessionState);
|
|
26061
|
+
} catch (restoreError) {
|
|
26062
|
+
sessionRestoreError = restoreError;
|
|
26063
|
+
}
|
|
26064
|
+
this.restoreDefaultFallbackRuntimeState(previousDefaultFallbackRuntimeState);
|
|
24032
26065
|
this.#syncAgentSessionId(previousSessionState.sessionId);
|
|
26066
|
+
if (previousModelRolesOverride === undefined) this.settings.clearOverride("modelRoles");
|
|
26067
|
+
else this.settings.override("modelRoles", previousModelRolesOverride);
|
|
26068
|
+
if (previousAgentModelOverridesOverride === undefined)
|
|
26069
|
+
this.settings.clearOverride("task.agentModelOverrides");
|
|
26070
|
+
else this.settings.override("task.agentModelOverrides", previousAgentModelOverridesOverride);
|
|
26071
|
+
this.#activeProfileInstalledRoles = new Map(previousActiveProfileInstalledRoles);
|
|
26072
|
+
this.#activeProfileInstalledAgentOverrides = new Map(previousActiveProfileInstalledAgentOverrides);
|
|
26073
|
+
this.#preProfileModel = previousPreProfileModel;
|
|
24033
26074
|
this.#activeModelProfile = previousActiveModelProfile;
|
|
24034
26075
|
this.#restoreWorkflowGateEmitter(suspendedWorkflowGateEmitter);
|
|
24035
26076
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
@@ -24067,9 +26108,8 @@ export class AgentSession {
|
|
|
24067
26108
|
this.agent.clearAllQueues();
|
|
24068
26109
|
this.agent.restoreSteering(previousAgentSteeringQueue);
|
|
24069
26110
|
this.agent.restoreFollowUp(previousAgentFollowUpQueue);
|
|
24070
|
-
|
|
24071
|
-
|
|
24072
|
-
}
|
|
26111
|
+
this.#setAgentModelWithReasoningContext(previousModel);
|
|
26112
|
+
this.#syncAppendOnlyContext(previousModel);
|
|
24073
26113
|
this.#thinkingLevelMutationRevision++;
|
|
24074
26114
|
this.#thinkingLevelLiveMutationRevision++;
|
|
24075
26115
|
this.#pendingThinkingLevelControlSuccess = undefined;
|
|
@@ -24081,8 +26121,17 @@ export class AgentSession {
|
|
|
24081
26121
|
this.agent.serviceTier = previousServiceTier;
|
|
24082
26122
|
this.#syncTodoPhasesFromBranch();
|
|
24083
26123
|
this.#reconnectToAgent();
|
|
24084
|
-
|
|
24085
|
-
|
|
26124
|
+
const rollbackErrors = [exactRetirementAbortError, sessionRestoreError, restoreMcpError].filter(
|
|
26125
|
+
value => value !== undefined,
|
|
26126
|
+
);
|
|
26127
|
+
if (rollbackErrors.length > 0) {
|
|
26128
|
+
if (rollbackErrors.length > 1) {
|
|
26129
|
+
throw new AggregateError(
|
|
26130
|
+
[error, ...rollbackErrors],
|
|
26131
|
+
"Session switch and exact MCP rollback both failed",
|
|
26132
|
+
);
|
|
26133
|
+
}
|
|
26134
|
+
throw new AggregateError([error, rollbackErrors[0]], "Session switch rollback failed");
|
|
24086
26135
|
}
|
|
24087
26136
|
if (unavailableDefaultChainMessage) {
|
|
24088
26137
|
this.emitNotice(
|
|
@@ -24120,19 +26169,19 @@ export class AgentSession {
|
|
|
24120
26169
|
}> {
|
|
24121
26170
|
this.#beginSessionTransition("branch");
|
|
24122
26171
|
try {
|
|
24123
|
-
const previousSessionFile = this.sessionFile;
|
|
24124
|
-
const previousWorkflowGateSessionId = this.sessionId;
|
|
24125
|
-
const previousSessionIdentity = this.sessionManager.getSessionId();
|
|
24126
26172
|
const selectedEntry = this.sessionManager.getEntryForFidelity(entryId);
|
|
24127
|
-
|
|
24128
26173
|
if (selectedEntry?.type !== "message" || selectedEntry.message.role !== "user") {
|
|
24129
26174
|
throw new Error("Invalid entry ID for branching");
|
|
24130
26175
|
}
|
|
26176
|
+
const previousSessionFile = this.sessionFile;
|
|
26177
|
+
const previousWorkflowGateSessionId = this.sessionId;
|
|
26178
|
+
const previousSessionIdentity = this.sessionManager.getSessionId();
|
|
24131
26179
|
|
|
24132
26180
|
const selectedText = this.#extractUserMessageText(selectedEntry.message.content);
|
|
24133
26181
|
const previousAgentSteeringQueue = this.agent.snapshotSteering();
|
|
24134
26182
|
const previousAgentFollowUpQueue = this.agent.snapshotFollowUp();
|
|
24135
26183
|
const previousDeferredSdkFollowUps = [...this.#deferredSdkFollowUps];
|
|
26184
|
+
const previousPendingNextTurnMessages = [...this.#pendingNextTurnMessages];
|
|
24136
26185
|
|
|
24137
26186
|
let skipConversationRestore = false;
|
|
24138
26187
|
|
|
@@ -24149,21 +26198,27 @@ export class AgentSession {
|
|
|
24149
26198
|
skipConversationRestore = result?.skipConversationRestore ?? false;
|
|
24150
26199
|
}
|
|
24151
26200
|
|
|
26201
|
+
this.#settleTrackedQueuedInputsBeforeAgentDisconnect();
|
|
26202
|
+
this.#disconnectFromAgent();
|
|
26203
|
+
await this.#settleActivePromptForHistoryTransition();
|
|
26204
|
+
|
|
24152
26205
|
// Flush pending writes before preparing the successor.
|
|
24153
26206
|
await this.sessionManager.flush();
|
|
24154
26207
|
const prepared = selectedEntry.parentId
|
|
24155
26208
|
? await this.sessionManager.prepareBranchedSession(selectedEntry.parentId)
|
|
24156
26209
|
: await this.sessionManager.prepareNewSession({ parentSession: previousSessionFile });
|
|
26210
|
+
let exactRetirement: PreparedExactMcpRetirement | undefined;
|
|
24157
26211
|
try {
|
|
24158
26212
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
24159
26213
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
24160
26214
|
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
26215
|
+
exactRetirement = await this.#prepareExactMcpControlRetirement();
|
|
26216
|
+
if (exactRetirement && !exactRetirement.canCommit())
|
|
26217
|
+
throw new Error("Exact MCP control retirement changed before session adoption");
|
|
24161
26218
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
24162
|
-
|
|
24163
|
-
// manager under it (review thread P1).
|
|
24164
|
-
this.#rekeyJobManagerForSessionIdentity(previousSessionIdentity, previousSessionFile);
|
|
24165
|
-
await this.#runToolSessionTransitionCleanups();
|
|
26219
|
+
await exactRetirement?.commit();
|
|
24166
26220
|
} catch (error) {
|
|
26221
|
+
await exactRetirement?.abort();
|
|
24167
26222
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
24168
26223
|
}
|
|
24169
26224
|
this.#pendingNextTurnMessages = [];
|
|
@@ -24172,6 +26227,7 @@ export class AgentSession {
|
|
|
24172
26227
|
...previousAgentSteeringQueue,
|
|
24173
26228
|
...previousAgentFollowUpQueue,
|
|
24174
26229
|
...previousDeferredSdkFollowUps,
|
|
26230
|
+
...previousPendingNextTurnMessages.map(entry => entry.message),
|
|
24175
26231
|
]);
|
|
24176
26232
|
this.#deferredSdkFollowUps = [];
|
|
24177
26233
|
this.#resetActiveSdkRunOwnership();
|
|
@@ -24190,8 +26246,6 @@ export class AgentSession {
|
|
|
24190
26246
|
// Reload messages from entries (works for both file and in-memory mode)
|
|
24191
26247
|
const sessionContext = this.buildDisplaySessionContext();
|
|
24192
26248
|
|
|
24193
|
-
await this.#restoreMCPSelectionsForSessionContext(sessionContext);
|
|
24194
|
-
|
|
24195
26249
|
if (!skipConversationRestore) {
|
|
24196
26250
|
this.agent.replaceMessages(sessionContext.messages, {
|
|
24197
26251
|
historyRewrite: { reason: "session-branch", preserveSeededPrefix: true },
|
|
@@ -24201,17 +26255,26 @@ export class AgentSession {
|
|
|
24201
26255
|
}
|
|
24202
26256
|
|
|
24203
26257
|
this.#resetIrcRosterDeliveryState();
|
|
26258
|
+
// Once committed, establish the successor's identity and prompt boundary
|
|
26259
|
+
// before fallible post-commit integrations. A cleanup or MCP failure must
|
|
26260
|
+
// not leave the next turn running with the parent's messages/session id.
|
|
26261
|
+
this.#rekeyJobManagerForSessionIdentity(previousSessionIdentity, previousSessionFile);
|
|
26262
|
+
await this.#runToolSessionTransitionCleanups();
|
|
26263
|
+
await this.#restoreMCPSelectionsForSessionContext(sessionContext);
|
|
24204
26264
|
// session_branch is the post-commit identity signal. Publish it only after
|
|
24205
26265
|
// the successor's messages and MCP selections are restored.
|
|
24206
26266
|
if (this.#extensionRunner) {
|
|
24207
|
-
await this.#
|
|
24208
|
-
|
|
24209
|
-
|
|
24210
|
-
|
|
26267
|
+
await this.#runCommittedSuccessorHook(() =>
|
|
26268
|
+
this.#extensionRunner!.emit({
|
|
26269
|
+
type: "session_branch",
|
|
26270
|
+
previousSessionFile,
|
|
26271
|
+
}),
|
|
26272
|
+
);
|
|
24211
26273
|
}
|
|
24212
26274
|
|
|
24213
26275
|
return { selectedText, cancelled: false };
|
|
24214
26276
|
} finally {
|
|
26277
|
+
this.#reconnectToAgent();
|
|
24215
26278
|
this.#endSessionTransition();
|
|
24216
26279
|
}
|
|
24217
26280
|
}
|
|
@@ -24246,22 +26309,14 @@ export class AgentSession {
|
|
|
24246
26309
|
this.#beginSessionTransition("navigate-tree");
|
|
24247
26310
|
try {
|
|
24248
26311
|
const oldLeafId = this.sessionManager.getLeafId();
|
|
24249
|
-
|
|
24250
|
-
// No-op if already at target
|
|
24251
|
-
if (targetId === oldLeafId) {
|
|
24252
|
-
return { cancelled: false };
|
|
24253
|
-
}
|
|
24254
|
-
|
|
24255
|
-
// Model required for summarization
|
|
26312
|
+
if (targetId === oldLeafId) return { cancelled: false };
|
|
24256
26313
|
if (options.summarize && !this.model) {
|
|
24257
26314
|
throw new Error("No model available for summarization");
|
|
24258
26315
|
}
|
|
24259
|
-
|
|
24260
26316
|
const targetEntry = this.sessionManager.getEntryForFidelity(targetId);
|
|
24261
26317
|
if (!targetEntry) {
|
|
24262
26318
|
throw new Error(`Entry ${targetId} not found`);
|
|
24263
26319
|
}
|
|
24264
|
-
|
|
24265
26320
|
// Collect entries to summarize (from old leaf to common ancestor).
|
|
24266
26321
|
const { entries: collectedEntriesToSummarize, commonAncestorId } = collectEntriesForBranchSummary(
|
|
24267
26322
|
this.sessionManager,
|
|
@@ -24302,6 +26357,10 @@ export class AgentSession {
|
|
|
24302
26357
|
}
|
|
24303
26358
|
}
|
|
24304
26359
|
|
|
26360
|
+
this.#settleTrackedQueuedInputsBeforeAgentDisconnect();
|
|
26361
|
+
this.#disconnectFromAgent();
|
|
26362
|
+
await this.#settleActivePromptForHistoryTransition();
|
|
26363
|
+
|
|
24305
26364
|
// Run default summarizer if needed
|
|
24306
26365
|
let summaryText: string | undefined;
|
|
24307
26366
|
let summaryDetails: unknown;
|
|
@@ -24388,6 +26447,9 @@ export class AgentSession {
|
|
|
24388
26447
|
const queuedSdkWork = this.#queuedMessagesForSessionTransition();
|
|
24389
26448
|
this.#terminalizeQueuedSdkWorkForSessionTransition(queuedSdkWork);
|
|
24390
26449
|
this.#deferredSdkFollowUps = [];
|
|
26450
|
+
this.#pendingNextTurnMessages = [];
|
|
26451
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
26452
|
+
this.#resetActiveSdkRunOwnership();
|
|
24391
26453
|
this.agent.clearAllQueues();
|
|
24392
26454
|
this.#steeringMessages = [];
|
|
24393
26455
|
this.#followUpMessages = [];
|
|
@@ -24409,18 +26471,21 @@ export class AgentSession {
|
|
|
24409
26471
|
// the emit and the context rebuild when no handlers are registered (mirrors
|
|
24410
26472
|
// the session_before_tree guard above).
|
|
24411
26473
|
if (this.#extensionRunner?.hasHandlers("session_tree")) {
|
|
24412
|
-
await this.#
|
|
24413
|
-
|
|
24414
|
-
|
|
24415
|
-
|
|
24416
|
-
|
|
24417
|
-
|
|
24418
|
-
|
|
26474
|
+
await this.#runCommittedSuccessorHook(() =>
|
|
26475
|
+
this.#extensionRunner!.emit({
|
|
26476
|
+
type: "session_tree",
|
|
26477
|
+
newLeafId: this.sessionManager.getLeafId(),
|
|
26478
|
+
oldLeafId,
|
|
26479
|
+
summaryEntry,
|
|
26480
|
+
fromExtension: summaryText ? fromExtension : undefined,
|
|
26481
|
+
}),
|
|
26482
|
+
);
|
|
24419
26483
|
const refreshedContext = this.buildDisplaySessionContext();
|
|
24420
26484
|
return { editorText, cancelled: false, summaryEntry, sessionContext: refreshedContext };
|
|
24421
26485
|
}
|
|
24422
26486
|
return { editorText, cancelled: false, summaryEntry, sessionContext: displayContext };
|
|
24423
26487
|
} finally {
|
|
26488
|
+
this.#reconnectToAgent();
|
|
24424
26489
|
this.#endSessionTransition();
|
|
24425
26490
|
}
|
|
24426
26491
|
}
|