@gajae-code/coding-agent 0.17.1 → 0.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +266 -0
- package/dist/types/async/job-manager.d.ts +7 -6
- package/dist/types/cli/daemon-cli.d.ts +3 -1
- package/dist/types/cli/github-release.d.ts +17 -0
- package/dist/types/cli/public-command-entry.d.ts +56 -0
- package/dist/types/cli/public-command-errors.d.ts +126 -0
- package/dist/types/cli/public-command-evidence.d.ts +99 -0
- package/dist/types/cli/public-command-help.d.ts +65 -0
- package/dist/types/cli/public-command-registry.d.ts +69 -0
- package/dist/types/cli/session-picker.d.ts +1 -1
- package/dist/types/cli/setup-cli.d.ts +1 -0
- package/dist/types/cli/skills-cli.d.ts +5 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +4 -50
- package/dist/types/commands/sdk.d.ts +20 -37
- package/dist/types/commands/setup.d.ts +3 -0
- package/dist/types/commands/skills.d.ts +9 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/file-lock.d.ts +10 -1
- package/dist/types/config/model-profile-activation.d.ts +63 -4
- package/dist/types/config/model-registry.d.ts +20 -2
- package/dist/types/config/settings-schema.d.ts +11 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
- package/dist/types/coordinator-mcp/server.d.ts +3 -0
- package/dist/types/daemon/operator-contract.d.ts +5 -1
- package/dist/types/eval/js/worker-protocol.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +12 -0
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +18 -0
- package/dist/types/extensibility/shared-events.d.ts +3 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
- package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/parse.d.ts +9 -1
- package/dist/types/main.d.ts +22 -0
- package/dist/types/modes/acp/acp-agent.d.ts +94 -1
- package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
- package/dist/types/modes/components/session-selector.d.ts +5 -2
- package/dist/types/modes/components/thinking-selector.d.ts +2 -1
- package/dist/types/modes/components/user-message-selector.d.ts +5 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/runtime/process-lifecycle.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +56 -0
- package/dist/types/runtime-mcp/pool.d.ts +17 -0
- package/dist/types/runtime-mcp/redaction.d.ts +20 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
- package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
- package/dist/types/sdk/acp/adapter.d.ts +27 -0
- package/dist/types/sdk/acp/final-text.d.ts +8 -0
- package/dist/types/sdk/broker/broker.d.ts +13 -2
- package/dist/types/sdk/broker/ensure.d.ts +46 -1
- package/dist/types/sdk/broker/identity.d.ts +3 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
- package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
- package/dist/types/sdk/broker/runtime.d.ts +1 -1
- package/dist/types/sdk/broker/session-index.d.ts +22 -3
- package/dist/types/sdk/broker/spawn-authority.d.ts +4 -0
- package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
- package/dist/types/sdk/bus/index.d.ts +19 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
- package/dist/types/sdk/cli/master-cli.d.ts +6 -10
- package/dist/types/sdk/cli/rows.d.ts +19 -1
- package/dist/types/sdk/cli/session-cli.d.ts +22 -5
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/guides/cli.d.ts +3 -4
- package/dist/types/sdk/guides/verify.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +9 -0
- package/dist/types/sdk/host/session-runtime.d.ts +31 -0
- package/dist/types/sdk/host/types.d.ts +10 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +83 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +38 -1
- package/dist/types/sdk/prompt-failure.d.ts +30 -1
- package/dist/types/sdk/prompt-status.d.ts +8 -0
- package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
- package/dist/types/sdk/router/replay-retention-gap-cache.d.ts +13 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -0
- package/dist/types/sdk/transport/relay.d.ts +1 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +73 -2
- package/dist/types/sdk/transport/stdio.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +168 -21
- package/dist/types/session/fallback-chain-controller.d.ts +6 -6
- package/dist/types/session/fold-coordinator.d.ts +17 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
- package/dist/types/session/session-manager.d.ts +64 -1
- package/dist/types/session/session-work-lease.d.ts +14 -0
- package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
- package/dist/types/setup/provider-onboarding.d.ts +81 -1
- package/dist/types/thinking.d.ts +3 -1
- package/dist/types/tools/bash.d.ts +37 -11
- package/dist/types/tools/job.d.ts +1 -1
- package/dist/types/tools/steer-fold.d.ts +31 -0
- package/dist/types/utils/git.d.ts +28 -2
- package/dist/types/utils/mupdf-embedded.d.ts +2 -0
- package/dist/types/utils/mupdf-wasm-embedded.d.ts +2 -0
- package/dist/types/utils/mupdf.d.ts +4 -0
- package/dist/types/utils/session-title-generation.d.ts +6 -0
- package/dist/types/utils/title-generator.d.ts +11 -2
- package/dist/types/web/scrapers/types.d.ts +3 -0
- package/package.json +18 -10
- package/scripts/build-binary.ts +9 -3
- package/scripts/compile-args.ts +0 -9
- package/scripts/embed-mupdf.ts +52 -0
- package/scripts/generate-sdk-operation-inventory.ts +31 -1
- package/scripts/vendor-markit.ts +196 -0
- package/src/async/job-manager.ts +60 -9
- package/src/cli/daemon-cli.ts +53 -18
- package/src/cli/github-release.ts +87 -5
- package/src/cli/mcp-cli.ts +38 -4
- package/src/cli/public-command-entry.ts +450 -0
- package/src/cli/public-command-errors.ts +581 -0
- package/src/cli/public-command-evidence.ts +717 -0
- package/src/cli/public-command-help.ts +360 -0
- package/src/cli/public-command-registry.ts +535 -0
- package/src/cli/session-picker.ts +2 -1
- package/src/cli/setup-cli.ts +9 -1
- package/src/cli/skills-cli.ts +46 -2
- package/src/cli.ts +61 -1
- package/src/commands/daemon.ts +65 -69
- package/src/commands/sdk.ts +232 -100
- package/src/commands/setup.ts +2 -0
- package/src/commands/skills.ts +11 -0
- package/src/config/autorouting-tier-map.ts +16 -0
- package/src/config/file-lock.ts +199 -68
- package/src/config/model-profile-activation.ts +406 -53
- package/src/config/model-registry.ts +204 -25
- package/src/config/settings-schema.ts +26 -2
- package/src/coordinator-mcp/policy.ts +10 -5
- package/src/coordinator-mcp/question-gate-codec.ts +17 -0
- package/src/coordinator-mcp/question-state.ts +175 -8
- package/src/coordinator-mcp/server.ts +528 -87
- package/src/coordinator-mcp/session-reaper.ts +6 -4
- package/src/crash/record-loader.ts +6 -2
- package/src/daemon/operator-contract.ts +26 -12
- package/src/debug/report-bundle.ts +114 -24
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
- package/src/eval/js/context-manager.ts +8 -8
- package/src/eval/js/worker-core.ts +9 -4
- package/src/eval/js/worker-protocol.ts +2 -0
- package/src/extensibility/extensions/runner.ts +52 -4
- package/src/extensibility/extensions/types.ts +10 -0
- package/src/extensibility/gjc-plugins/registry.ts +196 -37
- package/src/extensibility/runtime-skill-discovery.ts +61 -3
- package/src/extensibility/shared-events.ts +3 -1
- package/src/gjc-runtime/managed-owner-admission.ts +67 -50
- package/src/gjc-runtime/session-state-lock.ts +165 -24
- package/src/gjc-runtime/ultragoal-succession.ts +31 -3
- package/src/internal-urls/docs-index.generated.ts +21 -19
- package/src/internal-urls/embedded-protocol.ts +83 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/parse.ts +21 -1
- package/src/internal-urls/router.ts +4 -2
- package/src/main.ts +123 -55
- package/src/modes/acp/acp-agent.ts +593 -47
- package/src/modes/acp/acp-event-mapper.ts +9 -0
- package/src/modes/acp/startup-provenance.ts +59 -0
- package/src/modes/components/custom-provider-wizard.ts +308 -7
- package/src/modes/components/model-selector.ts +31 -15
- package/src/modes/components/oauth-selector.ts +9 -0
- package/src/modes/components/session-selector.ts +47 -4
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/components/thinking-selector.ts +7 -3
- package/src/modes/components/user-message-selector.ts +57 -15
- package/src/modes/controllers/command-controller.ts +58 -32
- package/src/modes/controllers/extension-ui-controller.ts +33 -7
- package/src/modes/controllers/input-controller.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +177 -18
- package/src/modes/interactive-mode.ts +21 -7
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
- package/src/modes/types.ts +2 -2
- package/src/prompts/agent-fragments/browser-aside.md +1 -0
- package/src/prompts/system/title-system.md +1 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/read.md +2 -0
- package/src/runtime/network-prewarm-service.test.ts +36 -1
- package/src/runtime/network-prewarm-service.ts +28 -3
- package/src/runtime/process-lifecycle.ts +22 -11
- package/src/runtime-mcp/config.ts +16 -1
- package/src/runtime-mcp/manager.ts +689 -55
- package/src/runtime-mcp/pool.test.ts +31 -0
- package/src/runtime-mcp/pool.ts +304 -28
- package/src/runtime-mcp/redaction.ts +42 -0
- package/src/runtime-mcp/startup-policy.ts +7 -0
- package/src/runtime-mcp/tool-bridge.ts +71 -0
- package/src/runtime-mcp/transports/http.ts +108 -25
- package/src/sdk/acp/adapter.ts +90 -9
- package/src/sdk/acp/final-text.ts +19 -2
- package/src/sdk/broker/broker.ts +378 -52
- package/src/sdk/broker/ensure.ts +97 -19
- package/src/sdk/broker/identity.ts +9 -1
- package/src/sdk/broker/lifecycle-ledger.ts +95 -31
- package/src/sdk/broker/lifecycle.ts +690 -141
- package/src/sdk/broker/runtime.ts +1 -1
- package/src/sdk/broker/session-index.ts +97 -4
- package/src/sdk/broker/spawn-authority.ts +35 -7
- package/src/sdk/broker/stderr-drainer.ts +59 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/index.ts +721 -84
- package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -3
- package/src/sdk/cli/master-cli.ts +47 -36
- package/src/sdk/cli/rows.ts +30 -7
- package/src/sdk/cli/session-cli.ts +687 -184
- package/src/sdk/client/client.ts +18 -0
- package/src/sdk/guides/catalog.ts +2 -2
- package/src/sdk/guides/cli.ts +20 -62
- package/src/sdk/guides/verify.ts +14 -0
- package/src/sdk/host/host.ts +49 -5
- package/src/sdk/host/session-runtime.test.ts +636 -9
- package/src/sdk/host/session-runtime.ts +499 -118
- package/src/sdk/host/types.ts +12 -1
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
- package/src/sdk/host/websocket-transport.ts +40 -6
- package/src/sdk/index.ts +11 -0
- package/src/sdk/prompt-deadline-flush.ts +334 -0
- package/src/sdk/prompt-deadline-manager.ts +136 -4
- package/src/sdk/prompt-failure.ts +43 -1
- package/src/sdk/prompt-status.ts +8 -0
- package/src/sdk/prompt-tool-boundary.ts +121 -0
- package/src/sdk/protocol/operation-inventory.generated.json +106 -0
- package/src/sdk/router/replay-retention-gap-cache.ts +27 -0
- package/src/sdk/router/session-router.ts +31 -4
- package/src/sdk/session.ts +194 -16
- package/src/sdk/startup-capability.ts +1 -1
- package/src/sdk/transport/relay.ts +71 -6
- package/src/sdk/transport/serve-cli.ts +494 -49
- package/src/sdk/transport/socket.ts +12 -11
- package/src/sdk/transport/stdio.ts +7 -3
- package/src/session/agent-session.ts +2437 -400
- package/src/session/fallback-chain-controller.ts +6 -7
- package/src/session/fold-coordinator.ts +24 -5
- package/src/session/internal/managed-session-scope.ts +58 -18
- package/src/session/internal/managed-session-storage.ts +304 -11
- package/src/session/session-manager.ts +382 -44
- package/src/session/session-work-lease.ts +34 -0
- package/src/session-import/codex.ts +12 -3
- package/src/setup/model-onboarding-guidance.ts +1 -1
- package/src/setup/provider-onboarding.ts +397 -24
- package/src/slash-commands/builtin-registry.ts +258 -14
- package/src/task/index.ts +14 -2
- package/src/thinking.ts +21 -2
- package/src/tools/bash.ts +98 -78
- package/src/tools/fetch.ts +119 -20
- package/src/tools/job.ts +151 -25
- package/src/tools/output-meta.ts +15 -2
- package/src/tools/path-utils.ts +27 -3
- package/src/tools/read.ts +3 -3
- package/src/tools/steer-fold.ts +107 -0
- package/src/tools/tool-catalog.generated.ts +29 -2
- package/src/tools/tool-errors.ts +4 -0
- package/src/utils/git.ts +53 -3
- package/src/utils/markit.ts +55 -41
- package/src/utils/mupdf-assets.d.ts +4 -0
- package/src/utils/mupdf-embedded.ts +4 -0
- package/src/utils/mupdf-wasm-embedded.ts +3 -0
- package/src/utils/mupdf-wasm.ts +24 -2
- package/src/utils/mupdf.ts +129 -0
- package/src/utils/session-title-generation.ts +29 -0
- package/src/utils/title-generator.ts +46 -2
- package/src/web/scrapers/types.ts +24 -3
- package/vendor/markit-ai/LICENSE +21 -0
- package/vendor/markit-ai/MANIFEST.json +412 -0
- package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
- package/vendor/markit-ai/dist/commands/config.js +139 -0
- package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
- package/vendor/markit-ai/dist/commands/convert.js +113 -0
- package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/formats.js +92 -0
- package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/init.js +29 -0
- package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/onboard.js +61 -0
- package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
- package/vendor/markit-ai/dist/commands/plugin.js +58 -0
- package/vendor/markit-ai/dist/config.d.ts +26 -0
- package/vendor/markit-ai/dist/config.js +42 -0
- package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
- package/vendor/markit-ai/dist/converters/audio.js +104 -0
- package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
- package/vendor/markit-ai/dist/converters/csv.js +83 -0
- package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/docx.js +57 -0
- package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/epub.js +128 -0
- package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
- package/vendor/markit-ai/dist/converters/github.js +148 -0
- package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/html.js +30 -0
- package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/image.js +124 -0
- package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
- package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
- package/vendor/markit-ai/dist/converters/iwork.js +391 -0
- package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/json.js +21 -0
- package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
- package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
- package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
- package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
- package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
- package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
- package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
- package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
- package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
- package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
- package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
- package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
- package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
- package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
- package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
- package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
- package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/pptx.js +289 -0
- package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
- package/vendor/markit-ai/dist/converters/rss.js +138 -0
- package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
- package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
- package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/xml.js +18 -0
- package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/yaml.js +17 -0
- package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/zip.js +57 -0
- package/vendor/markit-ai/dist/index.d.ts +30 -0
- package/vendor/markit-ai/dist/index.js +26 -0
- package/vendor/markit-ai/dist/main.d.ts +2 -0
- package/vendor/markit-ai/dist/main.js +191 -0
- package/vendor/markit-ai/dist/markit.d.ts +45 -0
- package/vendor/markit-ai/dist/markit.js +267 -0
- package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
- package/vendor/markit-ai/dist/plugins/api.js +42 -0
- package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
- package/vendor/markit-ai/dist/plugins/index.js +3 -0
- package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
- package/vendor/markit-ai/dist/plugins/installer.js +176 -0
- package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
- package/vendor/markit-ai/dist/plugins/loader.js +61 -0
- package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
- package/vendor/markit-ai/dist/plugins/types.js +1 -0
- package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
- package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
- package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
- package/vendor/markit-ai/dist/providers/index.js +56 -0
- package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
- package/vendor/markit-ai/dist/providers/openai.js +76 -0
- package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
- package/vendor/markit-ai/dist/providers/types.js +1 -0
- package/vendor/markit-ai/dist/types.d.ts +36 -0
- package/vendor/markit-ai/dist/types.js +1 -0
- package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
- package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
- package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
- package/vendor/markit-ai/dist/utils/output.js +31 -0
- package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
- package/vendor/markit-ai/dist/utils/turndown.js +70 -0
- package/vendor/markit-ai/package.json +74 -0
- package/vendor/markit-ai.patch +49 -0
|
@@ -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
|
|
@@ -5673,6 +6421,7 @@ export class AgentSession {
|
|
|
5673
6421
|
* Does NOT push to the agent's steering/followUp queue — that happens
|
|
5674
6422
|
* separately inside `sendCustomMessage`. */
|
|
5675
6423
|
enqueueCustomMessageDisplay(text: string, mode: "steer" | "followUp"): string {
|
|
6424
|
+
this.#assertExternalSessionIngress();
|
|
5676
6425
|
const tag = `gjc-cmd-${Date.now()}-${++this.#customDisplayTagCounter}`;
|
|
5677
6426
|
const displayText = text.trim();
|
|
5678
6427
|
if (!displayText) return tag;
|
|
@@ -6011,6 +6760,10 @@ export class AgentSession {
|
|
|
6011
6760
|
}
|
|
6012
6761
|
|
|
6013
6762
|
#trackAgentEvent = (event: AgentEvent): Promise<void> => {
|
|
6763
|
+
const eventScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
|
|
6764
|
+
if ((event.type === "agent_start" || event.type === "turn_start") && eventScope !== undefined) {
|
|
6765
|
+
this.#bindAttemptScopeToActiveRun(eventScope);
|
|
6766
|
+
}
|
|
6014
6767
|
this.#agentEventAdmission.set(event, {
|
|
6015
6768
|
scope: this.#activeAttemptScope,
|
|
6016
6769
|
sdkRunToken: this.#activeSdkRunToken,
|
|
@@ -6369,6 +7122,7 @@ export class AgentSession {
|
|
|
6369
7122
|
// re-enter prompt(), so a successor's running transition serializes after it.
|
|
6370
7123
|
// Do not let a lock-hostile sidecar suppress the terminal event itself.
|
|
6371
7124
|
const terminalPersistence = this.#queueCoordinatorRuntimeStatePersist(event);
|
|
7125
|
+
this.#settleTrackedQueuedInputTerminal(attemptScope);
|
|
6372
7126
|
this.#emit(event);
|
|
6373
7127
|
void terminalPersistence;
|
|
6374
7128
|
await this.#emitExtensionEvent(event);
|
|
@@ -6556,16 +7310,23 @@ export class AgentSession {
|
|
|
6556
7310
|
if (userDisplayDequeueAlreadyHandled) this.#displayDequeueAlreadyHandled = undefined;
|
|
6557
7311
|
if (event.type === "message_start" && event.message.role === "user" && !userDisplayDequeueAlreadyHandled) {
|
|
6558
7312
|
const messageText = userMessageText;
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
7313
|
+
const boundIndex = this.#steeringMessages.findIndex(entry => entry.message === event.message);
|
|
7314
|
+
if (boundIndex !== -1) {
|
|
7315
|
+
this.#steeringMessages.splice(boundIndex, 1);
|
|
7316
|
+
} else {
|
|
7317
|
+
const followUpBoundIndex = this.#followUpMessages.findIndex(entry => entry.message === event.message);
|
|
7318
|
+
if (followUpBoundIndex !== -1) this.#followUpMessages.splice(followUpBoundIndex, 1);
|
|
7319
|
+
else if (messageText) {
|
|
7320
|
+
// Legacy display rows without a bound executable message retain text fallback.
|
|
7321
|
+
const steeringIndex = this.#steeringMessages.findIndex(
|
|
7322
|
+
entry => entry.message === undefined && entry.text === messageText,
|
|
7323
|
+
);
|
|
7324
|
+
if (steeringIndex !== -1) this.#steeringMessages.splice(steeringIndex, 1);
|
|
7325
|
+
else {
|
|
7326
|
+
const followUpIndex = this.#followUpMessages.findIndex(
|
|
7327
|
+
entry => entry.message === undefined && entry.text === messageText,
|
|
7328
|
+
);
|
|
7329
|
+
if (followUpIndex !== -1) this.#followUpMessages.splice(followUpIndex, 1);
|
|
6569
7330
|
}
|
|
6570
7331
|
}
|
|
6571
7332
|
}
|
|
@@ -6701,7 +7462,7 @@ export class AgentSession {
|
|
|
6701
7462
|
text: [
|
|
6702
7463
|
"Session transcript reached the managed per-file limit; this result could not be recorded durably.",
|
|
6703
7464
|
committed,
|
|
6704
|
-
|
|
7465
|
+
`To continue, ${SESSION_LIMIT_RECOVERY_ACTIONS}; re-verify the edited file before relying on it.`,
|
|
6705
7466
|
].join("\n"),
|
|
6706
7467
|
},
|
|
6707
7468
|
];
|
|
@@ -7138,6 +7899,10 @@ export class AgentSession {
|
|
|
7138
7899
|
!(event.stopReason === "maintenance" && isContinuingMidRunMaintenanceOutcome(event.maintenanceOutcome))
|
|
7139
7900
|
) {
|
|
7140
7901
|
this.#releaseDeferredSdkFollowUps();
|
|
7902
|
+
if (this.#compactionQueuedDeliveryArmed) {
|
|
7903
|
+
if (this.agent.hasQueuedMessages()) this.#scheduleQueuedDelivery();
|
|
7904
|
+
else this.#compactionQueuedDeliveryArmed = false;
|
|
7905
|
+
}
|
|
7141
7906
|
}
|
|
7142
7907
|
|
|
7143
7908
|
// Check auto-retry and auto-compaction after agent completes
|
|
@@ -7333,11 +8098,36 @@ export class AgentSession {
|
|
|
7333
8098
|
this.#ttsrResumePromise = undefined;
|
|
7334
8099
|
}
|
|
7335
8100
|
|
|
7336
|
-
#
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
8101
|
+
#dropQueuedTtsrFollowUps(): void {
|
|
8102
|
+
const deferredTtsr = new Set(
|
|
8103
|
+
this.#deferredSdkFollowUps.filter(
|
|
8104
|
+
message => message.role === "custom" && message.customType === "ttsr-injection",
|
|
8105
|
+
),
|
|
8106
|
+
);
|
|
8107
|
+
const queuedTtsr = [
|
|
8108
|
+
...this.agent
|
|
8109
|
+
.snapshotFollowUp()
|
|
8110
|
+
.filter(message => message.role === "custom" && message.customType === "ttsr-injection"),
|
|
8111
|
+
...deferredTtsr,
|
|
8112
|
+
];
|
|
8113
|
+
if (queuedTtsr.length === 0) return;
|
|
8114
|
+
this.agent.removeQueuedMessages(
|
|
8115
|
+
message => message.role === "custom" && message.customType === "ttsr-injection",
|
|
8116
|
+
"followUp",
|
|
8117
|
+
);
|
|
8118
|
+
this.#deferredSdkFollowUps = this.#deferredSdkFollowUps.filter(message => !deferredTtsr.has(message));
|
|
8119
|
+
for (const message of deferredTtsr) this.#deferredFollowUpForceOneAtATime.delete(message);
|
|
8120
|
+
this.#followUpMessages = this.#followUpMessages.filter(
|
|
8121
|
+
entry => entry.message === undefined || !queuedTtsr.includes(entry.message),
|
|
8122
|
+
);
|
|
8123
|
+
this.#resolveTtsrResume();
|
|
8124
|
+
}
|
|
8125
|
+
|
|
8126
|
+
#ensurePostPromptTasksPromise(): void {
|
|
8127
|
+
if (this.#postPromptTasksPromise) return;
|
|
8128
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
8129
|
+
this.#postPromptTasksPromise = promise;
|
|
8130
|
+
this.#postPromptTasksResolve = resolve;
|
|
7341
8131
|
}
|
|
7342
8132
|
|
|
7343
8133
|
#resolvePostPromptTasks(): void {
|
|
@@ -7355,6 +8145,7 @@ export class AgentSession {
|
|
|
7355
8145
|
excludeFromRecovery = false,
|
|
7356
8146
|
): void {
|
|
7357
8147
|
this.#postPromptTasks.add(task);
|
|
8148
|
+
this.#postPromptWorkLeases.set(task, this.#sessionWorkLease.acquire());
|
|
7358
8149
|
this.#postPromptTaskSelectionFenceGenerations.set(task, selectionFenceGeneration);
|
|
7359
8150
|
if (excludeFromRecovery) this.#postPromptTaskRecoveryExcluded.add(task);
|
|
7360
8151
|
this.#ensurePostPromptTasksPromise();
|
|
@@ -7365,6 +8156,8 @@ export class AgentSession {
|
|
|
7365
8156
|
void task
|
|
7366
8157
|
.catch(() => {})
|
|
7367
8158
|
.finally(() => {
|
|
8159
|
+
this.#postPromptWorkLeases.get(task)?.release();
|
|
8160
|
+
this.#postPromptWorkLeases.delete(task);
|
|
7368
8161
|
this.#postPromptTasks.delete(task);
|
|
7369
8162
|
this.#postPromptTaskSelectionFenceGenerations.delete(task);
|
|
7370
8163
|
this.#postPromptTaskRecoveryExcluded.delete(task);
|
|
@@ -7564,6 +8357,12 @@ export class AgentSession {
|
|
|
7564
8357
|
skip("queue_drained");
|
|
7565
8358
|
return false;
|
|
7566
8359
|
}
|
|
8360
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) {
|
|
8361
|
+
if (this.agent.hasQueuedMessages() || this.#deferredSdkFollowUps.length > 0)
|
|
8362
|
+
this.#queuedDeliveryPendingWhileTransition = true;
|
|
8363
|
+
skip("handoff_in_progress");
|
|
8364
|
+
return false;
|
|
8365
|
+
}
|
|
7567
8366
|
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
7568
8367
|
skip("queue_drained");
|
|
7569
8368
|
return false;
|
|
@@ -7703,8 +8502,10 @@ export class AgentSession {
|
|
|
7703
8502
|
error: error.message,
|
|
7704
8503
|
});
|
|
7705
8504
|
}
|
|
7706
|
-
if (!predecessorAccepted)
|
|
8505
|
+
if (!predecessorAccepted) {
|
|
8506
|
+
this.#settleTrackedOwnRunPromotionFailures();
|
|
7707
8507
|
this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
|
|
8508
|
+
}
|
|
7708
8509
|
throw error;
|
|
7709
8510
|
}
|
|
7710
8511
|
} catch (error) {
|
|
@@ -7823,6 +8624,18 @@ export class AgentSession {
|
|
|
7823
8624
|
deferredPredecessorAgentEnd?: AgentSessionEvent,
|
|
7824
8625
|
sdkRunToken?: string,
|
|
7825
8626
|
): void {
|
|
8627
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) {
|
|
8628
|
+
this.#deferredAutoContinueDuringTransition = () =>
|
|
8629
|
+
this.#scheduleAutoContinuePrompt(
|
|
8630
|
+
generation,
|
|
8631
|
+
requireUnfinishedWork,
|
|
8632
|
+
resourceRunId,
|
|
8633
|
+
deferredSelectionFenceGeneration,
|
|
8634
|
+
deferredPredecessorAgentEnd,
|
|
8635
|
+
sdkRunToken,
|
|
8636
|
+
);
|
|
8637
|
+
return;
|
|
8638
|
+
}
|
|
7826
8639
|
const scheduledGeneration = generation;
|
|
7827
8640
|
const scheduledSdkRunToken =
|
|
7828
8641
|
sdkRunToken ??
|
|
@@ -7843,6 +8656,10 @@ export class AgentSession {
|
|
|
7843
8656
|
);
|
|
7844
8657
|
return false;
|
|
7845
8658
|
}
|
|
8659
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) {
|
|
8660
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "session_transition");
|
|
8661
|
+
return false;
|
|
8662
|
+
}
|
|
7846
8663
|
// A same-turn auto-continue of a terminally aborted turn is denied
|
|
7847
8664
|
// (corrected turn semantics); owned-completion deliveries are not
|
|
7848
8665
|
// affected — they flow through followUp as fresh turns.
|
|
@@ -7984,6 +8801,8 @@ export class AgentSession {
|
|
|
7984
8801
|
#abandonPostPromptTasks(): void {
|
|
7985
8802
|
this.#postPromptTasksAbortController.abort();
|
|
7986
8803
|
this.#postPromptTasksAbortController = new AbortController();
|
|
8804
|
+
for (const lease of this.#postPromptWorkLeases.values()) lease.release();
|
|
8805
|
+
this.#postPromptWorkLeases.clear();
|
|
7987
8806
|
this.#postPromptTasks.clear();
|
|
7988
8807
|
this.#postPromptTaskSelectionFenceGenerations.clear();
|
|
7989
8808
|
this.#postPromptTaskRecoveryExcluded.clear();
|
|
@@ -8186,7 +9005,9 @@ export class AgentSession {
|
|
|
8186
9005
|
if (!injection) {
|
|
8187
9006
|
return;
|
|
8188
9007
|
}
|
|
8189
|
-
this
|
|
9008
|
+
const ttsrAbortEpoch = this.#abortEpoch;
|
|
9009
|
+
const ttsrAbortSignal = this.#promptPreflightAbortController.signal;
|
|
9010
|
+
const message: CustomMessage = {
|
|
8190
9011
|
role: "custom",
|
|
8191
9012
|
customType: "ttsr-injection",
|
|
8192
9013
|
content: injection.content,
|
|
@@ -8194,27 +9015,46 @@ export class AgentSession {
|
|
|
8194
9015
|
details: { rules: injection.rules.map(rule => rule.name) },
|
|
8195
9016
|
attribution: "agent",
|
|
8196
9017
|
timestamp: Date.now(),
|
|
8197
|
-
}
|
|
9018
|
+
};
|
|
8198
9019
|
this.#ensureTtsrResumePromise();
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
this.#resolveTtsrResume();
|
|
9020
|
+
void this.#queueFollowUpAfterReservation(
|
|
9021
|
+
message,
|
|
9022
|
+
this.#getCustomMessageTextContent(message),
|
|
9023
|
+
{
|
|
9024
|
+
createDisplayEntry: false,
|
|
9025
|
+
trackExternalFollowUp: false,
|
|
8206
9026
|
},
|
|
8207
|
-
|
|
8208
|
-
|
|
9027
|
+
ttsrAbortSignal,
|
|
9028
|
+
)
|
|
9029
|
+
.then(() => {
|
|
9030
|
+
if (this.#abortEpoch !== ttsrAbortEpoch || ttsrAbortSignal.aborted) {
|
|
8209
9031
|
this.#resolveTtsrResume();
|
|
8210
|
-
return
|
|
9032
|
+
return;
|
|
8211
9033
|
}
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
9034
|
+
// Mark as injected after this custom message is delivered and persisted (handled in message_end).
|
|
9035
|
+
// followUp() only enqueues; resume on the next tick once streaming settles.
|
|
9036
|
+
this.#scheduleAgentContinue({
|
|
9037
|
+
delayMs: 1,
|
|
9038
|
+
generation: this.#promptGeneration,
|
|
9039
|
+
onSkip: () => {
|
|
9040
|
+
this.#resolveTtsrResume();
|
|
9041
|
+
},
|
|
9042
|
+
shouldContinue: () => {
|
|
9043
|
+
if (this.agent.state.isStreaming || !this.agent.hasQueuedMessages()) {
|
|
9044
|
+
this.#resolveTtsrResume();
|
|
9045
|
+
return false;
|
|
9046
|
+
}
|
|
9047
|
+
return true;
|
|
9048
|
+
},
|
|
9049
|
+
onError: () => {
|
|
9050
|
+
this.#resolveTtsrResume();
|
|
9051
|
+
},
|
|
9052
|
+
});
|
|
9053
|
+
})
|
|
9054
|
+
.catch(error => {
|
|
8215
9055
|
this.#resolveTtsrResume();
|
|
8216
|
-
|
|
8217
|
-
|
|
9056
|
+
logger.warn("TTSR follow-up injection was rejected", { error: String(error) });
|
|
9057
|
+
});
|
|
8218
9058
|
}
|
|
8219
9059
|
|
|
8220
9060
|
/** Build TTSR match context for tool call argument deltas. */
|
|
@@ -9036,6 +9876,16 @@ export class AgentSession {
|
|
|
9036
9876
|
}
|
|
9037
9877
|
}
|
|
9038
9878
|
|
|
9879
|
+
#bindAttemptScopeToActiveRun(scope: AttemptScope): void {
|
|
9880
|
+
this.#activeAttemptScope = scope;
|
|
9881
|
+
if (this.#activeLogicalRunId !== undefined) {
|
|
9882
|
+
this.#logicalRunIdByAttemptScope.set(scope, this.#activeLogicalRunId);
|
|
9883
|
+
}
|
|
9884
|
+
if (this.#activeSdkRunToken !== undefined) {
|
|
9885
|
+
this.#sdkRunTokensByAttemptScope.set(scope, this.#activeSdkRunToken);
|
|
9886
|
+
}
|
|
9887
|
+
}
|
|
9888
|
+
|
|
9039
9889
|
/**
|
|
9040
9890
|
* Subscribe to agent events.
|
|
9041
9891
|
* Session persistence is handled internally (saves messages on message_end).
|
|
@@ -9063,6 +9913,7 @@ export class AgentSession {
|
|
|
9063
9913
|
*/
|
|
9064
9914
|
#disconnectFromAgent(): void {
|
|
9065
9915
|
this.#abortActiveMidRunBarriers();
|
|
9916
|
+
this.#compactionQueuedDeliveryArmed = false;
|
|
9066
9917
|
if (this.#unsubscribeAgent) {
|
|
9067
9918
|
this.#unsubscribeAgent();
|
|
9068
9919
|
this.#unsubscribeAgent = undefined;
|
|
@@ -9204,21 +10055,27 @@ export class AgentSession {
|
|
|
9204
10055
|
* Remove all listeners, flush pending writes, and disconnect from agent.
|
|
9205
10056
|
* Call this when completely done with the session.
|
|
9206
10057
|
*/
|
|
9207
|
-
dispose(): Promise<void> {
|
|
10058
|
+
dispose(options: { sessionShutdownReason?: "detached_idle" } = {}): Promise<void> {
|
|
9208
10059
|
this.#evalExecutionDisposing = true;
|
|
9209
10060
|
if (this.#disposeCompleted) return Promise.resolve();
|
|
9210
10061
|
if (this.#disposeTerminalError !== undefined) return Promise.reject(this.#disposeTerminalError);
|
|
9211
10062
|
if (this.#disposeCallerPromise) return this.#disposeCallerPromise;
|
|
9212
10063
|
if (!this.#disposeRunPromise) {
|
|
10064
|
+
this.#sessionShutdownReason = options.sessionShutdownReason;
|
|
9213
10065
|
this.#disposeDeadline = Date.now() + this.#disposeTimeoutMs;
|
|
9214
10066
|
this.#disposeDeadlineExpired = Promise.withResolvers<void>();
|
|
9215
10067
|
this.#disposeDeadlineTimer = setTimeout(() => this.#disposeDeadlineExpired?.resolve(), this.#disposeTimeoutMs);
|
|
9216
10068
|
this.#disposeDeadlineTimer.unref?.();
|
|
9217
10069
|
this.#abortAdmissionEpoch++;
|
|
9218
10070
|
this.#isDisposed = true;
|
|
10071
|
+
// Dispose disconnects the Agent event bridge before bounded abort cleanup;
|
|
10072
|
+
// settle every accepted SDK handle now so consumed submissions cannot wait
|
|
10073
|
+
// forever for an agent_end that teardown deliberately suppresses.
|
|
10074
|
+
this.#terminalizeQueuedSdkWorkForSessionTransition(this.#queuedMessagesForSessionTransition());
|
|
9219
10075
|
// Invalidate every coordinator event admitted before disposal. Handlers may
|
|
9220
10076
|
// still unwind, but their captured generation can no longer enqueue a write.
|
|
9221
10077
|
this.#coordinatorPersistGeneration += 1;
|
|
10078
|
+
this.#wakeFollowUpReservationTransitionWaiters();
|
|
9222
10079
|
this.#disposeAbortController.abort();
|
|
9223
10080
|
// Disposal owns a bounded Agent abort below. Waiting for the active prompt's
|
|
9224
10081
|
// admission here would put that unbounded prompt ahead of the abort budget.
|
|
@@ -9383,7 +10240,10 @@ export class AgentSession {
|
|
|
9383
10240
|
if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
|
|
9384
10241
|
await awaitDisposeStep(
|
|
9385
10242
|
"session shutdown handlers",
|
|
9386
|
-
this.#extensionRunner.emit({
|
|
10243
|
+
this.#extensionRunner.emit({
|
|
10244
|
+
type: "session_shutdown",
|
|
10245
|
+
...(this.#sessionShutdownReason === undefined ? {} : { reason: this.#sessionShutdownReason }),
|
|
10246
|
+
}),
|
|
9387
10247
|
);
|
|
9388
10248
|
}
|
|
9389
10249
|
} catch (error) {
|
|
@@ -9721,6 +10581,24 @@ export class AgentSession {
|
|
|
9721
10581
|
|
|
9722
10582
|
/** Test seam: await all currently admitted coordinator sidecar writes. */
|
|
9723
10583
|
async awaitCoordinatorRuntimeStatePersistenceForTests(): Promise<void> {
|
|
10584
|
+
// A terminal abort can schedule a preserved follow-up as a fresh turn after
|
|
10585
|
+
// the aborted run settles. Join that continuation before observing the
|
|
10586
|
+
// sidecar queue; otherwise teardown can race a rearmed run and wait on its
|
|
10587
|
+
// persistence while the test's manager is already being disposed.
|
|
10588
|
+
const idleStatus = await Promise.race([
|
|
10589
|
+
this.waitForIdle().then(
|
|
10590
|
+
() => ({ status: "settled" as const }),
|
|
10591
|
+
error => ({ status: "error" as const, error }),
|
|
10592
|
+
),
|
|
10593
|
+
Bun.sleep(COORDINATOR_PERSISTENCE_TEST_IDLE_TIMEOUT_MS).then(() => ({ status: "timed_out" as const })),
|
|
10594
|
+
]);
|
|
10595
|
+
if (idleStatus.status === "error") throw idleStatus.error;
|
|
10596
|
+
if (idleStatus.status === "timed_out") {
|
|
10597
|
+
logger.warn("Coordinator persistence test seam timed out waiting for session idle", {
|
|
10598
|
+
sessionId: this.sessionId,
|
|
10599
|
+
timeoutMs: COORDINATOR_PERSISTENCE_TEST_IDLE_TIMEOUT_MS,
|
|
10600
|
+
});
|
|
10601
|
+
}
|
|
9724
10602
|
await this.#coordinatorPersistQueue;
|
|
9725
10603
|
await this.#drainUnbarrieredCoordinatorPersists();
|
|
9726
10604
|
}
|
|
@@ -10068,7 +10946,10 @@ export class AgentSession {
|
|
|
10068
10946
|
reason: FoldReason = "chord",
|
|
10069
10947
|
adapter?: FoldAdapter,
|
|
10070
10948
|
): Promise<ForegroundFoldOutcome> {
|
|
10071
|
-
|
|
10949
|
+
// An explicit adapter (the steer watcher naming its own wait) is validated by
|
|
10950
|
+
// the coordinator's registration-bound identity check; only the implicit
|
|
10951
|
+
// chord / SDK-control path needs a targetable participant to exist.
|
|
10952
|
+
if (!adapter && !this.#foldCoordinator.hasFoldableParticipant()) {
|
|
10072
10953
|
return this.#hasRunningFoldedJob() ? { status: "already_backgrounded" } : { status: "no_active_bash" };
|
|
10073
10954
|
}
|
|
10074
10955
|
try {
|
|
@@ -10106,6 +10987,240 @@ export class AgentSession {
|
|
|
10106
10987
|
return Array.from(this.#toolRegistry.keys());
|
|
10107
10988
|
}
|
|
10108
10989
|
|
|
10990
|
+
/**
|
|
10991
|
+
* Exact MCP controls: stock-workflow transactional successor revocation.
|
|
10992
|
+
*
|
|
10993
|
+
* Serialize an exact-config MCP control against both session mutexes.
|
|
10994
|
+
*
|
|
10995
|
+
* Admission serializes it with prompts and selection changes and fails closed
|
|
10996
|
+
* once dispose has closed admission; the transition lease excludes it from
|
|
10997
|
+
* new/switch/branch/fork/handoff in both directions. Neither side waits for
|
|
10998
|
+
* the other: whichever arrives second is rejected with a busy error, so a
|
|
10999
|
+
* control never observes or mutates state inside a transition window.
|
|
11000
|
+
*/
|
|
11001
|
+
async #withExactMcpControlAdmission<T>(body: () => Promise<T>): Promise<T> {
|
|
11002
|
+
return this.#withSessionAdmission("mcp-control", async () => {
|
|
11003
|
+
this.#beginSessionTransition("mcp-control");
|
|
11004
|
+
try {
|
|
11005
|
+
return await body();
|
|
11006
|
+
} finally {
|
|
11007
|
+
this.#endSessionTransition();
|
|
11008
|
+
}
|
|
11009
|
+
});
|
|
11010
|
+
}
|
|
11011
|
+
|
|
11012
|
+
get hasExactMcpControls(): boolean {
|
|
11013
|
+
return getExactMcpControls(this) !== undefined;
|
|
11014
|
+
}
|
|
11015
|
+
|
|
11016
|
+
/**
|
|
11017
|
+
* Exact-config MCP status for the TUI `/mcp` command.
|
|
11018
|
+
*
|
|
11019
|
+
* Returns `undefined` unless this session holds the exact-config capability,
|
|
11020
|
+
* so a session built outside the root interactive entry point cannot read MCP
|
|
11021
|
+
* state through it. Data comes only from the session-owned tools-only manager
|
|
11022
|
+
* and the call never starts a deferred MCP startup or mutates connections.
|
|
11023
|
+
*/
|
|
11024
|
+
async getExactMcpStatusSnapshot(): Promise<ExactMcpStatusSnapshot | undefined> {
|
|
11025
|
+
const controls = getExactMcpControls(this);
|
|
11026
|
+
if (!controls) return undefined;
|
|
11027
|
+
return this.#withExactMcpControlAdmission(async () => {
|
|
11028
|
+
const manager = this.#ownedMcpManager;
|
|
11029
|
+
const live = new Map<string, ExactMcpServerState>();
|
|
11030
|
+
if (manager) {
|
|
11031
|
+
for (const name of manager.getAllServerNames()) live.set(name, manager.getConnectionStatus(name));
|
|
11032
|
+
}
|
|
11033
|
+
const declared = await this.#readExactMcpDeclaredServers(controls.configPath);
|
|
11034
|
+
const names = declared ? [...declared.keys()] : [...live.keys()];
|
|
11035
|
+
if (names.length === 0) {
|
|
11036
|
+
return {
|
|
11037
|
+
startup: declared ? "no-servers-declared" : "not-started",
|
|
11038
|
+
servers: [],
|
|
11039
|
+
} satisfies ExactMcpStatusSnapshot;
|
|
11040
|
+
}
|
|
11041
|
+
const tools = manager?.getTools() ?? [];
|
|
11042
|
+
const servers = names.map(name => ({
|
|
11043
|
+
name,
|
|
11044
|
+
transport: declared?.get(name) ?? ("unknown" as const),
|
|
11045
|
+
state: manager?.isExactServerSuppressed(name)
|
|
11046
|
+
? ("suspended" as const)
|
|
11047
|
+
: (live.get(name) ?? ("unavailable" as const)),
|
|
11048
|
+
toolCount: tools.reduce((count, tool) => (tool.mcpServerName === name ? count + 1 : count), 0),
|
|
11049
|
+
}));
|
|
11050
|
+
const startup =
|
|
11051
|
+
live.size === 0
|
|
11052
|
+
? "not-started"
|
|
11053
|
+
: servers.some(server => server.state === "connecting")
|
|
11054
|
+
? "in-flight"
|
|
11055
|
+
: "settled";
|
|
11056
|
+
return { startup, servers } satisfies ExactMcpStatusSnapshot;
|
|
11057
|
+
});
|
|
11058
|
+
}
|
|
11059
|
+
|
|
11060
|
+
/**
|
|
11061
|
+
* Apply one session-local exact-config control. Selection persistence is
|
|
11062
|
+
* deliberately bypassed: suppression is runtime state, not user authority.
|
|
11063
|
+
*/
|
|
11064
|
+
async controlExactMcpServer(
|
|
11065
|
+
action: ExactMcpControlAction,
|
|
11066
|
+
name: string,
|
|
11067
|
+
): Promise<ExactMcpControlResult | undefined> {
|
|
11068
|
+
const controls = getExactMcpControls(this);
|
|
11069
|
+
if (!controls) return undefined;
|
|
11070
|
+
return this.#withExactMcpControlAdmission(async () => {
|
|
11071
|
+
const manager = this.#ownedMcpManager;
|
|
11072
|
+
const declared = await this.#readExactMcpDeclaredServers(controls.configPath);
|
|
11073
|
+
const known = declared?.has(name) ?? manager?.getAllServerNames().includes(name) ?? false;
|
|
11074
|
+
if (!known) return { action, name, status: "unknown-server", toolCount: 0 };
|
|
11075
|
+
if (!manager) return { action, name, status: "unavailable", toolCount: 0 };
|
|
11076
|
+
|
|
11077
|
+
const selected = this.getSelectedMCPToolNames();
|
|
11078
|
+
const active = this.getActiveToolNames().filter(toolName => isMCPToolName(toolName));
|
|
11079
|
+
const serverToolNames = new Set(
|
|
11080
|
+
Array.from(this.#toolRegistry.values())
|
|
11081
|
+
.filter(
|
|
11082
|
+
tool =>
|
|
11083
|
+
isMCPBridgeTool(tool) && (tool as AgentTool & { mcpServerName?: string }).mcpServerName === name,
|
|
11084
|
+
)
|
|
11085
|
+
.map(tool => tool.name),
|
|
11086
|
+
);
|
|
11087
|
+
|
|
11088
|
+
const prepared = await manager.prepareExactServerControl(action, name);
|
|
11089
|
+
const result = prepared.result;
|
|
11090
|
+
if (result.status !== "suspended" && result.status !== "resumed" && result.status !== "reconnected") {
|
|
11091
|
+
await prepared.abort();
|
|
11092
|
+
return { action, ...result };
|
|
11093
|
+
}
|
|
11094
|
+
const snapshot = this.#captureExactMcpSessionState();
|
|
11095
|
+
const suspended = this.#exactMcpSuspendedTools.get(name);
|
|
11096
|
+
try {
|
|
11097
|
+
await this.refreshMCPTools(prepared.tools, {
|
|
11098
|
+
selectedMCPToolNames:
|
|
11099
|
+
action === "suspend"
|
|
11100
|
+
? selected.filter(toolName => !serverToolNames.has(toolName))
|
|
11101
|
+
: action === "resume"
|
|
11102
|
+
? [...selected, ...(suspended?.selected ?? [])]
|
|
11103
|
+
: selected,
|
|
11104
|
+
activeMCPToolNames:
|
|
11105
|
+
action === "suspend"
|
|
11106
|
+
? active.filter(toolName => !serverToolNames.has(toolName))
|
|
11107
|
+
: action === "resume"
|
|
11108
|
+
? [...active, ...(suspended?.active ?? [])]
|
|
11109
|
+
: active,
|
|
11110
|
+
persistMCPSelection: false,
|
|
11111
|
+
});
|
|
11112
|
+
await prepared.commit();
|
|
11113
|
+
if (action === "suspend") {
|
|
11114
|
+
this.#exactMcpSuspendedTools.set(name, {
|
|
11115
|
+
selected: selected.filter(toolName => serverToolNames.has(toolName)),
|
|
11116
|
+
active: active.filter(toolName => serverToolNames.has(toolName)),
|
|
11117
|
+
});
|
|
11118
|
+
} else if (action === "resume") {
|
|
11119
|
+
this.#exactMcpSuspendedTools.delete(name);
|
|
11120
|
+
}
|
|
11121
|
+
return { action, ...result };
|
|
11122
|
+
} catch (error) {
|
|
11123
|
+
this.#restoreExactMcpSessionState(snapshot);
|
|
11124
|
+
await prepared.abort();
|
|
11125
|
+
throw error;
|
|
11126
|
+
}
|
|
11127
|
+
});
|
|
11128
|
+
}
|
|
11129
|
+
|
|
11130
|
+
#captureExactMcpSessionState(): ExactMcpSessionStateSnapshot {
|
|
11131
|
+
return {
|
|
11132
|
+
toolRegistry: new Map(this.#toolRegistry),
|
|
11133
|
+
discoverableMCPTools: new Map(this.#discoverableMCPTools),
|
|
11134
|
+
selectedMCPToolNames: new Set(this.#selectedMCPToolNames),
|
|
11135
|
+
selectedDiscoveredToolNames: new Set(this.#selectedDiscoveredToolNames),
|
|
11136
|
+
tools: [...this.agent.state.tools],
|
|
11137
|
+
baseSystemPrompt: this.#baseSystemPrompt,
|
|
11138
|
+
systemPrompt: this.agent.state.systemPrompt,
|
|
11139
|
+
lastAppliedToolSignature: this.#lastAppliedToolSignature,
|
|
11140
|
+
pendingAppliedToolSignature: this.#pendingAppliedToolSignature,
|
|
11141
|
+
defaultModelSelectionMutationRevision: this.#defaultModelSelectionMutationRevision,
|
|
11142
|
+
baseSystemPromptGeneration: this.#baseSystemPromptGeneration,
|
|
11143
|
+
suspendedTools: new Map(this.#exactMcpSuspendedTools),
|
|
11144
|
+
};
|
|
11145
|
+
}
|
|
11146
|
+
|
|
11147
|
+
#restoreExactMcpSessionState(snapshot: ExactMcpSessionStateSnapshot): void {
|
|
11148
|
+
this.#toolRegistry.clear();
|
|
11149
|
+
for (const [name, tool] of snapshot.toolRegistry) this.#toolRegistry.set(name, tool);
|
|
11150
|
+
this.#discoverableMCPTools = snapshot.discoverableMCPTools;
|
|
11151
|
+
this.#selectedMCPToolNames = snapshot.selectedMCPToolNames;
|
|
11152
|
+
this.#selectedDiscoveredToolNames = snapshot.selectedDiscoveredToolNames;
|
|
11153
|
+
// These are the already-prepared predecessor objects from agent.state;
|
|
11154
|
+
// re-preparing them would stack execution guards and break identity.
|
|
11155
|
+
this.agent.setTools(snapshot.tools);
|
|
11156
|
+
this.#baseSystemPrompt = snapshot.baseSystemPrompt;
|
|
11157
|
+
this.agent.setSystemPrompt(snapshot.systemPrompt);
|
|
11158
|
+
this.#lastAppliedToolSignature = snapshot.lastAppliedToolSignature;
|
|
11159
|
+
this.#pendingAppliedToolSignature = snapshot.pendingAppliedToolSignature;
|
|
11160
|
+
this.#defaultModelSelectionMutationRevision = snapshot.defaultModelSelectionMutationRevision;
|
|
11161
|
+
this.#baseSystemPromptGeneration = snapshot.baseSystemPromptGeneration;
|
|
11162
|
+
this.#exactMcpSuspendedTools = snapshot.suspendedTools;
|
|
11163
|
+
this.#invalidateDiscoveryCaches();
|
|
11164
|
+
}
|
|
11165
|
+
|
|
11166
|
+
async #prepareExactMcpControlRetirement(): Promise<PreparedExactMcpRetirement | undefined> {
|
|
11167
|
+
const controls = getExactMcpControls(this);
|
|
11168
|
+
if (!controls) return undefined;
|
|
11169
|
+
const snapshot = this.#captureExactMcpSessionState();
|
|
11170
|
+
const prepared = await this.#ownedMcpManager?.prepareExactServerControlReset();
|
|
11171
|
+
const restoreControls = (): void => attachExactMcpControls(this, controls);
|
|
11172
|
+
try {
|
|
11173
|
+
if (prepared) await this.refreshMCPTools(prepared.tools, { persistMCPSelection: false });
|
|
11174
|
+
return {
|
|
11175
|
+
canCommit: () => prepared?.canCommit() ?? true,
|
|
11176
|
+
commit: async () => {
|
|
11177
|
+
try {
|
|
11178
|
+
await prepared?.commit();
|
|
11179
|
+
this.#exactMcpSuspendedTools.clear();
|
|
11180
|
+
revokeExactMcpControls(this);
|
|
11181
|
+
} catch (error) {
|
|
11182
|
+
this.#restoreExactMcpSessionState(snapshot);
|
|
11183
|
+
restoreControls();
|
|
11184
|
+
await prepared?.abort();
|
|
11185
|
+
throw error;
|
|
11186
|
+
}
|
|
11187
|
+
},
|
|
11188
|
+
abort: async () => {
|
|
11189
|
+
this.#restoreExactMcpSessionState(snapshot);
|
|
11190
|
+
restoreControls();
|
|
11191
|
+
await prepared?.abort();
|
|
11192
|
+
},
|
|
11193
|
+
};
|
|
11194
|
+
} catch (error) {
|
|
11195
|
+
this.#restoreExactMcpSessionState(snapshot);
|
|
11196
|
+
restoreControls();
|
|
11197
|
+
await prepared?.abort();
|
|
11198
|
+
throw error;
|
|
11199
|
+
}
|
|
11200
|
+
}
|
|
11201
|
+
|
|
11202
|
+
/**
|
|
11203
|
+
* Server names and transports the exact config declares, or `undefined` when
|
|
11204
|
+
* the file cannot be read or parsed. Only the name and the transport
|
|
11205
|
+
* discriminator are taken; command, args, url, headers, and env never leave
|
|
11206
|
+
* this method, and a read failure degrades to the manager's own view instead
|
|
11207
|
+
* of surfacing a parse error to the operator.
|
|
11208
|
+
*/
|
|
11209
|
+
async #readExactMcpDeclaredServers(configPath: string): Promise<Map<string, ExactMcpTransport> | undefined> {
|
|
11210
|
+
try {
|
|
11211
|
+
const parsed: unknown = await Bun.file(configPath).json();
|
|
11212
|
+
const servers = (parsed as { mcpServers?: unknown } | null)?.mcpServers;
|
|
11213
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) return undefined;
|
|
11214
|
+
const declared = new Map<string, ExactMcpTransport>();
|
|
11215
|
+
for (const [name, entry] of Object.entries(servers as Record<string, unknown>)) {
|
|
11216
|
+
declared.set(name, exactMcpTransportOf(entry));
|
|
11217
|
+
}
|
|
11218
|
+
return declared;
|
|
11219
|
+
} catch {
|
|
11220
|
+
return undefined;
|
|
11221
|
+
}
|
|
11222
|
+
}
|
|
11223
|
+
|
|
10109
11224
|
#getEditModeSession() {
|
|
10110
11225
|
return {
|
|
10111
11226
|
settings: this.settings,
|
|
@@ -10764,7 +11879,14 @@ export class AgentSession {
|
|
|
10764
11879
|
* Replace MCP tools in the registry and recompute the visible MCP tool set immediately.
|
|
10765
11880
|
* This allows /mcp add/remove/reauth to take effect without restarting the session.
|
|
10766
11881
|
*/
|
|
10767
|
-
async refreshMCPTools(
|
|
11882
|
+
async refreshMCPTools(
|
|
11883
|
+
mcpTools: CustomTool[],
|
|
11884
|
+
options: {
|
|
11885
|
+
selectedMCPToolNames?: string[];
|
|
11886
|
+
activeMCPToolNames?: string[];
|
|
11887
|
+
persistMCPSelection?: boolean;
|
|
11888
|
+
} = {},
|
|
11889
|
+
): Promise<void> {
|
|
10768
11890
|
const previousSelectedMCPToolNames = this.getSelectedMCPToolNames();
|
|
10769
11891
|
const existingNames = Array.from(this.#toolRegistry.keys());
|
|
10770
11892
|
for (const name of existingNames) {
|
|
@@ -10798,15 +11920,20 @@ export class AgentSession {
|
|
|
10798
11920
|
this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
|
|
10799
11921
|
this.#pruneSelectedMCPToolNames();
|
|
10800
11922
|
const hasPersistedMCPToolSelection = this.buildDisplaySessionContext().hasPersistedMCPToolSelection;
|
|
10801
|
-
if (
|
|
11923
|
+
if (options.selectedMCPToolNames) {
|
|
11924
|
+
this.#selectedMCPToolNames = new Set(this.#filterSelectableMCPToolNames(options.selectedMCPToolNames));
|
|
11925
|
+
} else if (!hasPersistedMCPToolSelection) {
|
|
10802
11926
|
this.#selectedMCPToolNames = new Set(
|
|
10803
11927
|
this.#resolveConstructorMCPToolSelection() ?? this.#getConfiguredDefaultSelectedMCPToolNames(),
|
|
10804
11928
|
);
|
|
10805
11929
|
}
|
|
10806
|
-
const nextActive = [
|
|
11930
|
+
const nextActive = [
|
|
11931
|
+
...this.#getActiveNonMCPToolNames(),
|
|
11932
|
+
...(options.activeMCPToolNames ?? this.getSelectedMCPToolNames()),
|
|
11933
|
+
];
|
|
10807
11934
|
await this.#applyActiveToolsByName(nextActive, {
|
|
10808
11935
|
previousSelectedMCPToolNames,
|
|
10809
|
-
persistMCPSelection: hasPersistedMCPToolSelection,
|
|
11936
|
+
persistMCPSelection: options.persistMCPSelection ?? hasPersistedMCPToolSelection,
|
|
10810
11937
|
});
|
|
10811
11938
|
}
|
|
10812
11939
|
|
|
@@ -10946,6 +12073,11 @@ export class AgentSession {
|
|
|
10946
12073
|
return this.#postPromptTasks.size > 0;
|
|
10947
12074
|
}
|
|
10948
12075
|
|
|
12076
|
+
/** The host-liveness lease shared by admission, execution, and recovery work. */
|
|
12077
|
+
get sessionWorkLease(): SessionWorkLease {
|
|
12078
|
+
return this.#sessionWorkLease;
|
|
12079
|
+
}
|
|
12080
|
+
|
|
10949
12081
|
/** Stable resource ownership identifier for the active prompt run. */
|
|
10950
12082
|
get activePromptHandle(): string | undefined {
|
|
10951
12083
|
return this.agent.activeResourceRunId;
|
|
@@ -11029,7 +12161,7 @@ export class AgentSession {
|
|
|
11029
12161
|
|
|
11030
12162
|
/** Main startup calls this exactly once, after a strict open returned `kind: "opened"`. */
|
|
11031
12163
|
async continuePersistedHistory(): Promise<void> {
|
|
11032
|
-
this.#
|
|
12164
|
+
this.#assertExternalSessionIngress();
|
|
11033
12165
|
this.#assertRecoveryHydrationPromoted();
|
|
11034
12166
|
this.#removeEphemeralCustomMessages();
|
|
11035
12167
|
|
|
@@ -11072,7 +12204,7 @@ export class AgentSession {
|
|
|
11072
12204
|
// Re-check after the awaited preparation: a handoff can engage during the
|
|
11073
12205
|
// volatile-context/hindsight awaits above and this would otherwise start a
|
|
11074
12206
|
// turn against the session being handed off.
|
|
11075
|
-
this.#
|
|
12207
|
+
this.#assertExternalSessionIngress();
|
|
11076
12208
|
await this.agent.continue({
|
|
11077
12209
|
...this.#managedFallbackPromptOptions(),
|
|
11078
12210
|
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
@@ -11500,7 +12632,26 @@ export class AgentSession {
|
|
|
11500
12632
|
path.join(sessionStateDir(this.sessionManager.getCwd(), sessionId), "workflow-gates.json"),
|
|
11501
12633
|
)
|
|
11502
12634
|
: new MemoryGateStore();
|
|
11503
|
-
|
|
12635
|
+
let emitter: BrokerWorkflowGateEmitter;
|
|
12636
|
+
emitter = new BrokerWorkflowGateEmitter(sessionId, gateStore, {
|
|
12637
|
+
continuationLost: () => {
|
|
12638
|
+
if (
|
|
12639
|
+
!isCurrentWorkflowGateContinuation(
|
|
12640
|
+
this.sessionManager.getSessionId(),
|
|
12641
|
+
sessionId,
|
|
12642
|
+
this.#workflowGateEmitter,
|
|
12643
|
+
emitter,
|
|
12644
|
+
)
|
|
12645
|
+
)
|
|
12646
|
+
return;
|
|
12647
|
+
void this.abort({ cause: "internal", silent: true }).catch(error => {
|
|
12648
|
+
logger.warn("Failed to settle a workflow gate turn after continuation loss", {
|
|
12649
|
+
error: error instanceof Error ? error.message : String(error),
|
|
12650
|
+
});
|
|
12651
|
+
});
|
|
12652
|
+
},
|
|
12653
|
+
});
|
|
12654
|
+
return emitter;
|
|
11504
12655
|
}
|
|
11505
12656
|
|
|
11506
12657
|
/**
|
|
@@ -12061,11 +13212,14 @@ export class AgentSession {
|
|
|
12061
13212
|
options?.images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
|
|
12062
13213
|
if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
|
|
12063
13214
|
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
13215
|
+
if (options?.synthetic !== true && options?.attribution !== "agent")
|
|
13216
|
+
invalidateSessionTitleGeneration(this.sessionManager);
|
|
12064
13217
|
const sdkRunToken = readSdkRunCapability(options?.sdkRunCapability);
|
|
12065
13218
|
const internalOptions: InternalPromptOptions | undefined = options
|
|
12066
13219
|
? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) }
|
|
12067
13220
|
: undefined;
|
|
12068
13221
|
this.#assertRecoveryHydrationPromoted();
|
|
13222
|
+
this.#assertExternalSessionIngress();
|
|
12069
13223
|
const owner = this.#sessionAdmissionContext.getStore();
|
|
12070
13224
|
if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
|
|
12071
13225
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
@@ -12174,7 +13328,7 @@ export class AgentSession {
|
|
|
12174
13328
|
throw new AgentBusyError();
|
|
12175
13329
|
}
|
|
12176
13330
|
if (options.streamingBehavior === "followUp") {
|
|
12177
|
-
await this.#
|
|
13331
|
+
await this.#queueFollowUpTextAfterReservation(expandedText, options?.images, {
|
|
12178
13332
|
forceOneAtATime: options.followUpQueuePolicy === "sequential",
|
|
12179
13333
|
claimsGenuineUserIntent,
|
|
12180
13334
|
});
|
|
@@ -12187,8 +13341,10 @@ export class AgentSession {
|
|
|
12187
13341
|
if (workflowIntentDiff) {
|
|
12188
13342
|
this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
|
|
12189
13343
|
}
|
|
13344
|
+
this.#assertExternalSessionIngress();
|
|
12190
13345
|
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
12191
13346
|
else options?.onPreflightAccepted?.();
|
|
13347
|
+
this.#assertExternalSessionIngress();
|
|
12192
13348
|
return;
|
|
12193
13349
|
}
|
|
12194
13350
|
|
|
@@ -12207,7 +13363,7 @@ export class AgentSession {
|
|
|
12207
13363
|
) {
|
|
12208
13364
|
admission.release();
|
|
12209
13365
|
if (options.streamingBehavior === "followUp") {
|
|
12210
|
-
await this.#
|
|
13366
|
+
await this.#queueFollowUpTextAfterReservation(expandedText, options?.images, {
|
|
12211
13367
|
forceOneAtATime: options.followUpQueuePolicy === "sequential",
|
|
12212
13368
|
claimsGenuineUserIntent,
|
|
12213
13369
|
});
|
|
@@ -12220,8 +13376,10 @@ export class AgentSession {
|
|
|
12220
13376
|
if (workflowIntentDiff) {
|
|
12221
13377
|
this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
|
|
12222
13378
|
}
|
|
13379
|
+
this.#assertExternalSessionIngress();
|
|
12223
13380
|
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
12224
13381
|
else options?.onPreflightAccepted?.();
|
|
13382
|
+
this.#assertExternalSessionIngress();
|
|
12225
13383
|
return;
|
|
12226
13384
|
}
|
|
12227
13385
|
this.#throwIfPromptPreflightCancelled(admissionGeneration, admissionSignal);
|
|
@@ -12409,6 +13567,7 @@ export class AgentSession {
|
|
|
12409
13567
|
const internalOptions: InternalCustomMessageOptions | undefined = options
|
|
12410
13568
|
? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) }
|
|
12411
13569
|
: undefined;
|
|
13570
|
+
this.#assertExternalSessionIngress();
|
|
12412
13571
|
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
12413
13572
|
const textContent =
|
|
12414
13573
|
typeof message.content === "string"
|
|
@@ -12468,9 +13627,11 @@ export class AgentSession {
|
|
|
12468
13627
|
await activationSeed?.rollback();
|
|
12469
13628
|
throw promptPreflightCancelledError();
|
|
12470
13629
|
}
|
|
13630
|
+
this.#assertExternalSessionIngress();
|
|
12471
13631
|
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
12472
13632
|
else options?.onPreflightAccepted?.();
|
|
12473
13633
|
durableAcceptanceCompleted = true;
|
|
13634
|
+
this.#assertExternalSessionIngress();
|
|
12474
13635
|
if (options?.preflightSignal?.aborted) {
|
|
12475
13636
|
await activationSeed?.rollback();
|
|
12476
13637
|
throw promptPreflightCancelledError();
|
|
@@ -12907,10 +14068,12 @@ export class AgentSession {
|
|
|
12907
14068
|
await this.#promptAgentWithIdleRetry(preSubmit, agentPromptOptions, predecessorAgentEndHold, {
|
|
12908
14069
|
signal: preflightSignal,
|
|
12909
14070
|
resourceRunId: this.#runResourceLeaseContext.getStore()?.resourceRunId,
|
|
12910
|
-
onPreflightAccepted: () => {
|
|
14071
|
+
onPreflightAccepted: async () => {
|
|
14072
|
+
this.#assertExternalSessionIngress({ allowCancelAndSubmit: true });
|
|
12911
14073
|
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
12912
|
-
if (options?.onPreflightAcceptCommit)
|
|
12913
|
-
options?.onPreflightAccepted?.();
|
|
14074
|
+
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
14075
|
+
else options?.onPreflightAccepted?.();
|
|
14076
|
+
this.#assertExternalSessionIngress({ allowCancelAndSubmit: true });
|
|
12914
14077
|
},
|
|
12915
14078
|
});
|
|
12916
14079
|
const activeTerminalScope = this.#activeAttemptScope;
|
|
@@ -13169,7 +14332,9 @@ export class AgentSession {
|
|
|
13169
14332
|
images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
|
|
13170
14333
|
if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
|
|
13171
14334
|
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
14335
|
+
invalidateSessionTitleGeneration(this.sessionManager);
|
|
13172
14336
|
this.#assertRecoveryHydrationPromoted();
|
|
14337
|
+
this.#assertExternalSessionIngress({ allowCancelAndSubmit: true });
|
|
13173
14338
|
if (text.startsWith("/")) {
|
|
13174
14339
|
this.#throwIfExtensionCommand(text);
|
|
13175
14340
|
}
|
|
@@ -13178,7 +14343,10 @@ export class AgentSession {
|
|
|
13178
14343
|
if (expandedText.trim().length === 0 && !hasUsableImage)
|
|
13179
14344
|
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
13180
14345
|
assertImagePlaceholdersHavePayload(expandedText, images);
|
|
13181
|
-
await this.#queueSteer(expandedText, images, {
|
|
14346
|
+
await this.#queueSteer(expandedText, images, {
|
|
14347
|
+
claimsGenuineUserIntent: true,
|
|
14348
|
+
allowCancelAndSubmit: true,
|
|
14349
|
+
});
|
|
13182
14350
|
}
|
|
13183
14351
|
|
|
13184
14352
|
/**
|
|
@@ -13193,7 +14361,9 @@ export class AgentSession {
|
|
|
13193
14361
|
images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
|
|
13194
14362
|
if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
|
|
13195
14363
|
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
14364
|
+
invalidateSessionTitleGeneration(this.sessionManager);
|
|
13196
14365
|
this.#assertRecoveryHydrationPromoted();
|
|
14366
|
+
this.#assertExternalSessionIngress({ allowCancelAndSubmit: true });
|
|
13197
14367
|
if (text.startsWith("/")) {
|
|
13198
14368
|
this.#throwIfExtensionCommand(text);
|
|
13199
14369
|
}
|
|
@@ -13202,9 +14372,10 @@ export class AgentSession {
|
|
|
13202
14372
|
if (expandedText.trim().length === 0 && !hasUsableImage)
|
|
13203
14373
|
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
13204
14374
|
assertImagePlaceholdersHavePayload(expandedText, images);
|
|
13205
|
-
await this.#
|
|
14375
|
+
await this.#queueFollowUpTextAfterReservation(expandedText, images, {
|
|
13206
14376
|
forceOneAtATime: options?.followUpQueuePolicy === "sequential",
|
|
13207
14377
|
claimsGenuineUserIntent: true,
|
|
14378
|
+
allowCancelAndSubmit: true,
|
|
13208
14379
|
});
|
|
13209
14380
|
}
|
|
13210
14381
|
|
|
@@ -13217,12 +14388,19 @@ export class AgentSession {
|
|
|
13217
14388
|
options?: {
|
|
13218
14389
|
claimsGenuineUserIntent?: boolean;
|
|
13219
14390
|
onPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
|
|
14391
|
+
onQueued?: (message: AgentMessage) => void;
|
|
14392
|
+
onQueuedAfterAdmission?: (message: AgentMessage, cancelQueued: () => boolean) => void;
|
|
13220
14393
|
external?: boolean;
|
|
13221
14394
|
sdkRunToken?: string;
|
|
13222
14395
|
forceOneAtATime?: boolean;
|
|
14396
|
+
allowDuringSessionTransition?: boolean;
|
|
14397
|
+
allowCancelAndSubmit?: boolean;
|
|
13223
14398
|
},
|
|
13224
|
-
): Promise<
|
|
13225
|
-
this.#
|
|
14399
|
+
): Promise<QueuedFollowUpOwner> {
|
|
14400
|
+
this.#assertExternalSessionIngress({
|
|
14401
|
+
allowTrackedSuccessor: options?.allowDuringSessionTransition,
|
|
14402
|
+
allowCancelAndSubmit: options?.allowCancelAndSubmit,
|
|
14403
|
+
});
|
|
13226
14404
|
assertImagePlaceholdersHavePayload(text, images);
|
|
13227
14405
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
13228
14406
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
@@ -13235,7 +14413,7 @@ export class AgentSession {
|
|
|
13235
14413
|
// in a queue nobody owns.
|
|
13236
14414
|
const admission = this.agent.steer(message, options?.forceOneAtATime ? { forceOneAtATime: true } : undefined);
|
|
13237
14415
|
if (!admission.admitted) {
|
|
13238
|
-
await this.#
|
|
14416
|
+
const queuedFollowUp = await this.#queueFollowUpAfterReservation(message, displayText, {
|
|
13239
14417
|
forceOneAtATime: options?.forceOneAtATime,
|
|
13240
14418
|
claimsGenuineUserIntent: options?.claimsGenuineUserIntent,
|
|
13241
14419
|
onPromoted: options?.onPromoted,
|
|
@@ -13243,16 +14421,21 @@ export class AgentSession {
|
|
|
13243
14421
|
scheduleNonAdmittedWake: false,
|
|
13244
14422
|
onQueued: message => {
|
|
13245
14423
|
if (this.#abortUnwind && !options?.forceOneAtATime) this.#abortUnwindSteerFallbacks.push(message);
|
|
14424
|
+
options?.onQueued?.(message);
|
|
13246
14425
|
},
|
|
14426
|
+
onQueuedAfterAdmission: options?.onQueuedAfterAdmission,
|
|
14427
|
+
allowDuringSessionTransition: options?.allowDuringSessionTransition,
|
|
14428
|
+
allowCancelAndSubmit: options?.allowCancelAndSubmit,
|
|
13247
14429
|
});
|
|
13248
14430
|
this.#scheduleNonAdmittedQueuedContinuation();
|
|
13249
|
-
return;
|
|
14431
|
+
return queuedFollowUp;
|
|
13250
14432
|
}
|
|
13251
14433
|
if (options?.forceOneAtATime) this.#sequentialSteerMessages.add(message);
|
|
13252
14434
|
this.#steeringMessages.push(this.#createQueuedDisplayEntry(displayText, undefined, message));
|
|
13253
14435
|
if (options?.external) {
|
|
13254
14436
|
this.#externalSteerMessages.add(message);
|
|
13255
14437
|
this.#externalSteerAdmissionSeq.set(message, ++this.#steeringAdmissionSeq);
|
|
14438
|
+
this.#queuedAdmissionSeq.set(message, ++this.#queuedAdmissionSequence);
|
|
13256
14439
|
if (options.onPromoted) this.#steerPromotionHooks.set(message, options.onPromoted);
|
|
13257
14440
|
}
|
|
13258
14441
|
if (options?.sdkRunToken) this.#sdkRunTokensByQueuedMessage.set(message, options.sdkRunToken);
|
|
@@ -13260,6 +14443,45 @@ export class AgentSession {
|
|
|
13260
14443
|
const epoch = this.#claimDeepInterviewUserIntent();
|
|
13261
14444
|
this.#deepInterviewGenuineUserMessageEpochs.set(message, epoch);
|
|
13262
14445
|
}
|
|
14446
|
+
options?.onQueued?.(message);
|
|
14447
|
+
const cancelQueuedSteer = (): boolean => {
|
|
14448
|
+
const removedQueues = this.agent.removeQueuedMessages(candidate => candidate === message);
|
|
14449
|
+
const removed = removedQueues.steering > 0 || removedQueues.followUp > 0;
|
|
14450
|
+
if (!removed) return false;
|
|
14451
|
+
this.#steeringMessages = this.#steeringMessages.filter(entry => entry.message !== message);
|
|
14452
|
+
this.#followUpMessages = this.#followUpMessages.filter(entry => entry.message !== message);
|
|
14453
|
+
this.#externalSteerMessages.delete(message);
|
|
14454
|
+
this.#externalFollowUps.delete(message);
|
|
14455
|
+
this.#sequentialSteerMessages.delete(message);
|
|
14456
|
+
this.#fireQueuedRemovalHooks([message]);
|
|
14457
|
+
this.#releaseDeferredSdkFollowUps();
|
|
14458
|
+
return true;
|
|
14459
|
+
};
|
|
14460
|
+
options?.onQueuedAfterAdmission?.(message, cancelQueuedSteer);
|
|
14461
|
+
return { cancel: cancelQueuedSteer };
|
|
14462
|
+
}
|
|
14463
|
+
|
|
14464
|
+
/**
|
|
14465
|
+
* One-shot preflight-abort binding: an invocation cancelled after its queue
|
|
14466
|
+
* admission must cancel the submission it admitted, so an aborted dispatch
|
|
14467
|
+
* can never execute later. Both explicit delivery paths (follow-up and
|
|
14468
|
+
* steer) share this helper so their cancellation semantics cannot diverge
|
|
14469
|
+
* (exact-head review P1).
|
|
14470
|
+
*/
|
|
14471
|
+
#bindPreflightAbortCancellation(
|
|
14472
|
+
signal: AbortSignal | undefined,
|
|
14473
|
+
owner: QueuedFollowUpOwner,
|
|
14474
|
+
state?: TrackedQueuedInput,
|
|
14475
|
+
): void {
|
|
14476
|
+
if (!signal) return;
|
|
14477
|
+
const cancelQueued = () => {
|
|
14478
|
+
if (state) state.submission.cancel();
|
|
14479
|
+
else owner.cancel();
|
|
14480
|
+
};
|
|
14481
|
+
const removeListener = () => signal.removeEventListener("abort", cancelQueued);
|
|
14482
|
+
if (state) state.removePreflightAbortListener = removeListener;
|
|
14483
|
+
signal.addEventListener("abort", cancelQueued, { once: true });
|
|
14484
|
+
if (signal.aborted) cancelQueued();
|
|
13263
14485
|
}
|
|
13264
14486
|
|
|
13265
14487
|
/**
|
|
@@ -13288,7 +14510,12 @@ export class AgentSession {
|
|
|
13288
14510
|
* run that would have polled the queue is gone, so nothing else owns it.
|
|
13289
14511
|
*/
|
|
13290
14512
|
#scheduleQueuedDelivery(delayMs?: number): void {
|
|
13291
|
-
if (this.#cancelAndSubmitInProgress
|
|
14513
|
+
if (this.#cancelAndSubmitInProgress) return;
|
|
14514
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) {
|
|
14515
|
+
this.#queuedDeliveryPendingWhileTransition = true;
|
|
14516
|
+
return;
|
|
14517
|
+
}
|
|
14518
|
+
if (!this.#canDeliverQueuedMessages()) return;
|
|
13292
14519
|
this.#scheduleAgentContinue({
|
|
13293
14520
|
...(delayMs === undefined ? {} : { delayMs }),
|
|
13294
14521
|
shouldContinue: () => this.#canDeliverQueuedMessages() && this.agent.hasQueuedMessages(),
|
|
@@ -13303,109 +14530,201 @@ export class AgentSession {
|
|
|
13303
14530
|
async #queueFollowUp(
|
|
13304
14531
|
text: string,
|
|
13305
14532
|
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
|
-
},
|
|
14533
|
+
options?: QueueFollowUpOptions,
|
|
13314
14534
|
): Promise<QueuedFollowUpOwner> {
|
|
13315
|
-
this.#
|
|
14535
|
+
this.#assertExternalSessionIngress({
|
|
14536
|
+
allowTrackedSuccessor: options?.allowDuringSessionTransition,
|
|
14537
|
+
allowCancelAndSubmit: options?.allowCancelAndSubmit,
|
|
14538
|
+
});
|
|
14539
|
+
assertImagePlaceholdersHavePayload(text, images);
|
|
14540
|
+
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
14541
|
+
if (images && images.length > 0) content.push(...images);
|
|
14542
|
+
const message = { role: "user" as const, content, attribution: "user" as const, timestamp: Date.now() };
|
|
14543
|
+
return this.#queueFollowUpMessage(message, text || (images && images.length > 0 ? "[Image]" : ""), options);
|
|
14544
|
+
}
|
|
14545
|
+
|
|
14546
|
+
async #queueFollowUpTextAfterReservation(
|
|
14547
|
+
text: string,
|
|
14548
|
+
images?: ImageContent[],
|
|
14549
|
+
options?: QueueFollowUpOptions,
|
|
14550
|
+
): Promise<QueuedFollowUpOwner> {
|
|
14551
|
+
this.#assertExternalSessionIngress({
|
|
14552
|
+
allowTrackedSuccessor: options?.allowDuringSessionTransition,
|
|
14553
|
+
allowCancelAndSubmit: options?.allowCancelAndSubmit,
|
|
14554
|
+
});
|
|
13316
14555
|
assertImagePlaceholdersHavePayload(text, images);
|
|
13317
|
-
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
13318
|
-
const queueWasEmpty = !this.agent.hasQueuedMessages();
|
|
13319
14556
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
13320
14557
|
if (images && images.length > 0) content.push(...images);
|
|
13321
14558
|
const message = { role: "user" as const, content, attribution: "user" as const, timestamp: Date.now() };
|
|
14559
|
+
return this.#queueFollowUpAfterReservation(
|
|
14560
|
+
message,
|
|
14561
|
+
text || (images && images.length > 0 ? "[Image]" : ""),
|
|
14562
|
+
options,
|
|
14563
|
+
);
|
|
14564
|
+
}
|
|
14565
|
+
|
|
14566
|
+
#queueFollowUpMessage(
|
|
14567
|
+
message: AgentMessage,
|
|
14568
|
+
displayText: string,
|
|
14569
|
+
options?: QueueFollowUpOptions,
|
|
14570
|
+
): QueuedFollowUpOwner {
|
|
14571
|
+
const queueWasEmpty = !this.agent.hasQueuedMessages();
|
|
13322
14572
|
options?.onQueued?.(message);
|
|
13323
|
-
|
|
13324
|
-
|
|
13325
|
-
|
|
13326
|
-
|
|
13327
|
-
this.#
|
|
14573
|
+
const displayEntry =
|
|
14574
|
+
options?.createDisplayEntry === false
|
|
14575
|
+
? undefined
|
|
14576
|
+
: this.#createQueuedDisplayEntry(displayText, undefined, message);
|
|
14577
|
+
if (displayEntry) this.#followUpMessages.push(displayEntry);
|
|
14578
|
+
if (options?.trackExternalFollowUp !== false) this.#externalFollowUps.add(message);
|
|
13328
14579
|
if (options?.onPromoted) this.#followUpPromotionHooks.set(message, options.onPromoted);
|
|
14580
|
+
if (options?.sdkRunToken || options?.onQueuedAfterAdmission) {
|
|
14581
|
+
this.#queuedAdmissionSeq.set(message, ++this.#queuedAdmissionSequence);
|
|
14582
|
+
}
|
|
13329
14583
|
if (options?.claimsGenuineUserIntent) {
|
|
13330
14584
|
const epoch = this.#claimDeepInterviewUserIntent();
|
|
13331
14585
|
this.#deepInterviewGenuineUserMessageEpochs.set(message, epoch);
|
|
13332
14586
|
}
|
|
13333
14587
|
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();
|
|
14588
|
+
// If older deferred work is waiting while the Agent is idle, release
|
|
14589
|
+
// that head before parking this newer message. Otherwise the newer message
|
|
14590
|
+
// would enter the live queue first and invert SDK FIFO ownership.
|
|
14591
|
+
if (!this.agent.state.isStreaming && !this.agent.hasQueuedMessages() && this.#deferredSdkFollowUps.length > 0) {
|
|
14592
|
+
this.#releaseDeferredSdkFollowUps();
|
|
13349
14593
|
}
|
|
13350
|
-
|
|
14594
|
+
const owner: QueuedFollowUpOwner = {
|
|
13351
14595
|
cancel: () => {
|
|
13352
14596
|
const deferredIndex = this.#deferredSdkFollowUps.indexOf(message);
|
|
13353
14597
|
let removed = false;
|
|
13354
14598
|
if (deferredIndex !== -1) {
|
|
13355
14599
|
this.#deferredSdkFollowUps.splice(deferredIndex, 1);
|
|
14600
|
+
this.#forgetDeferredFollowUpMetadata(message);
|
|
13356
14601
|
removed = true;
|
|
13357
14602
|
} else {
|
|
13358
14603
|
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
14604
|
}
|
|
13365
14605
|
if (removed) {
|
|
13366
|
-
this.#followUpMessages =
|
|
13367
|
-
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
//
|
|
13371
|
-
|
|
13372
|
-
|
|
14606
|
+
this.#followUpMessages = displayEntry
|
|
14607
|
+
? this.#followUpMessages.filter(entry => entry !== displayEntry)
|
|
14608
|
+
: this.#followUpMessages.filter(entry => entry.message !== message);
|
|
14609
|
+
this.#fireQueuedRemovalHooks([message]);
|
|
14610
|
+
// Either removal site can unblock the NEXT deferred SDK follow-up:
|
|
14611
|
+
// the cancelled entry may itself have been the deferred head, or it
|
|
14612
|
+
// may have been the live work that deferred head was waiting behind.
|
|
14613
|
+
// Without this release the successor stays accepted with no execution
|
|
14614
|
+
// or terminal settlement (exact-head review P1).
|
|
14615
|
+
this.#releaseDeferredSdkFollowUps();
|
|
13373
14616
|
}
|
|
13374
14617
|
return removed;
|
|
13375
14618
|
},
|
|
13376
14619
|
};
|
|
14620
|
+
options?.onQueuedAfterAdmission?.(message, owner.cancel);
|
|
14621
|
+
// Existing deferred work is part of the same FIFO even when the Agent
|
|
14622
|
+
// queue is currently empty. Enqueuing a newer successor directly into the
|
|
14623
|
+
// Agent in that state would let it run before the older deferred request.
|
|
14624
|
+
if (
|
|
14625
|
+
this.#deferredSdkFollowUps.length > 0 ||
|
|
14626
|
+
(options?.sdkRunToken && (this.agent.state.isStreaming || this.agent.hasQueuedMessages()))
|
|
14627
|
+
) {
|
|
14628
|
+
if (options?.forceOneAtATime || options?.sdkRunToken) {
|
|
14629
|
+
this.#deferredFollowUpForceOneAtATime.add(message);
|
|
14630
|
+
}
|
|
14631
|
+
this.#deferredSdkFollowUps.push(message);
|
|
14632
|
+
} else {
|
|
14633
|
+
this.agent.followUp(message, options?.forceOneAtATime ? { forceOneAtATime: true } : undefined);
|
|
14634
|
+
}
|
|
14635
|
+
if (queueWasEmpty) {
|
|
14636
|
+
this.#scheduleQueuedFollowUpContinuation(() =>
|
|
14637
|
+
this.agent.snapshotFollowUp().some(candidate => candidate === message),
|
|
14638
|
+
);
|
|
14639
|
+
if (options?.scheduleNonAdmittedWake !== false) this.#scheduleNonAdmittedQueuedContinuation();
|
|
14640
|
+
}
|
|
14641
|
+
return owner;
|
|
14642
|
+
}
|
|
14643
|
+
|
|
14644
|
+
#forgetDeferredFollowUpMetadata(message: AgentMessage): void {
|
|
14645
|
+
this.#deferredFollowUpForceOneAtATime.delete(message);
|
|
14646
|
+
const batch = this.#deferredFollowUpBatches.get(message);
|
|
14647
|
+
if (!batch) return;
|
|
14648
|
+
const remaining = batch.filter(candidate => this.#deferredSdkFollowUps.includes(candidate));
|
|
14649
|
+
for (const member of batch) this.#deferredFollowUpBatches.delete(member);
|
|
14650
|
+
if (remaining.length > 1) for (const member of remaining) this.#deferredFollowUpBatches.set(member, remaining);
|
|
14651
|
+
}
|
|
14652
|
+
|
|
14653
|
+
async #queueFollowUpAfterReservation(
|
|
14654
|
+
message: AgentMessage,
|
|
14655
|
+
displayText: string,
|
|
14656
|
+
options?: QueueFollowUpOptions,
|
|
14657
|
+
reservationSignal?: AbortSignal,
|
|
14658
|
+
): Promise<QueuedFollowUpOwner> {
|
|
14659
|
+
if (reservationSignal?.aborted) throw promptPreflightCancelledError();
|
|
14660
|
+
const transitionGeneration = this.#coordinatorPersistGeneration;
|
|
14661
|
+
const reservationEpoch = ++this.#followUpReservationEpoch;
|
|
14662
|
+
this.#activeFollowUpReservationEpochs.add(reservationEpoch);
|
|
14663
|
+
const releaseReservation = () => {
|
|
14664
|
+
this.#activeFollowUpReservationEpochs.delete(reservationEpoch);
|
|
14665
|
+
const waiters = [...this.#followUpReservationDrainWaiters];
|
|
14666
|
+
this.#followUpReservationDrainWaiters.clear();
|
|
14667
|
+
for (const waiter of waiters) waiter();
|
|
14668
|
+
};
|
|
14669
|
+
try {
|
|
14670
|
+
if ([...this.#activeFollowUpReservationEpochs].some(epoch => epoch < reservationEpoch)) {
|
|
14671
|
+
await this.#waitForEarlierFollowUpReservations(
|
|
14672
|
+
reservationEpoch,
|
|
14673
|
+
reservationSignal,
|
|
14674
|
+
undefined,
|
|
14675
|
+
transitionGeneration,
|
|
14676
|
+
);
|
|
14677
|
+
}
|
|
14678
|
+
if (reservationSignal?.aborted) throw promptPreflightCancelledError();
|
|
14679
|
+
this.#assertExternalSessionIngress({
|
|
14680
|
+
allowTrackedSuccessor: options?.allowDuringSessionTransition,
|
|
14681
|
+
allowCancelAndSubmit: options?.allowCancelAndSubmit,
|
|
14682
|
+
});
|
|
14683
|
+
return this.#queueFollowUpMessage(message, displayText, options);
|
|
14684
|
+
} finally {
|
|
14685
|
+
releaseReservation();
|
|
14686
|
+
}
|
|
13377
14687
|
}
|
|
13378
|
-
|
|
14688
|
+
|
|
14689
|
+
#releaseDeferredSdkFollowUps(): boolean {
|
|
13379
14690
|
// A deferred SDK follow-up must become the sole first message at the next
|
|
13380
14691
|
// acceptance so its run token is bound to the agent_start. Releasing it
|
|
13381
14692
|
// behind still-queued work reproduces the token-less mid-run consumption
|
|
13382
14693
|
// 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.
|
|
14694
|
+
if (this.agent.state.isStreaming || this.agent.hasQueuedMessages()) return false;
|
|
14695
|
+
const message = this.#deferredSdkFollowUps[0];
|
|
14696
|
+
if (!message) return false;
|
|
14697
|
+
const batch = this.#deferredFollowUpBatches.get(message);
|
|
14698
|
+
const released =
|
|
14699
|
+
batch && batch.length > 1 && batch.every((candidate, index) => this.#deferredSdkFollowUps[index] === candidate)
|
|
14700
|
+
? [...batch]
|
|
14701
|
+
: [message];
|
|
14702
|
+
const forceOneAtATime = this.#deferredFollowUpForceOneAtATime.has(message);
|
|
14703
|
+
this.#deferredSdkFollowUps.splice(0, released.length);
|
|
14704
|
+
for (const candidate of released) this.#forgetDeferredFollowUpMetadata(candidate);
|
|
14705
|
+
if (released.length > 1) {
|
|
14706
|
+
this.agent.markFollowUpBatch(released);
|
|
14707
|
+
this.agent.restoreFollowUp(released);
|
|
14708
|
+
} else {
|
|
14709
|
+
this.agent.followUp(message, forceOneAtATime ? { forceOneAtATime: true } : undefined);
|
|
14710
|
+
}
|
|
14711
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) {
|
|
14712
|
+
this.#queuedDeliveryPendingWhileTransition = true;
|
|
14713
|
+
return true;
|
|
14714
|
+
}
|
|
13387
14715
|
this.#scheduleAgentContinue({
|
|
13388
|
-
shouldContinue: () => this.#
|
|
14716
|
+
shouldContinue: () => this.#canDeliverQueuedMessages() && this.agent.hasQueuedMessages(),
|
|
13389
14717
|
rescheduleOnBusy: true,
|
|
13390
14718
|
continueQueuedOnly: true,
|
|
13391
14719
|
});
|
|
14720
|
+
return true;
|
|
13392
14721
|
}
|
|
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
14722
|
/**
|
|
13405
14723
|
* Gate for idle-path follow-up auto-continue. See `#queueFollowUp` for rationale.
|
|
13406
14724
|
*/
|
|
13407
14725
|
#canAutoContinueForFollowUp(): boolean {
|
|
13408
14726
|
if (this.#abortUnwind) return false;
|
|
14727
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) return false;
|
|
13409
14728
|
if (this.isStreaming) return false;
|
|
13410
14729
|
if (this.isCompacting) return false;
|
|
13411
14730
|
if (this.isBashRunning) return false;
|
|
@@ -13441,6 +14760,7 @@ export class AgentSession {
|
|
|
13441
14760
|
*/
|
|
13442
14761
|
#canDeliverQueuedMessages(): boolean {
|
|
13443
14762
|
if (this.#abortUnwind) return false;
|
|
14763
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) return false;
|
|
13444
14764
|
if (this.agent.state.isStreaming) return false;
|
|
13445
14765
|
if (this.isRetrying) return false;
|
|
13446
14766
|
if (this.isCompacting) return false;
|
|
@@ -13458,6 +14778,7 @@ export class AgentSession {
|
|
|
13458
14778
|
*/
|
|
13459
14779
|
#canAutoContinueForSteer(): boolean {
|
|
13460
14780
|
if (this.#abortUnwind) return false;
|
|
14781
|
+
if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) return false;
|
|
13461
14782
|
if (this.agent.state.isStreaming) return false;
|
|
13462
14783
|
if (this.isRetrying) return false;
|
|
13463
14784
|
const messages = this.agent.state.messages;
|
|
@@ -13473,11 +14794,13 @@ export class AgentSession {
|
|
|
13473
14794
|
}
|
|
13474
14795
|
|
|
13475
14796
|
queueDeferredMessage(message: CustomMessage): void {
|
|
13476
|
-
this.#
|
|
14797
|
+
this.#assertExternalSessionIngress({ allowCancelAndSubmit: true });
|
|
14798
|
+
this.#queueHiddenNextTurnMessage(message, true, "turn", true);
|
|
13477
14799
|
}
|
|
13478
14800
|
|
|
13479
14801
|
queueDeferredMessageForTests(message: CustomMessage, triggerTurn = true): void {
|
|
13480
|
-
this.#
|
|
14802
|
+
this.#assertExternalSessionIngress({ allowCancelAndSubmit: true });
|
|
14803
|
+
this.#queueHiddenNextTurnMessage(message, triggerTurn, "turn", true);
|
|
13481
14804
|
}
|
|
13482
14805
|
|
|
13483
14806
|
/** Read-only test seam for the hidden next-turn context queue. */
|
|
@@ -13494,11 +14817,12 @@ export class AgentSession {
|
|
|
13494
14817
|
message: CustomMessage,
|
|
13495
14818
|
triggerTurn: boolean,
|
|
13496
14819
|
origin: "turn" | "external" = "external",
|
|
14820
|
+
allowCancelAndSubmit = false,
|
|
13497
14821
|
): void {
|
|
13498
14822
|
// A hidden next-turn message queued during a handoff transition would be
|
|
13499
14823
|
// dropped when the successor clears predecessor queues; reject it as busy so
|
|
13500
14824
|
// the caller can retry against the settled session.
|
|
13501
|
-
this.#
|
|
14825
|
+
this.#assertExternalSessionIngress({ allowCancelAndSubmit });
|
|
13502
14826
|
this.#pendingNextTurnMessages.push({ message, origin });
|
|
13503
14827
|
if (!triggerTurn) return;
|
|
13504
14828
|
const generation = this.#promptGeneration;
|
|
@@ -13662,7 +14986,7 @@ export class AgentSession {
|
|
|
13662
14986
|
* monitor's registration, so removing it after an intermediate notification
|
|
13663
14987
|
* would let a later scope:"owned" abort miss the monitor and make subsequent
|
|
13664
14988
|
* notifications lose their envelope (review thread P1). */
|
|
13665
|
-
#settleDeliveredOwnedRegistrations(messages: AgentMessage[]): void {
|
|
14989
|
+
#settleDeliveredOwnedRegistrations(messages: readonly AgentMessage[]): void {
|
|
13666
14990
|
// Use the SESSION-OWNED manager first: the process-global instance is
|
|
13667
14991
|
// the last-created session, so checking this session's delivery against
|
|
13668
14992
|
// another session's manager (which lacks the same local job id) would
|
|
@@ -13771,6 +15095,7 @@ export class AgentSession {
|
|
|
13771
15095
|
},
|
|
13772
15096
|
): Promise<void> {
|
|
13773
15097
|
this.#assertRecoveryHydrationPromoted();
|
|
15098
|
+
this.#assertExternalSessionIngress();
|
|
13774
15099
|
const appMessage: CustomMessage<T> = {
|
|
13775
15100
|
role: "custom",
|
|
13776
15101
|
customType: message.customType,
|
|
@@ -13817,7 +15142,11 @@ export class AgentSession {
|
|
|
13817
15142
|
this.#settleDeliveredOwnedRegistrations([appMessage]);
|
|
13818
15143
|
return;
|
|
13819
15144
|
}
|
|
13820
|
-
this
|
|
15145
|
+
await this.#queueFollowUpAfterReservation(appMessage, this.#getCustomMessageTextContent(appMessage), {
|
|
15146
|
+
forceOneAtATime: sequential !== undefined,
|
|
15147
|
+
createDisplayEntry: false,
|
|
15148
|
+
trackExternalFollowUp: false,
|
|
15149
|
+
});
|
|
13821
15150
|
if (this.#abortUnwind && !sequential) this.#abortUnwindSteerFallbacks.push(appMessage);
|
|
13822
15151
|
// The chip now describes follow-up work: keep its mode and identity
|
|
13823
15152
|
// aligned with where the executable message actually landed.
|
|
@@ -13843,10 +15172,11 @@ export class AgentSession {
|
|
|
13843
15172
|
return;
|
|
13844
15173
|
}
|
|
13845
15174
|
|
|
13846
|
-
this
|
|
13847
|
-
|
|
13848
|
-
|
|
13849
|
-
|
|
15175
|
+
await this.#queueFollowUpAfterReservation(appMessage, this.#getCustomMessageTextContent(appMessage), {
|
|
15176
|
+
forceOneAtATime: options?.followUpQueuePolicy === "sequential",
|
|
15177
|
+
createDisplayEntry: false,
|
|
15178
|
+
trackExternalFollowUp: false,
|
|
15179
|
+
});
|
|
13850
15180
|
this.#bindCustomDisplayEntry(appMessage, "followUp");
|
|
13851
15181
|
// The session can report streaming while no agent loop owns the queue
|
|
13852
15182
|
// (post-prompt unwind): without a scheduled delivery a cron/monitor
|
|
@@ -13952,13 +15282,22 @@ export class AgentSession {
|
|
|
13952
15282
|
purgeQueuedCustomMessages(predicate: (message: CustomMessage) => boolean): PurgeQueuedCustomMessagesResult {
|
|
13953
15283
|
const isMatch = (m: AgentMessage): boolean => m.role === "custom" && predicate(m as CustomMessage);
|
|
13954
15284
|
const removedTags = new Set<string>();
|
|
13955
|
-
|
|
15285
|
+
const steeringBefore = this.agent.snapshotSteering();
|
|
15286
|
+
const followUpBefore = this.agent.snapshotFollowUp();
|
|
15287
|
+
const deferredBefore = [...this.#deferredSdkFollowUps];
|
|
15288
|
+
for (const m of [...steeringBefore, ...followUpBefore, ...deferredBefore]) {
|
|
13956
15289
|
if (isMatch(m)) {
|
|
13957
15290
|
const tag = readPendingDisplayTag((m as CustomMessage).details);
|
|
13958
15291
|
if (tag) removedTags.add(tag);
|
|
13959
15292
|
}
|
|
13960
15293
|
}
|
|
13961
15294
|
const agentRemoved = this.agent.removeQueuedMessages(isMatch);
|
|
15295
|
+
const removedAgentMessages = [...steeringBefore, ...followUpBefore].filter(isMatch);
|
|
15296
|
+
const removedDeferred = deferredBefore.filter(isMatch);
|
|
15297
|
+
if (removedDeferred.length > 0) {
|
|
15298
|
+
this.#deferredSdkFollowUps = this.#deferredSdkFollowUps.filter(message => !isMatch(message));
|
|
15299
|
+
for (const message of removedDeferred) this.#forgetDeferredFollowUpMetadata(message);
|
|
15300
|
+
}
|
|
13962
15301
|
const beforeNext = this.#pendingNextTurnMessages.length;
|
|
13963
15302
|
for (const entry of this.#pendingNextTurnMessages) {
|
|
13964
15303
|
if (predicate(entry.message)) {
|
|
@@ -13968,6 +15307,7 @@ export class AgentSession {
|
|
|
13968
15307
|
}
|
|
13969
15308
|
this.#pendingNextTurnMessages = this.#pendingNextTurnMessages.filter(entry => !predicate(entry.message));
|
|
13970
15309
|
const pendingNextTurn = beforeNext - this.#pendingNextTurnMessages.length;
|
|
15310
|
+
this.#fireQueuedRemovalHooks([...removedAgentMessages, ...removedDeferred]);
|
|
13971
15311
|
let displaySteering = 0;
|
|
13972
15312
|
let displayFollowUp = 0;
|
|
13973
15313
|
if (removedTags.size > 0) {
|
|
@@ -13978,6 +15318,7 @@ export class AgentSession {
|
|
|
13978
15318
|
this.#followUpMessages = this.#followUpMessages.filter(e => !(e.tag && removedTags.has(e.tag)));
|
|
13979
15319
|
displayFollowUp = beforeF - this.#followUpMessages.length;
|
|
13980
15320
|
}
|
|
15321
|
+
if (agentRemoved.total > 0 || removedDeferred.length > 0) this.#releaseDeferredSdkFollowUps();
|
|
13981
15322
|
return {
|
|
13982
15323
|
agentSteering: agentRemoved.steering,
|
|
13983
15324
|
agentFollowUp: agentRemoved.followUp,
|
|
@@ -13997,26 +15338,45 @@ export class AgentSession {
|
|
|
13997
15338
|
*/
|
|
13998
15339
|
async sendUserMessage(
|
|
13999
15340
|
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
|
-
},
|
|
15341
|
+
options?: SendUserMessageOptions,
|
|
15342
|
+
): Promise<void>;
|
|
15343
|
+
async sendUserMessage(
|
|
15344
|
+
content: string | (TextContent | ImageContent)[],
|
|
15345
|
+
options?: SendUserMessageOptions,
|
|
14013
15346
|
): Promise<void> {
|
|
15347
|
+
assertLegacySendUserMessageOptions(options);
|
|
15348
|
+
await this.#sendUserMessage(content, options);
|
|
15349
|
+
}
|
|
15350
|
+
|
|
15351
|
+
/**
|
|
15352
|
+
* Queue one steer or follow-up and return a stable lifecycle handle for that
|
|
15353
|
+
* exact submission. Unlike `sendUserMessage`, this method resolves when the
|
|
15354
|
+
* queue admission receipt is available; use the returned promises to await
|
|
15355
|
+
* execution and the owning terminal boundary.
|
|
15356
|
+
*/
|
|
15357
|
+
async submitUserMessage(
|
|
15358
|
+
content: string | (TextContent | ImageContent)[],
|
|
15359
|
+
options: TrackedSendUserMessageOptions,
|
|
15360
|
+
): Promise<QueuedInputSubmission> {
|
|
15361
|
+
assertTrackedSendUserMessageOptions(options);
|
|
15362
|
+
const submission = await this.#sendUserMessage(content, options);
|
|
15363
|
+
if (submission === undefined) throw new Error("Tracked user message did not produce a submission handle.");
|
|
15364
|
+
return submission;
|
|
15365
|
+
}
|
|
15366
|
+
|
|
15367
|
+
async #sendUserMessage(
|
|
15368
|
+
content: string | (TextContent | ImageContent)[],
|
|
15369
|
+
options?: SendUserMessageDispatchOptions,
|
|
15370
|
+
): Promise<undefined | QueuedInputSubmission> {
|
|
15371
|
+
assertQueuedInputQueuePolicy(options?.queuePolicy);
|
|
14014
15372
|
const sdkRunToken = readSdkRunCapability(options?.sdkRunCapability);
|
|
15373
|
+
const submissionTransitionGeneration = this.#coordinatorPersistGeneration;
|
|
14015
15374
|
const internalOptions = options ? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) } : undefined;
|
|
14016
15375
|
this.#assertRecoveryHydrationPromoted();
|
|
15376
|
+
const trackedSuccessorAdmission = this.#hasCommittedSuccessorTrackedAdmission(options);
|
|
15377
|
+
this.#assertExternalSessionIngress({ allowTrackedSuccessor: trackedSuccessorAdmission });
|
|
14017
15378
|
const owner = this.#sessionAdmissionContext.getStore();
|
|
14018
|
-
if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
|
|
14019
|
-
this.#assertSessionAdmissionOpen();
|
|
15379
|
+
if (owner && !owner.released && !trackedSuccessorAdmission) throw this.#sessionAdmissionBusyError();
|
|
14020
15380
|
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
14021
15381
|
if (typeof content !== "string" && !Array.isArray(content)) {
|
|
14022
15382
|
throw Object.assign(new Error("sendUserMessage requires string or content-array content."), {
|
|
@@ -14054,6 +15414,16 @@ export class AgentSession {
|
|
|
14054
15414
|
images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
|
|
14055
15415
|
if (text.trim().length === 0 && !hasUsableImage)
|
|
14056
15416
|
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
15417
|
+
if (options?.trackSubmission === true && options.deliverAs === undefined)
|
|
15418
|
+
throw Object.assign(new Error("trackSubmission requires deliverAs: steer or followUp."), {
|
|
15419
|
+
code: "invalid_input",
|
|
15420
|
+
});
|
|
15421
|
+
let trackedQueuedInput: TrackedQueuedInput | undefined;
|
|
15422
|
+
let onTrackedQueued: ((message: AgentMessage, cancelQueued: () => boolean) => void) | undefined;
|
|
15423
|
+
const onQueuedPromoted = (promotion: { startsOwnRun?: boolean; removed?: boolean }): void => {
|
|
15424
|
+
if (trackedQueuedInput) this.#handleTrackedQueuedInputPromotion(trackedQueuedInput, promotion);
|
|
15425
|
+
options?.onQueuedPromoted?.(promotion);
|
|
15426
|
+
};
|
|
14057
15427
|
|
|
14058
15428
|
let admissionSignal = options?.preflightSignal
|
|
14059
15429
|
? AbortSignal.any([this.#promptPreflightAbortController.signal, options.preflightSignal])
|
|
@@ -14120,7 +15490,19 @@ export class AgentSession {
|
|
|
14120
15490
|
await awaitPromptInvocationPreflight(this.#selectionFenceTail, admissionSignal);
|
|
14121
15491
|
}
|
|
14122
15492
|
const assertPreflightStillOpen = () => {
|
|
14123
|
-
this.#
|
|
15493
|
+
this.#assertExternalSessionIngress({ allowTrackedSuccessor: trackedSuccessorAdmission });
|
|
15494
|
+
if (
|
|
15495
|
+
(this.#sessionTransitionKind !== undefined ||
|
|
15496
|
+
this.#coordinatorPersistGeneration !== submissionTransitionGeneration) &&
|
|
15497
|
+
!this.#hasCommittedSuccessorTrackedAdmission(options)
|
|
15498
|
+
) {
|
|
15499
|
+
throw Object.assign(
|
|
15500
|
+
new AgentBusyError(
|
|
15501
|
+
`Cannot submit queued input while ${this.#sessionTransitionKind ?? "a session state transition"} is in progress.`,
|
|
15502
|
+
),
|
|
15503
|
+
{ code: "busy" },
|
|
15504
|
+
);
|
|
15505
|
+
}
|
|
14124
15506
|
if (
|
|
14125
15507
|
options?.preflightSignal?.aborted ||
|
|
14126
15508
|
this.#promptPreflightCancellationGeneration !== preflightCancellationGeneration
|
|
@@ -14139,33 +15521,69 @@ export class AgentSession {
|
|
|
14139
15521
|
followUpReservationEpoch,
|
|
14140
15522
|
options?.preflightSignal,
|
|
14141
15523
|
admissionSignal,
|
|
15524
|
+
submissionTransitionGeneration,
|
|
14142
15525
|
);
|
|
14143
15526
|
}
|
|
15527
|
+
assertPreflightStillOpen();
|
|
14144
15528
|
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
14145
15529
|
assertPreflightStillOpen();
|
|
15530
|
+
if (options?.trackSubmission === true) {
|
|
15531
|
+
trackedQueuedInput = this.#createTrackedQueuedInput(deliverAs, options.queuePolicy ?? "respect-mode");
|
|
15532
|
+
onTrackedQueued = (message: AgentMessage, cancelQueued: () => boolean) =>
|
|
15533
|
+
this.#admitTrackedQueuedInput(trackedQueuedInput!, message, cancelQueued);
|
|
15534
|
+
}
|
|
14146
15535
|
const queuedFollowUp = await this.#queueFollowUp(text, images, {
|
|
14147
15536
|
claimsGenuineUserIntent: true,
|
|
14148
|
-
forceOneAtATime: Boolean(options?.
|
|
14149
|
-
onPromoted:
|
|
15537
|
+
forceOneAtATime: Boolean(options?.queuedAtDispatch || options?.queuePolicy === "sequential"),
|
|
15538
|
+
onPromoted: onQueuedPromoted,
|
|
14150
15539
|
sdkRunToken: internalOptions?.sdkRunToken,
|
|
15540
|
+
onQueuedAfterAdmission: onTrackedQueued,
|
|
15541
|
+
allowDuringSessionTransition: this.#hasCommittedSuccessorTrackedAdmission(options),
|
|
14151
15542
|
});
|
|
14152
|
-
|
|
14153
|
-
|
|
14154
|
-
|
|
14155
|
-
|
|
14156
|
-
|
|
15543
|
+
this.#bindPreflightAbortCancellation(options?.preflightSignal, queuedFollowUp, trackedQueuedInput);
|
|
15544
|
+
try {
|
|
15545
|
+
assertPreflightStillOpen();
|
|
15546
|
+
options?.onPreflightAccepted?.();
|
|
15547
|
+
assertPreflightStillOpen();
|
|
15548
|
+
} catch (error) {
|
|
15549
|
+
if (trackedQueuedInput) this.#settleTrackedAdmissionCallbackFailure(trackedQueuedInput, queuedFollowUp);
|
|
15550
|
+
else queuedFollowUp.cancel();
|
|
15551
|
+
throw error;
|
|
15552
|
+
}
|
|
15553
|
+
return trackedQueuedInput?.submission;
|
|
14157
15554
|
}
|
|
14158
15555
|
if (deliverAs === "steer") {
|
|
14159
15556
|
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
14160
15557
|
assertPreflightStillOpen();
|
|
14161
|
-
|
|
15558
|
+
if (options?.trackSubmission === true) {
|
|
15559
|
+
trackedQueuedInput = this.#createTrackedQueuedInput(deliverAs, options.queuePolicy ?? "respect-mode");
|
|
15560
|
+
onTrackedQueued = (message: AgentMessage, cancelQueued: () => boolean) =>
|
|
15561
|
+
this.#admitTrackedQueuedInput(trackedQueuedInput!, message, cancelQueued);
|
|
15562
|
+
}
|
|
15563
|
+
const queuedSteer = await this.#queueSteer(text, images, {
|
|
14162
15564
|
claimsGenuineUserIntent: true,
|
|
14163
|
-
onPromoted:
|
|
15565
|
+
onPromoted: onQueuedPromoted,
|
|
14164
15566
|
external: true,
|
|
14165
15567
|
sdkRunToken: internalOptions?.sdkRunToken,
|
|
15568
|
+
forceOneAtATime: options?.queuePolicy === "sequential",
|
|
15569
|
+
onQueuedAfterAdmission: onTrackedQueued,
|
|
15570
|
+
allowDuringSessionTransition: this.#hasCommittedSuccessorTrackedAdmission(options),
|
|
14166
15571
|
});
|
|
14167
|
-
|
|
14168
|
-
|
|
15572
|
+
// An explicit steer is admitted to the live loop immediately, so an
|
|
15573
|
+
// aborted invocation must cancel the exact submission it admitted:
|
|
15574
|
+
// otherwise the steer stays executable after its caller gave up
|
|
15575
|
+
// (exact-head review P1).
|
|
15576
|
+
this.#bindPreflightAbortCancellation(options?.preflightSignal, queuedSteer, trackedQueuedInput);
|
|
15577
|
+
try {
|
|
15578
|
+
assertPreflightStillOpen();
|
|
15579
|
+
options?.onPreflightAccepted?.();
|
|
15580
|
+
assertPreflightStillOpen();
|
|
15581
|
+
} catch (error) {
|
|
15582
|
+
if (trackedQueuedInput) this.#settleTrackedAdmissionCallbackFailure(trackedQueuedInput, queuedSteer);
|
|
15583
|
+
else queuedSteer.cancel();
|
|
15584
|
+
throw error;
|
|
15585
|
+
}
|
|
15586
|
+
return trackedQueuedInput?.submission;
|
|
14169
15587
|
}
|
|
14170
15588
|
|
|
14171
15589
|
// No explicit delivery mode: only a live stream makes prompt() throw
|
|
@@ -14225,6 +15643,13 @@ export class AgentSession {
|
|
|
14225
15643
|
preflightSignal: options?.preflightSignal,
|
|
14226
15644
|
} as InternalPromptOptions);
|
|
14227
15645
|
} finally {
|
|
15646
|
+
// A preflight fence or queue admission error can occur after the tracked
|
|
15647
|
+
// state is allocated but before a queue callback binds its exact message.
|
|
15648
|
+
// No handle escaped in that case, so discard the unreachable state rather
|
|
15649
|
+
// than retaining an unresolved submission forever.
|
|
15650
|
+
if (trackedQueuedInput?.message === undefined) {
|
|
15651
|
+
if (trackedQueuedInput) this.#settleTrackedQueuedInputRemoved(trackedQueuedInput, "removed");
|
|
15652
|
+
}
|
|
14228
15653
|
releaseFollowUpReservation();
|
|
14229
15654
|
}
|
|
14230
15655
|
}
|
|
@@ -14346,15 +15771,36 @@ export class AgentSession {
|
|
|
14346
15771
|
const deferredIndex = this.#deferredSdkFollowUps.indexOf(removedMessage);
|
|
14347
15772
|
if (deferredIndex !== -1) {
|
|
14348
15773
|
this.#deferredSdkFollowUps.splice(deferredIndex, 1);
|
|
15774
|
+
this.#forgetDeferredFollowUpMetadata(removedMessage);
|
|
14349
15775
|
} else {
|
|
14350
15776
|
this.agent.removeQueuedMessages(candidate => candidate === removedMessage);
|
|
14351
15777
|
}
|
|
14352
15778
|
} else if (index >= 0) {
|
|
14353
|
-
// Legacy entries without an identity link
|
|
14354
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
15779
|
+
// Legacy entries without an identity link use a content/tag lookup. The
|
|
15780
|
+
// visible index is not an Agent-queue index because hidden messages may
|
|
15781
|
+
// occupy executable slots ahead of the row.
|
|
15782
|
+
const executable = resolvedMode === "steer" ? this.agent.snapshotSteering() : this.agent.snapshotFollowUp();
|
|
15783
|
+
const legacyIndex = executable.findIndex(candidate => {
|
|
15784
|
+
if (entry?.tag !== undefined && candidate.role === "custom") {
|
|
15785
|
+
return readPendingDisplayTag(candidate.details) === entry.tag;
|
|
15786
|
+
}
|
|
15787
|
+
if (entry?.text === undefined) return false;
|
|
15788
|
+
if (candidate.role === "user") return this.#getUserMessageText(candidate) === entry.text;
|
|
15789
|
+
if (candidate.role === "custom") return this.#getCustomMessageTextContent(candidate) === entry.text;
|
|
15790
|
+
return false;
|
|
15791
|
+
});
|
|
15792
|
+
if (legacyIndex !== -1) {
|
|
15793
|
+
removedMessage =
|
|
15794
|
+
resolvedMode === "steer"
|
|
15795
|
+
? this.agent.removeSteerAt(legacyIndex)
|
|
15796
|
+
: this.agent.removeFollowUpAt(legacyIndex);
|
|
15797
|
+
}
|
|
15798
|
+
}
|
|
15799
|
+
// Removing a deferred SDK follow-up — or the live follow-up it was waiting
|
|
15800
|
+
// behind — must release the NEXT deferred submission, exactly like the
|
|
15801
|
+
// cancellation path does, or it stays accepted with no execution or
|
|
15802
|
+
// terminal settlement (exact-head review P1).
|
|
15803
|
+
if (removedMessage !== undefined) this.#releaseDeferredSdkFollowUps();
|
|
14358
15804
|
// The removed message left its queue WITHOUT consumption: its accepted SDK
|
|
14359
15805
|
// submission must terminalize boundedly, not strand accepted forever
|
|
14360
15806
|
// (#4668 review P1).
|
|
@@ -14705,11 +16151,13 @@ export class AgentSession {
|
|
|
14705
16151
|
| undefined;
|
|
14706
16152
|
|
|
14707
16153
|
if (this.#extensionRunner && savedCompactionEntry) {
|
|
14708
|
-
await this.#
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
16154
|
+
await this.#runCommittedSuccessorHook(() =>
|
|
16155
|
+
this.#extensionRunner!.emit({
|
|
16156
|
+
type: "session_compact",
|
|
16157
|
+
compactionEntry: savedCompactionEntry,
|
|
16158
|
+
fromExtension: fromExtension ?? false,
|
|
16159
|
+
}),
|
|
16160
|
+
);
|
|
14713
16161
|
}
|
|
14714
16162
|
|
|
14715
16163
|
return savedCompactionEntry;
|
|
@@ -14797,6 +16245,7 @@ export class AgentSession {
|
|
|
14797
16245
|
this.#promptPreflightCancellationGeneration++;
|
|
14798
16246
|
this.#promptPreflightAbortController.abort();
|
|
14799
16247
|
this.#promptPreflightAbortController = new AbortController();
|
|
16248
|
+
this.#dropQueuedTtsrFollowUps();
|
|
14800
16249
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
14801
16250
|
if (options?.preserveCompaction) this.#autoCompactionAbortController?.abort();
|
|
14802
16251
|
else this.abortCompaction();
|
|
@@ -14872,6 +16321,7 @@ export class AgentSession {
|
|
|
14872
16321
|
this.#promptPreflightCancellationGeneration++;
|
|
14873
16322
|
this.#promptPreflightAbortController.abort();
|
|
14874
16323
|
this.#promptPreflightAbortController = new AbortController();
|
|
16324
|
+
this.#dropQueuedTtsrFollowUps();
|
|
14875
16325
|
this.#drainTerminalOwnedYieldEntries();
|
|
14876
16326
|
if (options?.preserveCompaction !== true) this.abortCompaction();
|
|
14877
16327
|
const overlappingPostPromptDrain = this.#cancelPostPromptTasks();
|
|
@@ -15019,6 +16469,7 @@ export class AgentSession {
|
|
|
15019
16469
|
this.#promptPreflightCancellationGeneration++;
|
|
15020
16470
|
this.#promptPreflightAbortController.abort();
|
|
15021
16471
|
this.#promptPreflightAbortController = new AbortController();
|
|
16472
|
+
this.#dropQueuedTtsrFollowUps();
|
|
15022
16473
|
}
|
|
15023
16474
|
/**
|
|
15024
16475
|
* Capture the steering admission snapshot at abort ADMISSION: the host
|
|
@@ -15105,6 +16556,31 @@ export class AgentSession {
|
|
|
15105
16556
|
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
15106
16557
|
return AsyncJobManager.endpointIdOf(manager) ?? this.sessionManager.getSessionId() ?? "local";
|
|
15107
16558
|
}
|
|
16559
|
+
/**
|
|
16560
|
+
* Tool call ids the run resource ledger currently holds for `handle` (#5637).
|
|
16561
|
+
*
|
|
16562
|
+
* The authoritative answer to "is a tool running?": AgentLoop reserves the
|
|
16563
|
+
* `kind: "tool"` lease SYNCHRONOUSLY inside its dispatch loop, before the
|
|
16564
|
+
* tool's `execute` is invoked, and the lease settles when the call really
|
|
16565
|
+
* ends — whereas `tool_execution_start` only reaches an extension after an
|
|
16566
|
+
* asynchronous fanout. Strictly read-only: it takes a snapshot and never
|
|
16567
|
+
* claims, reserves, seals or quarantines. An unknown run reads as empty,
|
|
16568
|
+
* matching `RunResourceLedger.pending`.
|
|
16569
|
+
*/
|
|
16570
|
+
pendingToolExecutions(handle: string): readonly string[] {
|
|
16571
|
+
const ids = new Set<string>();
|
|
16572
|
+
for (const entry of this.agent.resourceLedger.pending(handle)) {
|
|
16573
|
+
if (entry.kind !== "tool") continue;
|
|
16574
|
+
// AgentLoop labels a tool reservation `<toolName>:<toolCallId>`, and
|
|
16575
|
+
// registers the reservation and its tracked task under the SAME label, so
|
|
16576
|
+
// the set also collapses that pair to one id. A label without a separator
|
|
16577
|
+
// is passed through rather than dropped: over-reporting a running tool
|
|
16578
|
+
// only costs a bounded wait, under-reporting kills it mid-write.
|
|
16579
|
+
const separator = entry.label.indexOf(":");
|
|
16580
|
+
ids.add(separator < 0 ? entry.label : entry.label.slice(separator + 1));
|
|
16581
|
+
}
|
|
16582
|
+
return [...ids];
|
|
16583
|
+
}
|
|
15108
16584
|
async abortPromptAndWait(
|
|
15109
16585
|
handle: string,
|
|
15110
16586
|
options: {
|
|
@@ -15237,16 +16713,21 @@ export class AgentSession {
|
|
|
15237
16713
|
// Purged ordinary follow-ups leave without consumption: terminalize
|
|
15238
16714
|
// their accepted SDK submissions boundedly (#4668 review P1).
|
|
15239
16715
|
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
|
-
|
|
16716
|
+
// Mirror the follow-up purge in the display list by bound message identity,
|
|
16717
|
+
// not by Agent-array position. Deferred SDK follow-ups and tag-only custom
|
|
16718
|
+
// entries are not represented one-for-one in followUpBeforePurge.
|
|
16719
|
+
const keptFollowUps = new Set([...this.agent.snapshotFollowUp(), ...this.#deferredSdkFollowUps]);
|
|
16720
|
+
const keptCustomTags = new Set(
|
|
16721
|
+
[...keptFollowUps].flatMap(message => {
|
|
16722
|
+
if (message.role !== "custom") return [];
|
|
16723
|
+
const tag = readPendingDisplayTag(message.details);
|
|
16724
|
+
return tag === undefined ? [] : [tag];
|
|
16725
|
+
}),
|
|
16726
|
+
);
|
|
16727
|
+
this.#followUpMessages = this.#followUpMessages.filter(entry => {
|
|
16728
|
+
if (entry.message !== undefined) return keptFollowUps.has(entry.message);
|
|
16729
|
+
return entry.tag !== undefined && keptCustomTags.has(entry.tag);
|
|
16730
|
+
});
|
|
15250
16731
|
}
|
|
15251
16732
|
// Do NOT advance the attempt epoch here: the terminal scope must stay
|
|
15252
16733
|
// keyed to the aborted epoch so #isTurnContinuationBlocked (which
|
|
@@ -15260,6 +16741,7 @@ export class AgentSession {
|
|
|
15260
16741
|
this.#promptPreflightCancellationGeneration++;
|
|
15261
16742
|
this.#promptPreflightAbortController.abort();
|
|
15262
16743
|
this.#promptPreflightAbortController = new AbortController();
|
|
16744
|
+
this.#dropQueuedTtsrFollowUps();
|
|
15263
16745
|
}
|
|
15264
16746
|
const aborted = this.#runCancellationDomains.abort(handle);
|
|
15265
16747
|
if (!aborted.ok) {
|
|
@@ -15376,6 +16858,7 @@ export class AgentSession {
|
|
|
15376
16858
|
const queueSnapshot = this.agent.snapshotQueues();
|
|
15377
16859
|
const steeringDisplaySnapshot = [...this.#steeringMessages];
|
|
15378
16860
|
const followUpDisplaySnapshot = [...this.#followUpMessages];
|
|
16861
|
+
const deferredFollowUpSnapshot = [...this.#deferredSdkFollowUps];
|
|
15379
16862
|
const pendingNextTurnSnapshot = [...this.#pendingNextTurnMessages];
|
|
15380
16863
|
const additionsSince = <T>(current: readonly T[], baseline: readonly T[]): T[] => {
|
|
15381
16864
|
const remaining = new Map<T, number>();
|
|
@@ -15387,7 +16870,7 @@ export class AgentSession {
|
|
|
15387
16870
|
return false;
|
|
15388
16871
|
});
|
|
15389
16872
|
};
|
|
15390
|
-
|
|
16873
|
+
let selected = (() => {
|
|
15391
16874
|
if (options?.queuedEntryId === undefined) return undefined;
|
|
15392
16875
|
const [mode, sequenceText] = options.queuedEntryId.split(":");
|
|
15393
16876
|
const sequence = Number(sequenceText);
|
|
@@ -15395,21 +16878,63 @@ export class AgentSession {
|
|
|
15395
16878
|
const displays = mode === "steer" ? steeringDisplaySnapshot : followUpDisplaySnapshot;
|
|
15396
16879
|
const index = displays.findIndex(entry => entry.sequence === sequence);
|
|
15397
16880
|
if (index === -1) return undefined;
|
|
16881
|
+
const display = displays[index]!;
|
|
16882
|
+
const queue = mode === "steer" ? queueSnapshot.steering : queueSnapshot.followUp;
|
|
16883
|
+
const message =
|
|
16884
|
+
display.message ??
|
|
16885
|
+
queue.find(candidate => {
|
|
16886
|
+
if (candidate.role !== "custom" || display.tag === undefined) return false;
|
|
16887
|
+
return readPendingDisplayTag(candidate.details) === display.tag;
|
|
16888
|
+
});
|
|
16889
|
+
const deferred =
|
|
16890
|
+
mode === "followUp" && message !== undefined && deferredFollowUpSnapshot.includes(message);
|
|
15398
16891
|
return {
|
|
15399
|
-
display
|
|
15400
|
-
message
|
|
16892
|
+
display,
|
|
16893
|
+
message,
|
|
15401
16894
|
mode,
|
|
15402
|
-
|
|
16895
|
+
deferred,
|
|
15403
16896
|
};
|
|
15404
16897
|
})();
|
|
16898
|
+
let cancelledSelection: typeof selected;
|
|
16899
|
+
const selectedPromotionHook = selected?.message
|
|
16900
|
+
? (this.#followUpPromotionHooks.get(selected.message) ?? this.#steerPromotionHooks.get(selected.message))
|
|
16901
|
+
: undefined;
|
|
16902
|
+
if (selected?.deferred && selected.message !== undefined) {
|
|
16903
|
+
const selectedDeferredIndex = this.#deferredSdkFollowUps.indexOf(selected.message);
|
|
16904
|
+
if (selectedDeferredIndex !== -1) {
|
|
16905
|
+
this.#deferredSdkFollowUps.splice(selectedDeferredIndex, 1);
|
|
16906
|
+
}
|
|
16907
|
+
}
|
|
16908
|
+
const revalidateSelected = (): void => {
|
|
16909
|
+
if (selected === undefined || selected.message === undefined) return;
|
|
16910
|
+
const stillDisplayed =
|
|
16911
|
+
this.#steeringMessages.includes(selected.display) ||
|
|
16912
|
+
this.#followUpMessages.includes(selected.display);
|
|
16913
|
+
// An abort temporarily removes steering from Agent's live queue and
|
|
16914
|
+
// returns it in the agent_end disowned batch; the display mirror is the
|
|
16915
|
+
// stable ownership marker across that boundary. A successful external
|
|
16916
|
+
// cancellation removes the display entry as part of the same operation.
|
|
16917
|
+
if (!stillDisplayed) {
|
|
16918
|
+
cancelledSelection = selected;
|
|
16919
|
+
selected = undefined;
|
|
16920
|
+
}
|
|
16921
|
+
};
|
|
15405
16922
|
let runAccepted = false;
|
|
15406
16923
|
const restore = () => {
|
|
15407
16924
|
const queues = this.agent.snapshotQueues();
|
|
15408
16925
|
const queueBaseline = [...queueSnapshot.steering, ...queueSnapshot.followUp];
|
|
15409
16926
|
const displayBaseline = [...steeringDisplaySnapshot, ...followUpDisplaySnapshot];
|
|
16927
|
+
const cancelledMessage = cancelledSelection?.message;
|
|
16928
|
+
const cancelledDisplay = cancelledSelection?.display;
|
|
15410
16929
|
this.agent.restoreQueues({
|
|
15411
|
-
steering: [
|
|
15412
|
-
|
|
16930
|
+
steering: [
|
|
16931
|
+
...queueSnapshot.steering.filter(message => message !== cancelledMessage),
|
|
16932
|
+
...additionsSince(queues.steering, queueBaseline).filter(message => message !== cancelledMessage),
|
|
16933
|
+
],
|
|
16934
|
+
followUp: [
|
|
16935
|
+
...queueSnapshot.followUp.filter(message => message !== cancelledMessage),
|
|
16936
|
+
...additionsSince(queues.followUp, queueBaseline).filter(message => message !== cancelledMessage),
|
|
16937
|
+
],
|
|
15413
16938
|
});
|
|
15414
16939
|
this.#pendingNextTurnMessages = [
|
|
15415
16940
|
...pendingNextTurnSnapshot,
|
|
@@ -15418,13 +16943,23 @@ export class AgentSession {
|
|
|
15418
16943
|
this.#cancelAndSubmitPendingNextTurnDrained ? [] : pendingNextTurnSnapshot,
|
|
15419
16944
|
),
|
|
15420
16945
|
];
|
|
16946
|
+
this.#deferredSdkFollowUps = [
|
|
16947
|
+
...deferredFollowUpSnapshot.filter(message => message !== cancelledMessage),
|
|
16948
|
+
...additionsSince(this.#deferredSdkFollowUps, deferredFollowUpSnapshot).filter(
|
|
16949
|
+
message => message !== cancelledMessage,
|
|
16950
|
+
),
|
|
16951
|
+
];
|
|
15421
16952
|
this.#steeringMessages = [
|
|
15422
|
-
...steeringDisplaySnapshot,
|
|
15423
|
-
...additionsSince(this.#steeringMessages, displayBaseline)
|
|
16953
|
+
...steeringDisplaySnapshot.filter(entry => entry !== cancelledDisplay),
|
|
16954
|
+
...additionsSince(this.#steeringMessages, displayBaseline).filter(
|
|
16955
|
+
entry => entry !== cancelledDisplay,
|
|
16956
|
+
),
|
|
15424
16957
|
];
|
|
15425
16958
|
this.#followUpMessages = [
|
|
15426
|
-
...followUpDisplaySnapshot,
|
|
15427
|
-
...additionsSince(this.#followUpMessages, displayBaseline)
|
|
16959
|
+
...followUpDisplaySnapshot.filter(entry => entry !== cancelledDisplay),
|
|
16960
|
+
...additionsSince(this.#followUpMessages, displayBaseline).filter(
|
|
16961
|
+
entry => entry !== cancelledDisplay,
|
|
16962
|
+
),
|
|
15428
16963
|
];
|
|
15429
16964
|
};
|
|
15430
16965
|
try {
|
|
@@ -15436,6 +16971,7 @@ export class AgentSession {
|
|
|
15436
16971
|
? await this.#cancelAndSubmitAbortOutcomeProviderForTests()
|
|
15437
16972
|
: await this.#abortWithOutcome({ cause: "user_interrupt", timeoutMs: 5_000 });
|
|
15438
16973
|
this.#disownedSteeringDisposition = undefined;
|
|
16974
|
+
revalidateSelected();
|
|
15439
16975
|
if (outcome.kind !== "settled") {
|
|
15440
16976
|
restore();
|
|
15441
16977
|
if (outcome.kind === "error") {
|
|
@@ -15456,22 +16992,21 @@ export class AgentSession {
|
|
|
15456
16992
|
};
|
|
15457
16993
|
const steeringDisplaysDuringWindow = additionsSince(this.#steeringMessages, steeringDisplaySnapshot);
|
|
15458
16994
|
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
|
-
]
|
|
16995
|
+
const committedSelection = selected;
|
|
16996
|
+
const selectedMessage = committedSelection?.message;
|
|
16997
|
+
const reclassifiedSteering = committedSelection
|
|
16998
|
+
? queueSnapshot.steering.filter(message => message !== selectedMessage)
|
|
16999
|
+
: [];
|
|
17000
|
+
const heldFollowUp = committedSelection
|
|
17001
|
+
? [...reclassifiedSteering, ...queueSnapshot.followUp.filter(message => message !== selectedMessage)]
|
|
15469
17002
|
: [];
|
|
15470
17003
|
let heldQueueRestored = false;
|
|
15471
17004
|
const restoreHeldQueue = () => {
|
|
15472
|
-
if (!
|
|
17005
|
+
if (!committedSelection || heldQueueRestored) return;
|
|
15473
17006
|
heldQueueRestored = true;
|
|
15474
17007
|
const current = this.agent.snapshotQueues();
|
|
17008
|
+
this.#markSteeringAsFollowUpPolicy(reclassifiedSteering);
|
|
17009
|
+
this.#markTrackedFollowUpSequential(heldFollowUp);
|
|
15475
17010
|
this.agent.restoreQueues({
|
|
15476
17011
|
steering: current.steering,
|
|
15477
17012
|
followUp: [...heldFollowUp, ...current.followUp],
|
|
@@ -15481,25 +17016,49 @@ export class AgentSession {
|
|
|
15481
17016
|
// restored once the new run is accepted (below), so the Agent
|
|
15482
17017
|
// admits them into a live run instead of re-labelling them as
|
|
15483
17018
|
// follow-ups drained turn after turn. Follow-ups keep their queue.
|
|
15484
|
-
const
|
|
15485
|
-
const
|
|
17019
|
+
const cancelledMessage = cancelledSelection?.message;
|
|
17020
|
+
const cancelledDisplay = cancelledSelection?.display;
|
|
17021
|
+
const heldSteering = committedSelection
|
|
17022
|
+
? []
|
|
17023
|
+
: queueSnapshot.steering.filter(message => message !== cancelledMessage);
|
|
17024
|
+
const heldSteeringDisplays = committedSelection
|
|
17025
|
+
? []
|
|
17026
|
+
: steeringDisplaySnapshot.filter(entry => entry !== cancelledDisplay);
|
|
15486
17027
|
this.agent.restoreQueues({
|
|
15487
|
-
steering:
|
|
15488
|
-
|
|
15489
|
-
|
|
15490
|
-
|
|
17028
|
+
steering: queuedDuringWindow.steering.filter(
|
|
17029
|
+
message => message !== selectedMessage && message !== cancelledMessage,
|
|
17030
|
+
),
|
|
17031
|
+
followUp: committedSelection
|
|
17032
|
+
? queuedDuringWindow.followUp.filter(message => message !== selectedMessage)
|
|
17033
|
+
: [
|
|
17034
|
+
...queueSnapshot.followUp.filter(message => message !== cancelledMessage),
|
|
17035
|
+
...queuedDuringWindow.followUp.filter(message => message !== cancelledMessage),
|
|
17036
|
+
],
|
|
15491
17037
|
});
|
|
15492
17038
|
this.#steeringMessages = steeringDisplaysDuringWindow;
|
|
15493
17039
|
// With a selected entry the held (unselected) messages are re-queued
|
|
15494
17040
|
// as follow-ups by restoreHeldQueue once the new run is accepted;
|
|
15495
17041
|
// mirror them in the display in the same order.
|
|
15496
|
-
this.#followUpMessages =
|
|
17042
|
+
this.#followUpMessages = committedSelection
|
|
15497
17043
|
? [
|
|
15498
|
-
...steeringDisplaySnapshot.filter(entry => entry !==
|
|
15499
|
-
...followUpDisplaySnapshot.filter(entry => entry !==
|
|
17044
|
+
...steeringDisplaySnapshot.filter(entry => entry !== committedSelection.display),
|
|
17045
|
+
...followUpDisplaySnapshot.filter(entry => entry !== committedSelection.display),
|
|
15500
17046
|
...followUpDisplaysDuringWindow,
|
|
15501
17047
|
]
|
|
15502
|
-
: [
|
|
17048
|
+
: [
|
|
17049
|
+
...followUpDisplaySnapshot.filter(entry => entry !== cancelledDisplay),
|
|
17050
|
+
...followUpDisplaysDuringWindow.filter(entry => entry !== cancelledDisplay),
|
|
17051
|
+
];
|
|
17052
|
+
if (committedSelection?.deferred && selectedMessage !== undefined) {
|
|
17053
|
+
this.#deferredSdkFollowUps = this.#deferredSdkFollowUps.filter(
|
|
17054
|
+
message => message !== selectedMessage,
|
|
17055
|
+
);
|
|
17056
|
+
}
|
|
17057
|
+
if (selectedMessage !== undefined && selectedPromotionHook !== undefined) {
|
|
17058
|
+
if (committedSelection?.mode === "steer")
|
|
17059
|
+
this.#steerPromotionHooks.set(selectedMessage, selectedPromotionHook);
|
|
17060
|
+
else this.#followUpPromotionHooks.set(selectedMessage, selectedPromotionHook);
|
|
17061
|
+
}
|
|
15503
17062
|
const message = selectedMessage ?? {
|
|
15504
17063
|
role: "user" as const,
|
|
15505
17064
|
content: [{ type: "text" as const, text }],
|
|
@@ -15517,7 +17076,7 @@ export class AgentSession {
|
|
|
15517
17076
|
: text;
|
|
15518
17077
|
await this.refreshGjcSubskillTools();
|
|
15519
17078
|
if (message.role === "custom") await this.#syncSkillPromptActiveStateSafely(message, true);
|
|
15520
|
-
if (
|
|
17079
|
+
if (committedSelection) {
|
|
15521
17080
|
const displayTag = message.role === "custom" ? readPendingDisplayTag(message.details) : undefined;
|
|
15522
17081
|
if (displayTag) this.#displayDequeueAlreadyHandled = { role: "custom", tag: displayTag };
|
|
15523
17082
|
else if (message.role === "user")
|
|
@@ -15529,7 +17088,7 @@ export class AgentSession {
|
|
|
15529
17088
|
resetRetryReplaySafety: true,
|
|
15530
17089
|
sdkRunToken: selectedSdkRunToken,
|
|
15531
17090
|
skipInitialSteeringPoll: heldSteering.length > 0,
|
|
15532
|
-
onRunAccepted: () => {
|
|
17091
|
+
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
15533
17092
|
runAccepted = true;
|
|
15534
17093
|
if (heldSteering.length > 0) {
|
|
15535
17094
|
// Re-admit the aborted turn's steers into the replacement run the
|
|
@@ -15541,10 +17100,17 @@ export class AgentSession {
|
|
|
15541
17100
|
this.agent.restoreSteering(heldSteering);
|
|
15542
17101
|
this.#steeringMessages = [...heldSteeringDisplays, ...this.#steeringMessages];
|
|
15543
17102
|
}
|
|
15544
|
-
if (
|
|
15545
|
-
|
|
15546
|
-
this.#
|
|
15547
|
-
|
|
17103
|
+
if (committedSelection) {
|
|
17104
|
+
this.#fireQueuedPromotionHooks([message], { startsOwnRun: true });
|
|
17105
|
+
this.#settleTrackedOwnRunPromotions(handle);
|
|
17106
|
+
}
|
|
17107
|
+
if (committedSelection) {
|
|
17108
|
+
this.#steeringMessages = this.#steeringMessages.filter(
|
|
17109
|
+
entry => entry !== committedSelection.display,
|
|
17110
|
+
);
|
|
17111
|
+
this.#followUpMessages = this.#followUpMessages.filter(
|
|
17112
|
+
entry => entry !== committedSelection.display,
|
|
17113
|
+
);
|
|
15548
17114
|
}
|
|
15549
17115
|
},
|
|
15550
17116
|
});
|
|
@@ -15559,6 +17125,7 @@ export class AgentSession {
|
|
|
15559
17125
|
if (runAccepted) {
|
|
15560
17126
|
return { kind: "submitted" };
|
|
15561
17127
|
}
|
|
17128
|
+
revalidateSelected();
|
|
15562
17129
|
this.#displayDequeueAlreadyHandled = undefined;
|
|
15563
17130
|
restore();
|
|
15564
17131
|
logger.error("Cancel-and-submit prompt failed before run acceptance", { cause });
|
|
@@ -15659,17 +17226,23 @@ export class AgentSession {
|
|
|
15659
17226
|
const noLeasePreviousSessionIdentity = this.sessionManager.getSessionId();
|
|
15660
17227
|
const noLeasePreviousSessionFile = this.sessionManager.getSessionFile();
|
|
15661
17228
|
const prepared = await this.sessionManager.prepareNewSession(options);
|
|
17229
|
+
let exactRetirement: PreparedExactMcpRetirement | undefined;
|
|
15662
17230
|
try {
|
|
15663
17231
|
// Last fallible gate while public getters still show the predecessor (#3138).
|
|
15664
17232
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
15665
17233
|
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
17234
|
+
exactRetirement = await this.#prepareExactMcpControlRetirement();
|
|
17235
|
+
if (exactRetirement && !exactRetirement.canCommit())
|
|
17236
|
+
throw new Error("Exact MCP control retirement changed before session adoption");
|
|
15666
17237
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
17238
|
+
await exactRetirement?.commit();
|
|
15667
17239
|
// Endpoint identity committed to the successor: re-register the
|
|
15668
17240
|
// manager so post-transition lineage bindings resolve and owned
|
|
15669
17241
|
// aborts classify in the successor session (review thread P1).
|
|
15670
17242
|
this.#rekeyJobManagerForSessionIdentity(noLeasePreviousSessionIdentity, noLeasePreviousSessionFile);
|
|
15671
17243
|
await this.#runToolSessionTransitionCleanups();
|
|
15672
17244
|
} catch (error) {
|
|
17245
|
+
await exactRetirement?.abort();
|
|
15673
17246
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
15674
17247
|
}
|
|
15675
17248
|
this.setTodoPhases([]);
|
|
@@ -15735,17 +17308,23 @@ export class AgentSession {
|
|
|
15735
17308
|
throw new Error("Owned async jobs did not settle before session replacement.");
|
|
15736
17309
|
}
|
|
15737
17310
|
const prepared = await this.sessionManager.prepareNewSession(options);
|
|
17311
|
+
let exactRetirement: PreparedExactMcpRetirement | undefined;
|
|
15738
17312
|
try {
|
|
15739
17313
|
// Last fallible gate while public getters still show the predecessor (#3138).
|
|
15740
17314
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
15741
17315
|
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
17316
|
+
exactRetirement = await this.#prepareExactMcpControlRetirement();
|
|
17317
|
+
if (exactRetirement && !exactRetirement.canCommit())
|
|
17318
|
+
throw new Error("Exact MCP control retirement changed before session adoption");
|
|
15742
17319
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
17320
|
+
await exactRetirement?.commit();
|
|
15743
17321
|
// Endpoint identity committed to the successor: re-register the
|
|
15744
17322
|
// manager so post-transition lineage bindings resolve and owned
|
|
15745
17323
|
// aborts classify in the successor session (review thread P1).
|
|
15746
17324
|
this.#rekeyJobManagerForSessionIdentity(previousSessionIdentity, previousSessionIdentityFile);
|
|
15747
17325
|
await this.#runToolSessionTransitionCleanups();
|
|
15748
17326
|
} catch (error) {
|
|
17327
|
+
await exactRetirement?.abort();
|
|
15749
17328
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
15750
17329
|
}
|
|
15751
17330
|
this.#disconnectFromAgent();
|
|
@@ -15851,11 +17430,13 @@ export class AgentSession {
|
|
|
15851
17430
|
this.#reconnectToAgent();
|
|
15852
17431
|
this.#resetIrcRosterDeliveryState();
|
|
15853
17432
|
if (this.#extensionRunner) {
|
|
15854
|
-
await this.#
|
|
15855
|
-
|
|
15856
|
-
|
|
15857
|
-
|
|
15858
|
-
|
|
17433
|
+
await this.#runCommittedSuccessorHook(() =>
|
|
17434
|
+
this.#extensionRunner!.emit({
|
|
17435
|
+
type: "session_switch",
|
|
17436
|
+
reason: "new",
|
|
17437
|
+
previousSessionFile,
|
|
17438
|
+
}),
|
|
17439
|
+
);
|
|
15859
17440
|
} else {
|
|
15860
17441
|
}
|
|
15861
17442
|
}
|
|
@@ -15923,6 +17504,17 @@ export class AgentSession {
|
|
|
15923
17504
|
const previousSessionFile = this.sessionFile;
|
|
15924
17505
|
const previousWorkflowGateSessionId = this.sessionId;
|
|
15925
17506
|
const previousSessionIdentity = this.sessionManager.getSessionId();
|
|
17507
|
+
const predecessorQueuedSdkWork = this.#queuedMessagesForSessionTransition();
|
|
17508
|
+
const settleForkPredecessorWork = (): void => {
|
|
17509
|
+
this.#terminalizeQueuedSdkWorkForSessionTransition(predecessorQueuedSdkWork);
|
|
17510
|
+
this.#deferredSdkFollowUps = [];
|
|
17511
|
+
this.#pendingNextTurnMessages = [];
|
|
17512
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
17513
|
+
this.agent.clearAllQueues();
|
|
17514
|
+
this.#steeringMessages = [];
|
|
17515
|
+
this.#followUpMessages = [];
|
|
17516
|
+
this.#resetActiveSdkRunOwnership();
|
|
17517
|
+
};
|
|
15926
17518
|
|
|
15927
17519
|
// Emit session_before_switch event with reason "fork" (can be cancelled)
|
|
15928
17520
|
if (this.#extensionRunner?.hasHandlers("session_before_switch")) {
|
|
@@ -15936,6 +17528,10 @@ export class AgentSession {
|
|
|
15936
17528
|
}
|
|
15937
17529
|
}
|
|
15938
17530
|
|
|
17531
|
+
this.#settleTrackedQueuedInputsBeforeAgentDisconnect();
|
|
17532
|
+
this.#disconnectFromAgent();
|
|
17533
|
+
await this.#settleActivePromptForHistoryTransition();
|
|
17534
|
+
|
|
15939
17535
|
// Flush current session to ensure all entries are written
|
|
15940
17536
|
await this.sessionManager.flush();
|
|
15941
17537
|
|
|
@@ -15951,6 +17547,8 @@ export class AgentSession {
|
|
|
15951
17547
|
"copy-retain",
|
|
15952
17548
|
this.settings.get("sessionMemory.mode"),
|
|
15953
17549
|
);
|
|
17550
|
+
let exactRetirement: PreparedExactMcpRetirement | undefined;
|
|
17551
|
+
let adopted = false;
|
|
15954
17552
|
try {
|
|
15955
17553
|
await initializeLocalRoot({
|
|
15956
17554
|
getArtifactsDir: () => forkedManager.getArtifactsDir(),
|
|
@@ -15960,7 +17558,15 @@ export class AgentSession {
|
|
|
15960
17558
|
});
|
|
15961
17559
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
15962
17560
|
this.#assertJobManagerEndpointAdmission(forkedManager.getSessionId(), forkedManager.getSessionFile());
|
|
17561
|
+
exactRetirement = await this.#prepareExactMcpControlRetirement();
|
|
17562
|
+
if (exactRetirement && !exactRetirement.canCommit())
|
|
17563
|
+
throw new Error("Exact MCP control retirement changed before session adoption");
|
|
17564
|
+
this.sessionManager = forkedManager;
|
|
17565
|
+
adopted = true;
|
|
17566
|
+
await exactRetirement?.commit();
|
|
15963
17567
|
} catch (error) {
|
|
17568
|
+
await exactRetirement?.abort();
|
|
17569
|
+
if (adopted) this.sessionManager = previousManager;
|
|
15964
17570
|
const forkedFile = forkedManager.getSessionFile();
|
|
15965
17571
|
const cleanupErrors: unknown[] = [];
|
|
15966
17572
|
try {
|
|
@@ -15989,8 +17595,8 @@ export class AgentSession {
|
|
|
15989
17595
|
}
|
|
15990
17596
|
throw error;
|
|
15991
17597
|
}
|
|
15992
|
-
this.sessionManager = forkedManager;
|
|
15993
17598
|
this.#rekeyJobManagerForSessionIdentity(previousSessionIdentity, previousSessionFile);
|
|
17599
|
+
settleForkPredecessorWork();
|
|
15994
17600
|
try {
|
|
15995
17601
|
await previousManager.close();
|
|
15996
17602
|
} catch (error) {
|
|
@@ -16004,16 +17610,23 @@ export class AgentSession {
|
|
|
16004
17610
|
// public manager getters remain bound to the predecessor.
|
|
16005
17611
|
const prepared = await this.sessionManager.prepareFork();
|
|
16006
17612
|
if (!prepared) return false;
|
|
17613
|
+
let exactRetirement: PreparedExactMcpRetirement | undefined;
|
|
16007
17614
|
try {
|
|
16008
17615
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
16009
17616
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
16010
17617
|
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
17618
|
+
exactRetirement = await this.#prepareExactMcpControlRetirement();
|
|
17619
|
+
if (exactRetirement && !exactRetirement.canCommit())
|
|
17620
|
+
throw new Error("Exact MCP control retirement changed before session adoption");
|
|
16011
17621
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
17622
|
+
await exactRetirement?.commit();
|
|
16012
17623
|
// Fork commits a successor endpoint identity; re-register the
|
|
16013
17624
|
// manager under it (review thread P1).
|
|
16014
17625
|
this.#rekeyJobManagerForSessionIdentity(previousSessionIdentity, previousSessionFile);
|
|
17626
|
+
settleForkPredecessorWork();
|
|
16015
17627
|
await this.#runToolSessionTransitionCleanups();
|
|
16016
17628
|
} catch (error) {
|
|
17629
|
+
await exactRetirement?.abort();
|
|
16017
17630
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
16018
17631
|
}
|
|
16019
17632
|
}
|
|
@@ -16025,15 +17638,18 @@ export class AgentSession {
|
|
|
16025
17638
|
|
|
16026
17639
|
// Emit session_switch event with reason "fork" to hooks
|
|
16027
17640
|
if (this.#extensionRunner) {
|
|
16028
|
-
await this.#
|
|
16029
|
-
|
|
16030
|
-
|
|
16031
|
-
|
|
16032
|
-
|
|
17641
|
+
await this.#runCommittedSuccessorHook(() =>
|
|
17642
|
+
this.#extensionRunner!.emit({
|
|
17643
|
+
type: "session_switch",
|
|
17644
|
+
reason: "fork",
|
|
17645
|
+
previousSessionFile,
|
|
17646
|
+
}),
|
|
17647
|
+
);
|
|
16033
17648
|
}
|
|
16034
17649
|
|
|
16035
17650
|
return true;
|
|
16036
17651
|
} finally {
|
|
17652
|
+
this.#reconnectToAgent();
|
|
16037
17653
|
this.#endSessionTransition();
|
|
16038
17654
|
}
|
|
16039
17655
|
}
|
|
@@ -16142,15 +17758,42 @@ export class AgentSession {
|
|
|
16142
17758
|
await this.refreshBaseSystemPrompt();
|
|
16143
17759
|
}
|
|
16144
17760
|
|
|
16145
|
-
/** Resolver intent only for assignments owned by
|
|
17761
|
+
/** Resolver intent only for default assignments owned by an active or runtime-recovered durable profile. */
|
|
16146
17762
|
#persistedModelProfileAliasIntent(role: string): { aliasIntent: "preset-equivalent" } | undefined {
|
|
16147
|
-
|
|
16148
|
-
const
|
|
17763
|
+
const runtimeDefaultIdentity = this.#defaultFallbackController?.chain;
|
|
17764
|
+
const profileName =
|
|
17765
|
+
this.#activeModelProfile ??
|
|
17766
|
+
(role === "default" &&
|
|
17767
|
+
runtimeDefaultIdentity?.origin === "runtime" &&
|
|
17768
|
+
runtimeDefaultIdentity.identity === this.settings.get("modelProfile.default")
|
|
17769
|
+
? runtimeDefaultIdentity.identity
|
|
17770
|
+
: undefined);
|
|
17771
|
+
if (!profileName) return undefined;
|
|
17772
|
+
const profile = this.#modelRegistry.getModelProfile?.(profileName);
|
|
16149
17773
|
if (!profile) return undefined;
|
|
16150
17774
|
const bindings = resolveProfileBindings(profile);
|
|
17775
|
+
const configuredDefault = role === "default" ? this.getConfiguredModelChain("default")?.[0] : undefined;
|
|
17776
|
+
const profileDefault = Array.isArray(bindings.defaultSelector)
|
|
17777
|
+
? bindings.defaultSelector[0]
|
|
17778
|
+
: bindings.defaultSelector;
|
|
17779
|
+
const configuredDefaultIdentity = configuredDefault
|
|
17780
|
+
? splitSelectorThinkingSuffix(configuredDefault).selector.trim().toLowerCase()
|
|
17781
|
+
: undefined;
|
|
17782
|
+
const profileDefaultIdentity = profileDefault
|
|
17783
|
+
? splitSelectorThinkingSuffix(profileDefault).selector.trim().toLowerCase()
|
|
17784
|
+
: undefined;
|
|
17785
|
+
const configuredDefaultModel = configuredDefaultIdentity
|
|
17786
|
+
? parseModelString(configuredDefaultIdentity)
|
|
17787
|
+
: undefined;
|
|
17788
|
+
const profileDefaultModel = profileDefaultIdentity ? parseModelString(profileDefaultIdentity) : undefined;
|
|
17789
|
+
const ownsConfiguredDefault =
|
|
17790
|
+
configuredDefaultModel && profileDefaultModel
|
|
17791
|
+
? configuredDefaultModel.provider === profileDefaultModel.provider &&
|
|
17792
|
+
configuredDefaultModel.id === profileDefaultModel.id
|
|
17793
|
+
: profileDefaultIdentity !== undefined && configuredDefaultIdentity === profileDefaultIdentity;
|
|
16151
17794
|
const owned =
|
|
16152
17795
|
role === "default"
|
|
16153
|
-
?
|
|
17796
|
+
? runtimeDefaultIdentity?.origin === "runtime" || ownsConfiguredDefault
|
|
16154
17797
|
: Object.hasOwn(bindings.modelRoles, role) || Object.hasOwn(bindings.agentModelOverrides, role);
|
|
16155
17798
|
return owned ? { aliasIntent: "preset-equivalent" } : undefined;
|
|
16156
17799
|
}
|
|
@@ -16172,9 +17815,10 @@ export class AgentSession {
|
|
|
16172
17815
|
* configured `modelBindings` (also installed into these two override slots
|
|
16173
17816
|
* once at startup) are not profile-owned and must survive the transition.
|
|
16174
17817
|
*/
|
|
16175
|
-
#resetSessionScopedModelProfileState(): void {
|
|
17818
|
+
#resetSessionScopedModelProfileState(options?: { preserveDefaultConfiguredChain?: boolean; force?: boolean }): void {
|
|
16176
17819
|
const persistedProfile = this.settings.get("modelProfile.default");
|
|
16177
|
-
if (persistedProfile !== undefined && persistedProfile === this.getActiveModelProfile())
|
|
17820
|
+
if (!options?.force && persistedProfile !== undefined && persistedProfile === this.getActiveModelProfile())
|
|
17821
|
+
return;
|
|
16178
17822
|
const hadInstalledKeys =
|
|
16179
17823
|
this.#activeProfileInstalledRoles.size > 0 || this.#activeProfileInstalledAgentOverrides.size > 0;
|
|
16180
17824
|
if (hadInstalledKeys) {
|
|
@@ -16197,7 +17841,7 @@ export class AgentSession {
|
|
|
16197
17841
|
this.#modelRegistry.reapplyConfiguredModelBindings(this.settings);
|
|
16198
17842
|
}
|
|
16199
17843
|
const defaultChain = getSessionContextForInternalRead(this.sessionManager).configuredModelChains.default;
|
|
16200
|
-
if (defaultChain && defaultChain.identity !== undefined) {
|
|
17844
|
+
if (!options?.preserveDefaultConfiguredChain && defaultChain && defaultChain.identity !== undefined) {
|
|
16201
17845
|
this.setConfiguredModelChain("default", [], "user-selection");
|
|
16202
17846
|
}
|
|
16203
17847
|
this.setActiveModelProfile(undefined);
|
|
@@ -16439,11 +18083,36 @@ export class AgentSession {
|
|
|
16439
18083
|
* must not mutate the persisted configured intent.
|
|
16440
18084
|
*/
|
|
16441
18085
|
setDefaultFallbackRuntimeModel(selector: string): void {
|
|
18086
|
+
this.installRecoveredDefaultFallbackChain([selector], undefined, 0, []);
|
|
18087
|
+
}
|
|
18088
|
+
|
|
18089
|
+
/** Install a runtime-only durable fallback without changing saved session intent. */
|
|
18090
|
+
installRecoveredDefaultFallbackChain(
|
|
18091
|
+
entries: readonly string[],
|
|
18092
|
+
identity: string | undefined,
|
|
18093
|
+
activeIndex: number,
|
|
18094
|
+
skips: Array<{ selector: string; reason: string }>,
|
|
18095
|
+
options?: { emitResolutionEvent?: boolean },
|
|
18096
|
+
): void {
|
|
16442
18097
|
this.#defaultFallbackController = new FallbackChainController(
|
|
16443
|
-
{ role: "default", entries: [
|
|
18098
|
+
{ role: "default", entries: [...entries], origin: "runtime", identity, explicitHead: true },
|
|
16444
18099
|
this.settings.get("fallback.maxAttempts"),
|
|
16445
18100
|
);
|
|
16446
18101
|
this.#defaultFallbackExhaustedLastTurn = false;
|
|
18102
|
+
this.#seedDefaultFallbackResolutionForController(
|
|
18103
|
+
this.#defaultFallbackController,
|
|
18104
|
+
activeIndex,
|
|
18105
|
+
skips,
|
|
18106
|
+
options?.emitResolutionEvent ?? true,
|
|
18107
|
+
);
|
|
18108
|
+
}
|
|
18109
|
+
|
|
18110
|
+
markStartupRecoveryBindingsRequired(): void {
|
|
18111
|
+
this.#startupRecoveryBindingsRequired = true;
|
|
18112
|
+
}
|
|
18113
|
+
|
|
18114
|
+
hasRecoveredDefaultFallbackChain(): boolean {
|
|
18115
|
+
return this.#startupRecoveryBindingsRequired || this.#defaultFallbackController?.chain.origin === "runtime";
|
|
16447
18116
|
}
|
|
16448
18117
|
|
|
16449
18118
|
/**
|
|
@@ -16451,9 +18120,18 @@ export class AgentSession {
|
|
|
16451
18120
|
* The configured chain's role, origin, and identity are retained by the controller.
|
|
16452
18121
|
*/
|
|
16453
18122
|
seedDefaultFallbackResolution(activeIndex: number, skips: Array<{ selector: string; reason: string }>): void {
|
|
16454
|
-
|
|
18123
|
+
this.#seedDefaultFallbackResolutionForController(this.#defaultFallbackChain(), activeIndex, skips);
|
|
18124
|
+
}
|
|
18125
|
+
|
|
18126
|
+
/** Seed an already-selected runtime controller without legacy-chain materialization. */
|
|
18127
|
+
#seedDefaultFallbackResolutionForController(
|
|
18128
|
+
controller: FallbackChainController,
|
|
18129
|
+
activeIndex: number,
|
|
18130
|
+
skips: Array<{ selector: string; reason: string }>,
|
|
18131
|
+
emitResolutionEvent = true,
|
|
18132
|
+
): void {
|
|
16455
18133
|
controller.seedResolution(activeIndex, skips);
|
|
16456
|
-
this.#emitResolutionFallbackSwitch(controller);
|
|
18134
|
+
if (emitResolutionEvent) this.#emitResolutionFallbackSwitch(controller);
|
|
16457
18135
|
}
|
|
16458
18136
|
|
|
16459
18137
|
getDefaultFallbackRuntimeState(): DefaultFallbackRuntimeState {
|
|
@@ -17942,6 +19620,14 @@ export class AgentSession {
|
|
|
17942
19620
|
*/
|
|
17943
19621
|
async compact(customInstructions?: string, options?: CompactOptions): Promise<CompactionResult> {
|
|
17944
19622
|
this.#assertSessionAdmissionOpen();
|
|
19623
|
+
// Automatic context maintenance owns the transition lease while its
|
|
19624
|
+
// post-append hooks run. A manual request must join behind that owner so
|
|
19625
|
+
// it can cancel/wait for the automatic work rather than racing its lease.
|
|
19626
|
+
if (this.#sessionTransitionKind === "auto-compaction") {
|
|
19627
|
+
this.abortCompaction();
|
|
19628
|
+
await this.#waitForAutoCompactionCompletions();
|
|
19629
|
+
await this.#cancelPostPromptTasks();
|
|
19630
|
+
}
|
|
17945
19631
|
// Serialize with every other session-identity transition via the shared lease
|
|
17946
19632
|
// (bidirectional mutual exclusion with handoff/new/switch/branch/clear/fork/
|
|
17947
19633
|
// navigateTree). Released in the outer finally below.
|
|
@@ -17954,11 +19640,66 @@ export class AgentSession {
|
|
|
17954
19640
|
}
|
|
17955
19641
|
const compactionAbortController = new AbortController();
|
|
17956
19642
|
this.#compactionAbortController = compactionAbortController;
|
|
19643
|
+
const steeringBeforeCompaction = this.agent.snapshotSteering();
|
|
19644
|
+
const steeringDisplaysBeforeCompaction = [...this.#steeringMessages];
|
|
19645
|
+
const trackedSteersBeforeCompaction = new Map(
|
|
19646
|
+
[...this.#trackedQueuedInputs.values()]
|
|
19647
|
+
.filter(state => state.message !== undefined && steeringBeforeCompaction.includes(state.message))
|
|
19648
|
+
.map(state => [state.message as AgentMessage, state] as const),
|
|
19649
|
+
);
|
|
19650
|
+
const followUpBeforeCompaction = this.agent.snapshotFollowUp();
|
|
19651
|
+
const followUpDisplaysBeforeCompaction = [...this.#followUpMessages];
|
|
19652
|
+
const trackedFollowUpsBeforeCompaction = new Map(
|
|
19653
|
+
[...this.#trackedQueuedInputs.values()]
|
|
19654
|
+
.filter(state => state.message !== undefined && followUpBeforeCompaction.includes(state.message))
|
|
19655
|
+
.map(state => [state.message as AgentMessage, state] as const),
|
|
19656
|
+
);
|
|
19657
|
+
// Compaction intentionally preserves still-queued inputs, but it drops the
|
|
19658
|
+
// Agent event bridge before aborting the active run. Settle submissions that
|
|
19659
|
+
// already crossed the queue boundary now; they cannot receive agent_end later.
|
|
19660
|
+
this.#settleTrackedQueuedInputsBeforeAgentDisconnect();
|
|
17957
19661
|
this.#disconnectFromAgent();
|
|
17958
19662
|
try {
|
|
17959
|
-
|
|
19663
|
+
try {
|
|
19664
|
+
await this.abort({ cause: "compaction", preserveCompaction: true });
|
|
19665
|
+
} finally {
|
|
19666
|
+
const steeringAfterAbort = new Set(this.agent.snapshotSteering());
|
|
19667
|
+
const missingSteering = steeringBeforeCompaction.filter(message => !steeringAfterAbort.has(message));
|
|
19668
|
+
const restorableSteering: AgentMessage[] = [];
|
|
19669
|
+
for (const message of missingSteering) {
|
|
19670
|
+
const trackedState = trackedSteersBeforeCompaction.get(message);
|
|
19671
|
+
if (trackedState && (trackedState.terminalSettled || trackedState.cancelRequested)) continue;
|
|
19672
|
+
restorableSteering.push(message);
|
|
19673
|
+
}
|
|
19674
|
+
if (restorableSteering.length > 0) this.agent.restoreSteering(restorableSteering);
|
|
19675
|
+
this.#reconcileCompactionQueueDisplay(steeringDisplaysBeforeCompaction, "steering");
|
|
19676
|
+
const followUpAfterAbort = new Set(this.agent.snapshotFollowUp());
|
|
19677
|
+
const missingFollowUp = followUpBeforeCompaction.filter(message => !followUpAfterAbort.has(message));
|
|
19678
|
+
const restorableFollowUp: AgentMessage[] = [];
|
|
19679
|
+
for (const message of missingFollowUp) {
|
|
19680
|
+
const trackedState = trackedFollowUpsBeforeCompaction.get(message);
|
|
19681
|
+
if (trackedState) {
|
|
19682
|
+
const state = trackedState;
|
|
19683
|
+
if (state && !state.terminalSettled) this.#settleTrackedQueuedInputRemoved(state, "removed");
|
|
19684
|
+
continue;
|
|
19685
|
+
}
|
|
19686
|
+
restorableFollowUp.push(message);
|
|
19687
|
+
}
|
|
19688
|
+
if (restorableFollowUp.length > 0) this.agent.restoreFollowUp(restorableFollowUp);
|
|
19689
|
+
this.#reconcileCompactionQueueDisplay(followUpDisplaysBeforeCompaction, "followUp");
|
|
19690
|
+
// The disconnected bridge cannot run finishAttempt for the aborted
|
|
19691
|
+
// predecessor. Do not let its SDK token or attempt identity bleed into
|
|
19692
|
+
// the first ordinary successor prompt.
|
|
19693
|
+
this.#resetActiveSdkRunOwnership();
|
|
19694
|
+
}
|
|
17960
19695
|
} catch (error) {
|
|
17961
19696
|
this.#compactionAbortController = undefined;
|
|
19697
|
+
if (!this.#isDisposed && !this.#sessionAdmissionClosing) {
|
|
19698
|
+
this.#reconnectToAgent();
|
|
19699
|
+
const releasedDeferred = this.#releaseDeferredSdkFollowUps();
|
|
19700
|
+
this.#compactionQueuedDeliveryArmed = this.agent.hasQueuedMessages();
|
|
19701
|
+
if (!releasedDeferred && this.#compactionQueuedDeliveryArmed) this.#scheduleQueuedDelivery();
|
|
19702
|
+
}
|
|
17962
19703
|
throw error;
|
|
17963
19704
|
}
|
|
17964
19705
|
await this.#waitForAutoCompactionCompletions();
|
|
@@ -18106,7 +19847,12 @@ export class AgentSession {
|
|
|
18106
19847
|
if (this.#compactionAbortController === compactionAbortController) {
|
|
18107
19848
|
this.#compactionAbortController = undefined;
|
|
18108
19849
|
}
|
|
18109
|
-
if (!this.#isDisposed && !this.#sessionAdmissionClosing)
|
|
19850
|
+
if (!this.#isDisposed && !this.#sessionAdmissionClosing) {
|
|
19851
|
+
this.#reconnectToAgent();
|
|
19852
|
+
const releasedDeferred = this.#releaseDeferredSdkFollowUps();
|
|
19853
|
+
this.#compactionQueuedDeliveryArmed = this.agent.hasQueuedMessages();
|
|
19854
|
+
if (!releasedDeferred && this.#compactionQueuedDeliveryArmed) this.#scheduleQueuedDelivery();
|
|
19855
|
+
}
|
|
18110
19856
|
}
|
|
18111
19857
|
} finally {
|
|
18112
19858
|
completion.resolve();
|
|
@@ -18350,6 +20096,7 @@ export class AgentSession {
|
|
|
18350
20096
|
let savedPath: string | undefined;
|
|
18351
20097
|
let committed = false;
|
|
18352
20098
|
let prepared: PreparedNewSession | undefined;
|
|
20099
|
+
let exactRetirement: PreparedExactMcpRetirement | undefined;
|
|
18353
20100
|
try {
|
|
18354
20101
|
// Prepare successor entries, persistence, display state, and gate
|
|
18355
20102
|
// construction without publishing manager identity. Managed local-root
|
|
@@ -18375,9 +20122,13 @@ export class AgentSession {
|
|
|
18375
20122
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
18376
20123
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
18377
20124
|
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
20125
|
+
exactRetirement = await this.#prepareExactMcpControlRetirement();
|
|
20126
|
+
if (exactRetirement && !exactRetirement.canCommit())
|
|
20127
|
+
throw new Error("Exact MCP control retirement changed before session adoption");
|
|
18378
20128
|
|
|
18379
20129
|
// --- Commit boundary: synchronous adoption is the sole identity publication.
|
|
18380
20130
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
20131
|
+
await exactRetirement?.commit();
|
|
18381
20132
|
// Handoff commits a successor endpoint identity; re-register the
|
|
18382
20133
|
// manager under it (review thread P1).
|
|
18383
20134
|
this.#rekeyJobManagerForSessionIdentity(rollbackSessionState.sessionId, rollbackSessionState.sessionFile);
|
|
@@ -18387,6 +20138,7 @@ export class AgentSession {
|
|
|
18387
20138
|
...rollbackAgentSteeringQueue,
|
|
18388
20139
|
...rollbackAgentFollowUpQueue,
|
|
18389
20140
|
...rollbackDeferredSdkFollowUps,
|
|
20141
|
+
...rollbackPendingNextTurnMessages.map(entry => entry.message),
|
|
18390
20142
|
]);
|
|
18391
20143
|
this.#resetActiveSdkRunOwnership();
|
|
18392
20144
|
this.agent.reset();
|
|
@@ -18437,11 +20189,13 @@ export class AgentSession {
|
|
|
18437
20189
|
// errors are isolated by ExtensionRunner and must not roll back the
|
|
18438
20190
|
// already-committed switch.
|
|
18439
20191
|
if (this.#extensionRunner) {
|
|
18440
|
-
await this.#
|
|
18441
|
-
|
|
18442
|
-
|
|
18443
|
-
|
|
18444
|
-
|
|
20192
|
+
await this.#runCommittedSuccessorHook(() =>
|
|
20193
|
+
this.#extensionRunner!.emit({
|
|
20194
|
+
type: "session_switch",
|
|
20195
|
+
reason: "new",
|
|
20196
|
+
previousSessionFile,
|
|
20197
|
+
}),
|
|
20198
|
+
);
|
|
18445
20199
|
}
|
|
18446
20200
|
|
|
18447
20201
|
return { document: handoffText, savedPath };
|
|
@@ -18458,6 +20212,7 @@ export class AgentSession {
|
|
|
18458
20212
|
// failure is non-destructive. Predecessor gate emitter, provider
|
|
18459
20213
|
// sessions, async jobs, IRC/plan bookkeeping, and injection signatures
|
|
18460
20214
|
// were never mutated before commit, so they survive intact.
|
|
20215
|
+
await exactRetirement?.abort();
|
|
18461
20216
|
await this.sessionManager.restoreRollbackState(rollbackSessionState);
|
|
18462
20217
|
this.#syncAgentSessionId(rollbackSessionState.sessionId);
|
|
18463
20218
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
@@ -19884,33 +21639,79 @@ export class AgentSession {
|
|
|
19884
21639
|
addCandidate(this.#resolveRoleModelFull(role, availableModels, currentModel).model);
|
|
19885
21640
|
}
|
|
19886
21641
|
|
|
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
21642
|
return candidates;
|
|
19906
21643
|
}
|
|
21644
|
+
#compactionCandidateSource(candidate: Model): string {
|
|
21645
|
+
const currentModel = this.model;
|
|
21646
|
+
if (currentModel && modelsAreEqual(candidate, currentModel)) return "active session model";
|
|
21647
|
+
for (const role of MODEL_ROLE_IDS) {
|
|
21648
|
+
const roleModel = this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), currentModel).model;
|
|
21649
|
+
if (roleModel && modelsAreEqual(candidate, roleModel)) return `modelRoles.${role}`;
|
|
21650
|
+
}
|
|
21651
|
+
return "configured compaction fallback";
|
|
21652
|
+
}
|
|
21653
|
+
#compactionFailureReason(error: unknown): string {
|
|
21654
|
+
const message = sanitizeCompactionCandidateFailureMessage(error instanceof Error ? error.message : String(error));
|
|
21655
|
+
if (/stream stalled while waiting for the next event/i.test(message)) {
|
|
21656
|
+
return "SSE stream stalled while waiting for the next event";
|
|
21657
|
+
}
|
|
21658
|
+
if (/timed? out while waiting for the first event/i.test(message)) {
|
|
21659
|
+
return "SSE stream timed out while waiting for the first event";
|
|
21660
|
+
}
|
|
21661
|
+
if (/auth_unavailable|no auth available|credential/i.test(message)) {
|
|
21662
|
+
return "candidate credentials unavailable";
|
|
21663
|
+
}
|
|
21664
|
+
return message.length > 160 ? `${message.slice(0, 157)}...` : message;
|
|
21665
|
+
}
|
|
21666
|
+
async #emitCompactionModelSubstitution(from: Model, to: Model, reason: string): Promise<void> {
|
|
21667
|
+
const fromKey = this.#getModelKey(from);
|
|
21668
|
+
const toKey = this.#getModelKey(to);
|
|
21669
|
+
const source = this.#compactionCandidateSource(to);
|
|
21670
|
+
try {
|
|
21671
|
+
await this.#emitSessionEvent({
|
|
21672
|
+
type: "notice",
|
|
21673
|
+
level: "warning",
|
|
21674
|
+
source: "compaction-recovery",
|
|
21675
|
+
message: `Compaction recovery switched model from ${fromKey} to ${toKey} (${source}) after ${reason}.`,
|
|
21676
|
+
});
|
|
21677
|
+
} catch (error) {
|
|
21678
|
+
logger.warn("Failed to publish compaction model substitution", {
|
|
21679
|
+
from: fromKey,
|
|
21680
|
+
to: toKey,
|
|
21681
|
+
error: String(error),
|
|
21682
|
+
});
|
|
21683
|
+
}
|
|
21684
|
+
}
|
|
21685
|
+
async #resolveCompactionCandidate(
|
|
21686
|
+
candidate: Model,
|
|
21687
|
+
previousCandidate: Model | undefined,
|
|
21688
|
+
previousFailure: unknown,
|
|
21689
|
+
): Promise<{ apiKey: string; authError?: undefined } | { apiKey: undefined; authError: Error }> {
|
|
21690
|
+
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.credentialSessionId);
|
|
21691
|
+
if (!apiKey) return { apiKey: undefined, authError: this.#buildCompactionAuthError(candidate) };
|
|
21692
|
+
if (previousCandidate) {
|
|
21693
|
+
await this.#emitCompactionModelSubstitution(
|
|
21694
|
+
previousCandidate,
|
|
21695
|
+
candidate,
|
|
21696
|
+
this.#compactionFailureReason(previousFailure),
|
|
21697
|
+
);
|
|
21698
|
+
}
|
|
21699
|
+
return { apiKey, authError: undefined };
|
|
21700
|
+
}
|
|
19907
21701
|
#isCompactionAuthFailure(error: unknown): boolean {
|
|
19908
21702
|
if (!(error instanceof Error)) return false;
|
|
21703
|
+
if (
|
|
21704
|
+
/stream stalled while waiting for the next event|timed? out while waiting for the first event/i.test(
|
|
21705
|
+
error.message,
|
|
21706
|
+
)
|
|
21707
|
+
) {
|
|
21708
|
+
return false;
|
|
21709
|
+
}
|
|
19909
21710
|
return /auth_unavailable|no auth available/i.test(error.message);
|
|
19910
21711
|
}
|
|
19911
21712
|
|
|
19912
|
-
#buildCompactionAuthError(): Error {
|
|
19913
|
-
const currentModel = this.model;
|
|
21713
|
+
#buildCompactionAuthError(candidate?: Model): Error {
|
|
21714
|
+
const currentModel = candidate ?? this.model;
|
|
19914
21715
|
if (!currentModel) {
|
|
19915
21716
|
return new Error(
|
|
19916
21717
|
"Compaction requires a model with usable credentials, but no authenticated compaction model is available.",
|
|
@@ -19960,10 +21761,20 @@ export class AgentSession {
|
|
|
19960
21761
|
): Promise<CompactionResult> {
|
|
19961
21762
|
const candidates = this.#getCompactionModelCandidates(this.#modelRegistry.getAvailable());
|
|
19962
21763
|
const telemetry = resolveTelemetry(this.agent.telemetry, this.sessionId);
|
|
21764
|
+
let previousCandidate: Model | undefined;
|
|
21765
|
+
let previousFailure: unknown;
|
|
19963
21766
|
|
|
19964
21767
|
for (const candidate of candidates) {
|
|
19965
|
-
const apiKey = await this.#
|
|
19966
|
-
|
|
21768
|
+
const { apiKey, authError } = await this.#resolveCompactionCandidate(
|
|
21769
|
+
candidate,
|
|
21770
|
+
previousCandidate,
|
|
21771
|
+
previousFailure,
|
|
21772
|
+
);
|
|
21773
|
+
if (apiKey === undefined) {
|
|
21774
|
+
previousCandidate = candidate;
|
|
21775
|
+
previousFailure = authError;
|
|
21776
|
+
continue;
|
|
21777
|
+
}
|
|
19967
21778
|
|
|
19968
21779
|
try {
|
|
19969
21780
|
return await compact(preparation, candidate, apiKey, customInstructions, signal, {
|
|
@@ -19981,10 +21792,12 @@ export class AgentSession {
|
|
|
19981
21792
|
if (!this.#isCompactionAuthFailure(error)) {
|
|
19982
21793
|
throw error;
|
|
19983
21794
|
}
|
|
21795
|
+
previousCandidate = candidate;
|
|
21796
|
+
previousFailure = this.#buildCompactionAuthError(candidate);
|
|
19984
21797
|
}
|
|
19985
21798
|
}
|
|
19986
21799
|
|
|
19987
|
-
throw this.#buildCompactionAuthError();
|
|
21800
|
+
throw previousFailure instanceof Error ? previousFailure : this.#buildCompactionAuthError();
|
|
19988
21801
|
}
|
|
19989
21802
|
|
|
19990
21803
|
async #prepareCompactionFromHooks(
|
|
@@ -20144,6 +21957,15 @@ export class AgentSession {
|
|
|
20144
21957
|
let action: "context-full" | "handoff" =
|
|
20145
21958
|
compactionSettings.strategy === "handoff" && reason !== "overflow" ? "handoff" : "context-full";
|
|
20146
21959
|
const continueAfterMaintenance = options?.continueAfterMaintenance !== false;
|
|
21960
|
+
let ownsAutoCompactionTransition = false;
|
|
21961
|
+
const acquireAutoCompactionTransition = (): boolean => {
|
|
21962
|
+
if (ownsAutoCompactionTransition) return true;
|
|
21963
|
+
if (this.#sessionTransitionKind !== undefined || this.#cancelAndSubmitInProgress) return false;
|
|
21964
|
+
this.#beginSessionTransition("auto-compaction");
|
|
21965
|
+
ownsAutoCompactionTransition = true;
|
|
21966
|
+
return true;
|
|
21967
|
+
};
|
|
21968
|
+
if (action === "context-full" && !acquireAutoCompactionTransition()) return { kind: "skipped" };
|
|
20147
21969
|
// Register the controller before the observable start event so an abort from
|
|
20148
21970
|
// a local subscriber, extension, dispose, or caller signal owns this run.
|
|
20149
21971
|
this.#autoCompactionAbortController?.abort();
|
|
@@ -20168,12 +21990,21 @@ export class AgentSession {
|
|
|
20168
21990
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
20169
21991
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
20170
21992
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
20171
|
-
|
|
20172
|
-
|
|
20173
|
-
|
|
20174
|
-
|
|
21993
|
+
// Start the workflow projection in parallel with the synchronous compaction
|
|
21994
|
+
// preparation. Overflow recovery often has no eligible history after the
|
|
21995
|
+
// failed assistant is removed; waiting for this filesystem projection before
|
|
21996
|
+
// discovering that no-op would delay the terminal auto_compaction_end event.
|
|
21997
|
+
// The promise is still awaited below so zero-progress tracking remains a
|
|
21998
|
+
// completed side effect for every compaction observation.
|
|
21999
|
+
const compactionStateSnapshotPromise = this.#compactionStateSnapshot({
|
|
22000
|
+
trackWorkflowRecoveryProgress: true,
|
|
22001
|
+
});
|
|
20175
22002
|
|
|
20176
22003
|
if (compactionSettings.strategy === "handoff" && reason !== "overflow") {
|
|
22004
|
+
await compactionStateSnapshotPromise;
|
|
22005
|
+
if (autoCompactionSignal.aborted || this.#isDisposed || this.#promptGeneration !== generation) {
|
|
22006
|
+
return await emitAborted();
|
|
22007
|
+
}
|
|
20177
22008
|
const handoffFocus = AUTO_HANDOFF_THRESHOLD_FOCUS;
|
|
20178
22009
|
const handoffResult = await this.handoff(handoffFocus, {
|
|
20179
22010
|
autoTriggered: true,
|
|
@@ -20196,6 +22027,7 @@ export class AgentSession {
|
|
|
20196
22027
|
reason,
|
|
20197
22028
|
});
|
|
20198
22029
|
action = "context-full";
|
|
22030
|
+
if (!acquireAutoCompactionTransition()) return { kind: "skipped" };
|
|
20199
22031
|
}
|
|
20200
22032
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
20201
22033
|
|
|
@@ -20218,28 +22050,14 @@ export class AgentSession {
|
|
|
20218
22050
|
}
|
|
20219
22051
|
|
|
20220
22052
|
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" };
|
|
22053
|
+
await compactionStateSnapshotPromise;
|
|
22054
|
+
throw new Error("No model selected");
|
|
20230
22055
|
}
|
|
20231
22056
|
|
|
20232
22057
|
const availableModels = this.#modelRegistry.getAvailable();
|
|
20233
22058
|
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" };
|
|
22059
|
+
await compactionStateSnapshotPromise;
|
|
22060
|
+
throw this.#buildCompactionAuthError(this.model);
|
|
20243
22061
|
}
|
|
20244
22062
|
|
|
20245
22063
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
@@ -20287,6 +22105,7 @@ export class AgentSession {
|
|
|
20287
22105
|
skipped: true,
|
|
20288
22106
|
continuationSkipReason,
|
|
20289
22107
|
});
|
|
22108
|
+
await compactionStateSnapshotPromise;
|
|
20290
22109
|
if (overflowNoopWouldReplay) {
|
|
20291
22110
|
if (continueAfterMaintenance && this.agent.hasQueuedMessages()) {
|
|
20292
22111
|
this.#scheduleAgentContinue({
|
|
@@ -20327,6 +22146,11 @@ export class AgentSession {
|
|
|
20327
22146
|
return { kind: "skipped" };
|
|
20328
22147
|
}
|
|
20329
22148
|
|
|
22149
|
+
const compactionStateSnapshot = await compactionStateSnapshotPromise;
|
|
22150
|
+
if (autoCompactionSignal.aborted || this.#isDisposed || this.#promptGeneration !== generation) {
|
|
22151
|
+
return await emitAborted();
|
|
22152
|
+
}
|
|
22153
|
+
|
|
20330
22154
|
let hookCompaction: CompactionResult | undefined;
|
|
20331
22155
|
let fromExtension = false;
|
|
20332
22156
|
let preserveData: Record<string, unknown> | undefined;
|
|
@@ -20421,14 +22245,29 @@ export class AgentSession {
|
|
|
20421
22245
|
const telemetry = resolveTelemetry(this.agent.telemetry, this.sessionId);
|
|
20422
22246
|
let compactResult: CompactionResult | undefined;
|
|
20423
22247
|
let lastError: unknown;
|
|
20424
|
-
// Every candidate that was tried and failed, in order.
|
|
20425
|
-
//
|
|
20426
|
-
//
|
|
22248
|
+
// Every candidate that was tried and failed, in order. Keep the first
|
|
22249
|
+
// failure visible, and retain candidate-specific diagnostics for explicit
|
|
22250
|
+
// role fallbacks instead of collapsing the chain to its final error.
|
|
20427
22251
|
const candidateFailures: Array<{ model: string; message: string }> = [];
|
|
22252
|
+
let previousCandidate: Model | undefined;
|
|
22253
|
+
let previousFailure: unknown;
|
|
20428
22254
|
|
|
20429
22255
|
for (const candidate of candidates) {
|
|
20430
|
-
const apiKey = await this.#
|
|
20431
|
-
|
|
22256
|
+
const { apiKey, authError } = await this.#resolveCompactionCandidate(
|
|
22257
|
+
candidate,
|
|
22258
|
+
previousCandidate,
|
|
22259
|
+
previousFailure,
|
|
22260
|
+
);
|
|
22261
|
+
if (apiKey === undefined) {
|
|
22262
|
+
lastError = authError;
|
|
22263
|
+
previousCandidate = candidate;
|
|
22264
|
+
previousFailure = authError;
|
|
22265
|
+
candidateFailures.push({
|
|
22266
|
+
model: `${candidate.provider}/${candidate.id}`,
|
|
22267
|
+
message: authError.message,
|
|
22268
|
+
});
|
|
22269
|
+
continue;
|
|
22270
|
+
}
|
|
20432
22271
|
|
|
20433
22272
|
let attempt = 0;
|
|
20434
22273
|
while (true) {
|
|
@@ -20457,8 +22296,14 @@ export class AgentSession {
|
|
|
20457
22296
|
|
|
20458
22297
|
const message = error instanceof Error ? error.message : String(error);
|
|
20459
22298
|
if (this.#isCompactionAuthFailure(error)) {
|
|
20460
|
-
|
|
20461
|
-
|
|
22299
|
+
const authError = this.#buildCompactionAuthError(candidate);
|
|
22300
|
+
lastError = authError;
|
|
22301
|
+
previousCandidate = candidate;
|
|
22302
|
+
previousFailure = authError;
|
|
22303
|
+
candidateFailures.push({
|
|
22304
|
+
model: `${candidate.provider}/${candidate.id}`,
|
|
22305
|
+
message: authError.message,
|
|
22306
|
+
});
|
|
20462
22307
|
break;
|
|
20463
22308
|
}
|
|
20464
22309
|
const retryAfterMs = this.#parseRetryAfterMsFromError(message);
|
|
@@ -20471,6 +22316,8 @@ export class AgentSession {
|
|
|
20471
22316
|
isUsageLimitError(message));
|
|
20472
22317
|
if (!shouldRetry) {
|
|
20473
22318
|
lastError = error;
|
|
22319
|
+
previousCandidate = candidate;
|
|
22320
|
+
previousFailure = error;
|
|
20474
22321
|
candidateFailures.push({ model: `${candidate.provider}/${candidate.id}`, message });
|
|
20475
22322
|
break;
|
|
20476
22323
|
}
|
|
@@ -20496,6 +22343,8 @@ export class AgentSession {
|
|
|
20496
22343
|
model: `${candidate.provider}/${candidate.id}`,
|
|
20497
22344
|
});
|
|
20498
22345
|
lastError = error;
|
|
22346
|
+
previousCandidate = candidate;
|
|
22347
|
+
previousFailure = error;
|
|
20499
22348
|
candidateFailures.push({ model: `${candidate.provider}/${candidate.id}`, message });
|
|
20500
22349
|
break; // Exit retry loop, continue to next candidate
|
|
20501
22350
|
}
|
|
@@ -20643,6 +22492,7 @@ export class AgentSession {
|
|
|
20643
22492
|
if (this.#autoCompactionAbortController === autoCompactionAbortController) {
|
|
20644
22493
|
this.#autoCompactionAbortController = undefined;
|
|
20645
22494
|
}
|
|
22495
|
+
if (ownsAutoCompactionTransition) this.#endSessionTransition();
|
|
20646
22496
|
}
|
|
20647
22497
|
}
|
|
20648
22498
|
|
|
@@ -20911,6 +22761,11 @@ export class AgentSession {
|
|
|
20911
22761
|
if (message.errorMessage?.startsWith("Managed fallback retried the escaped non-ASCII")) return "terminal";
|
|
20912
22762
|
if (message.stopReason !== "error") return "none";
|
|
20913
22763
|
if (message.errorKind === "provider_safety_stop") return "terminal";
|
|
22764
|
+
// A decode loop is deterministic for the submitted context: replaying the
|
|
22765
|
+
// identical conversation re-trips the guard and re-bills the full context.
|
|
22766
|
+
// Without this the message carries no transport facts and would fall
|
|
22767
|
+
// through to "unknown", which is admitted for bounded retry (#5627).
|
|
22768
|
+
if (message.errorCode === REPETITION_GUARD_ERROR_CODE) return "terminal";
|
|
20914
22769
|
if (message.errorKind === "local_snapshot_failure") return "local_snapshot";
|
|
20915
22770
|
if (message.errorKind === "local_buffer_overflow") return "local_buffer_overflow";
|
|
20916
22771
|
if (this.#isTypedFirstEventTimeout(message)) return "first_event_timeout";
|
|
@@ -21459,6 +23314,10 @@ export class AgentSession {
|
|
|
21459
23314
|
if (classifyContextOverflow(message, transportFailure, this.model?.contextWindow ?? 0)) return undefined;
|
|
21460
23315
|
const transport = classifyFallbackTrigger(transportFailure ?? { status: message.errorStatus });
|
|
21461
23316
|
if (transport.class !== "other") return transport;
|
|
23317
|
+
// HTTP/2 observations explain a terminal failure; they do not authorize replay.
|
|
23318
|
+
if (transportFailure?.http2RstCode !== undefined || transportFailure?.nativeErrorCode !== undefined) {
|
|
23319
|
+
return undefined;
|
|
23320
|
+
}
|
|
21462
23321
|
// Managed fallback receives authoritative transport facts from the request
|
|
21463
23322
|
// boundary. Once those facts classify as other, error prose must not upgrade
|
|
21464
23323
|
// the failure into an unbounded transient or quota retry.
|
|
@@ -23744,8 +25603,16 @@ export class AgentSession {
|
|
|
23744
25603
|
const previousPendingNextTurnMessages = [...this.#pendingNextTurnMessages];
|
|
23745
25604
|
const previousScheduledHiddenNextTurnGeneration = this.#scheduledHiddenNextTurnGeneration;
|
|
23746
25605
|
const previousModel = this.model;
|
|
25606
|
+
const previousDefaultFallbackRuntimeState = this.getDefaultFallbackRuntimeState();
|
|
23747
25607
|
const previousThinkingLevel = this.#thinkingLevel;
|
|
23748
25608
|
const previousActiveModelProfile = this.#activeModelProfile;
|
|
25609
|
+
const previousModelRolesOverride = structuredClone(this.settings.getOverride("modelRoles"));
|
|
25610
|
+
const previousAgentModelOverridesOverride = structuredClone(
|
|
25611
|
+
this.settings.getOverride("task.agentModelOverrides"),
|
|
25612
|
+
);
|
|
25613
|
+
const previousActiveProfileInstalledRoles = new Map(this.#activeProfileInstalledRoles);
|
|
25614
|
+
const previousActiveProfileInstalledAgentOverrides = new Map(this.#activeProfileInstalledAgentOverrides);
|
|
25615
|
+
const previousPreProfileModel = this.#preProfileModel;
|
|
23749
25616
|
const previousServiceTier = this.agent.serviceTier;
|
|
23750
25617
|
const previousSelectedMCPToolNames = new Set(this.#selectedMCPToolNames);
|
|
23751
25618
|
const previousTools = [...this.agent.state.tools];
|
|
@@ -23754,6 +25621,7 @@ export class AgentSession {
|
|
|
23754
25621
|
const previousAgentSteeringQueue = this.agent.snapshotSteering();
|
|
23755
25622
|
const previousAgentFollowUpQueue = this.agent.snapshotFollowUp();
|
|
23756
25623
|
const previousDeferredSdkFollowUps = [...this.#deferredSdkFollowUps];
|
|
25624
|
+
const previousTrackedQueuedInputs = [...this.#trackedQueuedInputs.values()];
|
|
23757
25625
|
const previousActiveSdkRunToken = this.#activeSdkRunToken;
|
|
23758
25626
|
const previousActiveAttemptScope = this.#activeAttemptScope;
|
|
23759
25627
|
const previousActiveLogicalRunId = this.#activeLogicalRunId;
|
|
@@ -23766,7 +25634,11 @@ export class AgentSession {
|
|
|
23766
25634
|
? this.#suspendWorkflowGateEmitter(previousSessionState.sessionId)
|
|
23767
25635
|
: undefined;
|
|
23768
25636
|
let unavailableDefaultChainMessage: string | undefined;
|
|
25637
|
+
let recoveredDefaultChainMessage: string | undefined;
|
|
25638
|
+
let recoveredThinkingLevel: ThinkingLevel | undefined;
|
|
25639
|
+
let durableDefaultRecoveryError: string | undefined;
|
|
23769
25640
|
let transitionCleanupCommitted = false;
|
|
25641
|
+
let exactRetirement: PreparedExactMcpRetirement | undefined;
|
|
23770
25642
|
|
|
23771
25643
|
try {
|
|
23772
25644
|
await this.sessionManager.setSessionFile(sessionPath, {
|
|
@@ -23822,12 +25694,6 @@ export class AgentSession {
|
|
|
23822
25694
|
|
|
23823
25695
|
const resumeModelBehavior = this.settings.get("session.resumeModelBehavior");
|
|
23824
25696
|
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
25697
|
const profileDefinitions = this.#modelRegistry.getModelProfiles?.() ?? new Map<string, unknown>();
|
|
23832
25698
|
const configuredProfileName = this.settings.get("modelProfile.default");
|
|
23833
25699
|
const configuredProfileIdentity = configuredProfileName
|
|
@@ -23839,18 +25705,45 @@ export class AgentSession {
|
|
|
23839
25705
|
const liveProfileIdentity = previousActiveModelProfile
|
|
23840
25706
|
? resolveModelProfileName(previousActiveModelProfile, profileDefinitions)
|
|
23841
25707
|
: undefined;
|
|
23842
|
-
|
|
25708
|
+
let targetActiveModelProfile =
|
|
23843
25709
|
resumeModelBehavior === "useCurrentDefault"
|
|
23844
|
-
?
|
|
23845
|
-
?
|
|
23846
|
-
: configuredProfileIdentity && profileDefinitions.has(configuredProfileIdentity)
|
|
25710
|
+
? switchingToDifferentSession
|
|
25711
|
+
? configuredProfileIdentity && profileDefinitions.has(configuredProfileIdentity)
|
|
23847
25712
|
? configuredProfileIdentity
|
|
23848
25713
|
: undefined
|
|
25714
|
+
: liveProfileIdentity && profileDefinitions.has(liveProfileIdentity)
|
|
25715
|
+
? liveProfileIdentity
|
|
25716
|
+
: configuredProfileIdentity && profileDefinitions.has(configuredProfileIdentity)
|
|
25717
|
+
? configuredProfileIdentity
|
|
25718
|
+
: undefined
|
|
23849
25719
|
: configuredDefaultChain?.origin === "profile-activation" &&
|
|
23850
25720
|
persistedProfileIdentity &&
|
|
23851
25721
|
profileDefinitions.has(persistedProfileIdentity)
|
|
23852
25722
|
? persistedProfileIdentity
|
|
23853
25723
|
: undefined;
|
|
25724
|
+
// Keep a durable profile's existing runtime layer when the successor
|
|
25725
|
+
// resolves to that same profile. A cross-file transition has no separate
|
|
25726
|
+
// profile activation to reinstall its role and delegation bindings. Any
|
|
25727
|
+
// session-only or different predecessor profile must still be removed
|
|
25728
|
+
// before resolving the successor's default chain.
|
|
25729
|
+
const retainsDurableProfileLayer =
|
|
25730
|
+
switchingToDifferentSession &&
|
|
25731
|
+
targetActiveModelProfile !== undefined &&
|
|
25732
|
+
targetActiveModelProfile === configuredProfileIdentity &&
|
|
25733
|
+
liveProfileIdentity === targetActiveModelProfile;
|
|
25734
|
+
let targetProfileRuntimeInstalled = !switchingToDifferentSession || retainsDurableProfileLayer;
|
|
25735
|
+
if (switchingToDifferentSession && !retainsDurableProfileLayer)
|
|
25736
|
+
this.#resetSessionScopedModelProfileState({
|
|
25737
|
+
preserveDefaultConfiguredChain: true,
|
|
25738
|
+
force: true,
|
|
25739
|
+
});
|
|
25740
|
+
const settingsDefaultEntries = normalizeModelSelectorValue(this.settings.getModelRole("default"));
|
|
25741
|
+
const defaultEntries =
|
|
25742
|
+
resumeModelBehavior === "useCurrentDefault"
|
|
25743
|
+
? settingsDefaultEntries
|
|
25744
|
+
: (configuredDefaultChain?.entries ??
|
|
25745
|
+
(sessionContext.models.default ? [sessionContext.models.default] : []));
|
|
25746
|
+
this.#activeModelProfile = targetActiveModelProfile;
|
|
23854
25747
|
this.#defaultFallbackController = undefined;
|
|
23855
25748
|
if (defaultEntries.length > 0) {
|
|
23856
25749
|
const resolution = await resolveModelChainWithAuth(
|
|
@@ -23864,14 +25757,77 @@ export class AgentSession {
|
|
|
23864
25757
|
...(this.#persistedModelProfileAliasIntent("default") ?? {}),
|
|
23865
25758
|
},
|
|
23866
25759
|
);
|
|
23867
|
-
|
|
25760
|
+
let controller = this.#defaultFallbackChain();
|
|
23868
25761
|
this.seedDefaultFallbackResolution(resolution.activeIndex, resolution.skips);
|
|
23869
|
-
|
|
23870
|
-
|
|
23871
|
-
|
|
25762
|
+
let resolvedModel = resolution.model;
|
|
25763
|
+
if (!resolvedModel) {
|
|
25764
|
+
if (resumeModelBehavior === "keepSessionModel") {
|
|
25765
|
+
try {
|
|
25766
|
+
const recovery = await resolveMissingSessionModelRecovery({
|
|
25767
|
+
modelRegistry: this.#modelRegistry,
|
|
25768
|
+
settings: this.settings,
|
|
25769
|
+
defaultEntries,
|
|
25770
|
+
skips: resolution.skips,
|
|
25771
|
+
savedDefault: sessionContext.models.default,
|
|
25772
|
+
credentialSessionId: this.credentialSessionId,
|
|
25773
|
+
...(this.#persistedModelProfileAliasIntent("default") ?? {}),
|
|
25774
|
+
});
|
|
25775
|
+
if (recovery?.model) {
|
|
25776
|
+
const installedProfileKeys =
|
|
25777
|
+
this.#activeProfileInstalledRoles.size > 0 ||
|
|
25778
|
+
this.#activeProfileInstalledAgentOverrides.size > 0;
|
|
25779
|
+
const recoveryNeedsBindings =
|
|
25780
|
+
switchingToDifferentSession ||
|
|
25781
|
+
targetActiveModelProfile !== recovery.profileName ||
|
|
25782
|
+
!installedProfileKeys;
|
|
25783
|
+
if (recoveryNeedsBindings) {
|
|
25784
|
+
if (!switchingToDifferentSession)
|
|
25785
|
+
this.#resetSessionScopedModelProfileState({
|
|
25786
|
+
preserveDefaultConfiguredChain: true,
|
|
25787
|
+
force: true,
|
|
25788
|
+
});
|
|
25789
|
+
await applyModelProfileRuntimeBindings({
|
|
25790
|
+
session: this,
|
|
25791
|
+
modelRegistry: this.#modelRegistry,
|
|
25792
|
+
settings: this.settings,
|
|
25793
|
+
profileName: recovery.profileName,
|
|
25794
|
+
});
|
|
25795
|
+
targetActiveModelProfile = recovery.profileName;
|
|
25796
|
+
targetProfileRuntimeInstalled = true;
|
|
25797
|
+
}
|
|
25798
|
+
this.installRecoveredDefaultFallbackChain(
|
|
25799
|
+
recovery.entries,
|
|
25800
|
+
recovery.profileName,
|
|
25801
|
+
recovery.activeIndex,
|
|
25802
|
+
recovery.skips,
|
|
25803
|
+
{ emitResolutionEvent: false },
|
|
25804
|
+
);
|
|
25805
|
+
controller = this.#defaultFallbackChain(false);
|
|
25806
|
+
resolvedModel = recovery.model;
|
|
25807
|
+
recoveredThinkingLevel = recovery.explicitThinkingLevel ? recovery.thinkingLevel : undefined;
|
|
25808
|
+
recoveredDefaultChainMessage =
|
|
25809
|
+
"Saved session model is no longer registered; restored the durable default preset instead.";
|
|
25810
|
+
} else if (recovery) {
|
|
25811
|
+
durableDefaultRecoveryError = "durable default preset resolution failed";
|
|
25812
|
+
}
|
|
25813
|
+
} catch (error) {
|
|
25814
|
+
// The saved chain remains authoritative on recovery failure, but the
|
|
25815
|
+
// durable preset's diagnostic is actionable and must not disappear.
|
|
25816
|
+
logger.warn("Failed to recover saved session model through durable default", {
|
|
25817
|
+
error: safeErrorDescription(error),
|
|
25818
|
+
});
|
|
25819
|
+
durableDefaultRecoveryError = "durable default preset resolution failed";
|
|
25820
|
+
}
|
|
25821
|
+
}
|
|
25822
|
+
if (!resolvedModel) {
|
|
25823
|
+
unavailableDefaultChainMessage = this.#fallbackExhaustionError(controller);
|
|
25824
|
+
if (durableDefaultRecoveryError)
|
|
25825
|
+
unavailableDefaultChainMessage += `; durable default preset recovery failed: ${durableDefaultRecoveryError}`;
|
|
25826
|
+
throw new Error(unavailableDefaultChainMessage);
|
|
25827
|
+
}
|
|
23872
25828
|
}
|
|
23873
|
-
if (!this.model || !modelsAreEqual(this.model,
|
|
23874
|
-
this.#setModelAuthoritatively(
|
|
25829
|
+
if (!this.model || !modelsAreEqual(this.model, resolvedModel)) {
|
|
25830
|
+
this.#setModelAuthoritatively(resolvedModel, "restore");
|
|
23875
25831
|
}
|
|
23876
25832
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
23877
25833
|
// No thinking-level write here: the recompute below is the single
|
|
@@ -23879,6 +25835,11 @@ export class AgentSession {
|
|
|
23879
25835
|
// would append a stray thinking_level_change entry that flips
|
|
23880
25836
|
// hasThinkingEntry and changes what the recompute restores.
|
|
23881
25837
|
}
|
|
25838
|
+
// The saved chain may need its profile identity for alias resolution, but
|
|
25839
|
+
// a cross-file transition must not advertise that profile after its
|
|
25840
|
+
// runtime role layer was removed. Otherwise delegation prompt state and
|
|
25841
|
+
// actual role routing diverge.
|
|
25842
|
+
if (!targetProfileRuntimeInstalled) this.#activeModelProfile = undefined;
|
|
23882
25843
|
|
|
23883
25844
|
const hasThinkingEntry = this.sessionManager
|
|
23884
25845
|
.getBranch()
|
|
@@ -23887,10 +25848,17 @@ export class AgentSession {
|
|
|
23887
25848
|
.getBranch()
|
|
23888
25849
|
.some(entry => entry.type === "service_tier_change");
|
|
23889
25850
|
const defaultThinkingLevel = this.settings.get("defaultThinkingLevel");
|
|
25851
|
+
const hasExplicitDefaultThinkingLevel = this.settings.has("defaultThinkingLevel");
|
|
23890
25852
|
const configuredServiceTier = this.settings.get("serviceTier");
|
|
23891
|
-
const
|
|
23892
|
-
|
|
23893
|
-
|
|
25853
|
+
const sessionThinkingLevel = sessionContext.thinkingLevel as ThinkingLevel | undefined;
|
|
25854
|
+
const recoveredModelThinkingLevel =
|
|
25855
|
+
recoveredDefaultChainMessage && !hasExplicitDefaultThinkingLevel
|
|
25856
|
+
? this.model?.thinking?.defaultLevel
|
|
25857
|
+
: undefined;
|
|
25858
|
+
const persistedThinkingLevel =
|
|
25859
|
+
hasThinkingEntry && sessionThinkingLevel !== ThinkingLevel.Inherit
|
|
25860
|
+
? sessionThinkingLevel
|
|
25861
|
+
: (recoveredThinkingLevel ?? recoveredModelThinkingLevel ?? defaultThinkingLevel);
|
|
23894
25862
|
const nextThinkingLevel = resolveThinkingLevelForModel(
|
|
23895
25863
|
this.model,
|
|
23896
25864
|
persistedThinkingLevel === ThinkingLevel.Inherit
|
|
@@ -23910,16 +25878,15 @@ export class AgentSession {
|
|
|
23910
25878
|
: configuredServiceTier === "none"
|
|
23911
25879
|
? undefined
|
|
23912
25880
|
: 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
25881
|
// Establish the successor's durable session identity only after every
|
|
23918
25882
|
// restored state facet is live. Identity-bound extension hooks run below.
|
|
23919
25883
|
await this.sessionManager.ensureOnDisk();
|
|
23920
25884
|
if (!switchingToDifferentSession) await this.#initializeLocalRootForLoadedSession();
|
|
23921
25885
|
|
|
23922
25886
|
if (switchingToDifferentSession) {
|
|
25887
|
+
// Profile cleanup changed the active role layer; refresh eager task
|
|
25888
|
+
// delegation and its base prompt before reconnecting the successor.
|
|
25889
|
+
await this.syncEagerDelegation();
|
|
23923
25890
|
// The local:// migration gate for this successor already ran above,
|
|
23924
25891
|
// before the identity was published (#2797 / #2925).
|
|
23925
25892
|
this.#resetHindsightConversationTrackingIfHindsight();
|
|
@@ -23931,6 +25898,11 @@ export class AgentSession {
|
|
|
23931
25898
|
previousSessionState.sessionId,
|
|
23932
25899
|
previousSessionState.sessionFile,
|
|
23933
25900
|
);
|
|
25901
|
+
exactRetirement = await this.#prepareExactMcpControlRetirement();
|
|
25902
|
+
if (exactRetirement && !exactRetirement.canCommit())
|
|
25903
|
+
throw new Error("Exact MCP control retirement changed before session switch commit");
|
|
25904
|
+
await exactRetirement?.commit();
|
|
25905
|
+
transitionCleanupCommitted = true;
|
|
23934
25906
|
let ownerShutdownSettled = true;
|
|
23935
25907
|
if (ownerShutdownManager && ownerShutdownLease && ownerId) {
|
|
23936
25908
|
try {
|
|
@@ -23959,7 +25931,18 @@ export class AgentSession {
|
|
|
23959
25931
|
);
|
|
23960
25932
|
}
|
|
23961
25933
|
}
|
|
23962
|
-
|
|
25934
|
+
// The successor identity is committed. Settle consumed tracked inputs
|
|
25935
|
+
// before reconnecting the successor or running session-switch hooks,
|
|
25936
|
+
// either of which can outlive the predecessor event bridge.
|
|
25937
|
+
this.#terminalizeQueuedSdkWorkForSessionTransition(
|
|
25938
|
+
[
|
|
25939
|
+
...previousAgentSteeringQueue,
|
|
25940
|
+
...previousAgentFollowUpQueue,
|
|
25941
|
+
...previousDeferredSdkFollowUps,
|
|
25942
|
+
...previousPendingNextTurnMessages.map(entry => entry.message),
|
|
25943
|
+
],
|
|
25944
|
+
previousTrackedQueuedInputs,
|
|
25945
|
+
);
|
|
23963
25946
|
// Different files may intentionally carry the same copied session id; pathname transition is the commit signal.
|
|
23964
25947
|
ownerShutdownTransitionCommitted = true;
|
|
23965
25948
|
if (ownerShutdownSettled) {
|
|
@@ -23982,22 +25965,39 @@ export class AgentSession {
|
|
|
23982
25965
|
// messages, model state, MCP selections, the agent subscription, and
|
|
23983
25966
|
// session-scoped tool cleanup are complete.
|
|
23984
25967
|
if (this.#extensionRunner) {
|
|
23985
|
-
await this.#
|
|
23986
|
-
|
|
23987
|
-
|
|
23988
|
-
|
|
23989
|
-
|
|
23990
|
-
|
|
25968
|
+
await this.#runCommittedSuccessorHook(() =>
|
|
25969
|
+
this.#extensionRunner!.emit({
|
|
25970
|
+
type: "session_switch",
|
|
25971
|
+
reason: "resume",
|
|
25972
|
+
previousSessionFile,
|
|
25973
|
+
...(options?.transition ? { transition: options.transition } : {}),
|
|
25974
|
+
}),
|
|
25975
|
+
);
|
|
23991
25976
|
}
|
|
23992
|
-
this.#terminalizeQueuedSdkWorkForSessionTransition(
|
|
23993
|
-
|
|
23994
|
-
|
|
23995
|
-
|
|
23996
|
-
|
|
23997
|
-
|
|
25977
|
+
this.#terminalizeQueuedSdkWorkForSessionTransition(
|
|
25978
|
+
[
|
|
25979
|
+
...previousAgentSteeringQueue,
|
|
25980
|
+
...previousAgentFollowUpQueue,
|
|
25981
|
+
...previousDeferredSdkFollowUps,
|
|
25982
|
+
...previousPendingNextTurnMessages.map(entry => entry.message),
|
|
25983
|
+
],
|
|
25984
|
+
previousTrackedQueuedInputs,
|
|
25985
|
+
);
|
|
25986
|
+
const predecessorDeferred = new Set(previousDeferredSdkFollowUps);
|
|
25987
|
+
this.#deferredSdkFollowUps = this.#deferredSdkFollowUps.filter(
|
|
25988
|
+
message => !predecessorDeferred.has(message),
|
|
25989
|
+
);
|
|
25990
|
+
if (recoveredDefaultChainMessage) this.#emitResolutionFallbackSwitch(this.#defaultFallbackChain(false));
|
|
25991
|
+
if (recoveredDefaultChainMessage) this.emitNotice("warning", recoveredDefaultChainMessage, "fallback");
|
|
23998
25992
|
return true;
|
|
23999
25993
|
} catch (error) {
|
|
24000
25994
|
if (transitionCleanupCommitted) throw error;
|
|
25995
|
+
let exactRetirementAbortError: unknown;
|
|
25996
|
+
try {
|
|
25997
|
+
await exactRetirement?.abort();
|
|
25998
|
+
} catch (abortError) {
|
|
25999
|
+
exactRetirementAbortError = abortError;
|
|
26000
|
+
}
|
|
24001
26001
|
// The switch never committed: rotate the manager's endpoint
|
|
24002
26002
|
// registration back to the predecessor before restoring it
|
|
24003
26003
|
// (review thread P1 — the map key must track the session id).
|
|
@@ -24027,9 +26027,22 @@ export class AgentSession {
|
|
|
24027
26027
|
`Session switch rollback aborted: predecessor endpoint "${previousSessionState.sessionId}" is no longer owned by this session.`,
|
|
24028
26028
|
);
|
|
24029
26029
|
}
|
|
24030
|
-
|
|
24031
|
-
|
|
26030
|
+
let sessionRestoreError: unknown;
|
|
26031
|
+
try {
|
|
26032
|
+
await this.sessionManager.restoreRollbackState(previousSessionState);
|
|
26033
|
+
} catch (restoreError) {
|
|
26034
|
+
sessionRestoreError = restoreError;
|
|
26035
|
+
}
|
|
26036
|
+
this.restoreDefaultFallbackRuntimeState(previousDefaultFallbackRuntimeState);
|
|
24032
26037
|
this.#syncAgentSessionId(previousSessionState.sessionId);
|
|
26038
|
+
if (previousModelRolesOverride === undefined) this.settings.clearOverride("modelRoles");
|
|
26039
|
+
else this.settings.override("modelRoles", previousModelRolesOverride);
|
|
26040
|
+
if (previousAgentModelOverridesOverride === undefined)
|
|
26041
|
+
this.settings.clearOverride("task.agentModelOverrides");
|
|
26042
|
+
else this.settings.override("task.agentModelOverrides", previousAgentModelOverridesOverride);
|
|
26043
|
+
this.#activeProfileInstalledRoles = new Map(previousActiveProfileInstalledRoles);
|
|
26044
|
+
this.#activeProfileInstalledAgentOverrides = new Map(previousActiveProfileInstalledAgentOverrides);
|
|
26045
|
+
this.#preProfileModel = previousPreProfileModel;
|
|
24033
26046
|
this.#activeModelProfile = previousActiveModelProfile;
|
|
24034
26047
|
this.#restoreWorkflowGateEmitter(suspendedWorkflowGateEmitter);
|
|
24035
26048
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
@@ -24067,9 +26080,8 @@ export class AgentSession {
|
|
|
24067
26080
|
this.agent.clearAllQueues();
|
|
24068
26081
|
this.agent.restoreSteering(previousAgentSteeringQueue);
|
|
24069
26082
|
this.agent.restoreFollowUp(previousAgentFollowUpQueue);
|
|
24070
|
-
|
|
24071
|
-
|
|
24072
|
-
}
|
|
26083
|
+
this.#setAgentModelWithReasoningContext(previousModel);
|
|
26084
|
+
this.#syncAppendOnlyContext(previousModel);
|
|
24073
26085
|
this.#thinkingLevelMutationRevision++;
|
|
24074
26086
|
this.#thinkingLevelLiveMutationRevision++;
|
|
24075
26087
|
this.#pendingThinkingLevelControlSuccess = undefined;
|
|
@@ -24081,8 +26093,17 @@ export class AgentSession {
|
|
|
24081
26093
|
this.agent.serviceTier = previousServiceTier;
|
|
24082
26094
|
this.#syncTodoPhasesFromBranch();
|
|
24083
26095
|
this.#reconnectToAgent();
|
|
24084
|
-
|
|
24085
|
-
|
|
26096
|
+
const rollbackErrors = [exactRetirementAbortError, sessionRestoreError, restoreMcpError].filter(
|
|
26097
|
+
value => value !== undefined,
|
|
26098
|
+
);
|
|
26099
|
+
if (rollbackErrors.length > 0) {
|
|
26100
|
+
if (rollbackErrors.length > 1) {
|
|
26101
|
+
throw new AggregateError(
|
|
26102
|
+
[error, ...rollbackErrors],
|
|
26103
|
+
"Session switch and exact MCP rollback both failed",
|
|
26104
|
+
);
|
|
26105
|
+
}
|
|
26106
|
+
throw new AggregateError([error, rollbackErrors[0]], "Session switch rollback failed");
|
|
24086
26107
|
}
|
|
24087
26108
|
if (unavailableDefaultChainMessage) {
|
|
24088
26109
|
this.emitNotice(
|
|
@@ -24120,19 +26141,19 @@ export class AgentSession {
|
|
|
24120
26141
|
}> {
|
|
24121
26142
|
this.#beginSessionTransition("branch");
|
|
24122
26143
|
try {
|
|
24123
|
-
const previousSessionFile = this.sessionFile;
|
|
24124
|
-
const previousWorkflowGateSessionId = this.sessionId;
|
|
24125
|
-
const previousSessionIdentity = this.sessionManager.getSessionId();
|
|
24126
26144
|
const selectedEntry = this.sessionManager.getEntryForFidelity(entryId);
|
|
24127
|
-
|
|
24128
26145
|
if (selectedEntry?.type !== "message" || selectedEntry.message.role !== "user") {
|
|
24129
26146
|
throw new Error("Invalid entry ID for branching");
|
|
24130
26147
|
}
|
|
26148
|
+
const previousSessionFile = this.sessionFile;
|
|
26149
|
+
const previousWorkflowGateSessionId = this.sessionId;
|
|
26150
|
+
const previousSessionIdentity = this.sessionManager.getSessionId();
|
|
24131
26151
|
|
|
24132
26152
|
const selectedText = this.#extractUserMessageText(selectedEntry.message.content);
|
|
24133
26153
|
const previousAgentSteeringQueue = this.agent.snapshotSteering();
|
|
24134
26154
|
const previousAgentFollowUpQueue = this.agent.snapshotFollowUp();
|
|
24135
26155
|
const previousDeferredSdkFollowUps = [...this.#deferredSdkFollowUps];
|
|
26156
|
+
const previousPendingNextTurnMessages = [...this.#pendingNextTurnMessages];
|
|
24136
26157
|
|
|
24137
26158
|
let skipConversationRestore = false;
|
|
24138
26159
|
|
|
@@ -24149,21 +26170,27 @@ export class AgentSession {
|
|
|
24149
26170
|
skipConversationRestore = result?.skipConversationRestore ?? false;
|
|
24150
26171
|
}
|
|
24151
26172
|
|
|
26173
|
+
this.#settleTrackedQueuedInputsBeforeAgentDisconnect();
|
|
26174
|
+
this.#disconnectFromAgent();
|
|
26175
|
+
await this.#settleActivePromptForHistoryTransition();
|
|
26176
|
+
|
|
24152
26177
|
// Flush pending writes before preparing the successor.
|
|
24153
26178
|
await this.sessionManager.flush();
|
|
24154
26179
|
const prepared = selectedEntry.parentId
|
|
24155
26180
|
? await this.sessionManager.prepareBranchedSession(selectedEntry.parentId)
|
|
24156
26181
|
: await this.sessionManager.prepareNewSession({ parentSession: previousSessionFile });
|
|
26182
|
+
let exactRetirement: PreparedExactMcpRetirement | undefined;
|
|
24157
26183
|
try {
|
|
24158
26184
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
24159
26185
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
24160
26186
|
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
26187
|
+
exactRetirement = await this.#prepareExactMcpControlRetirement();
|
|
26188
|
+
if (exactRetirement && !exactRetirement.canCommit())
|
|
26189
|
+
throw new Error("Exact MCP control retirement changed before session adoption");
|
|
24161
26190
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
24162
|
-
|
|
24163
|
-
// manager under it (review thread P1).
|
|
24164
|
-
this.#rekeyJobManagerForSessionIdentity(previousSessionIdentity, previousSessionFile);
|
|
24165
|
-
await this.#runToolSessionTransitionCleanups();
|
|
26191
|
+
await exactRetirement?.commit();
|
|
24166
26192
|
} catch (error) {
|
|
26193
|
+
await exactRetirement?.abort();
|
|
24167
26194
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
24168
26195
|
}
|
|
24169
26196
|
this.#pendingNextTurnMessages = [];
|
|
@@ -24172,6 +26199,7 @@ export class AgentSession {
|
|
|
24172
26199
|
...previousAgentSteeringQueue,
|
|
24173
26200
|
...previousAgentFollowUpQueue,
|
|
24174
26201
|
...previousDeferredSdkFollowUps,
|
|
26202
|
+
...previousPendingNextTurnMessages.map(entry => entry.message),
|
|
24175
26203
|
]);
|
|
24176
26204
|
this.#deferredSdkFollowUps = [];
|
|
24177
26205
|
this.#resetActiveSdkRunOwnership();
|
|
@@ -24190,8 +26218,6 @@ export class AgentSession {
|
|
|
24190
26218
|
// Reload messages from entries (works for both file and in-memory mode)
|
|
24191
26219
|
const sessionContext = this.buildDisplaySessionContext();
|
|
24192
26220
|
|
|
24193
|
-
await this.#restoreMCPSelectionsForSessionContext(sessionContext);
|
|
24194
|
-
|
|
24195
26221
|
if (!skipConversationRestore) {
|
|
24196
26222
|
this.agent.replaceMessages(sessionContext.messages, {
|
|
24197
26223
|
historyRewrite: { reason: "session-branch", preserveSeededPrefix: true },
|
|
@@ -24201,17 +26227,26 @@ export class AgentSession {
|
|
|
24201
26227
|
}
|
|
24202
26228
|
|
|
24203
26229
|
this.#resetIrcRosterDeliveryState();
|
|
26230
|
+
// Once committed, establish the successor's identity and prompt boundary
|
|
26231
|
+
// before fallible post-commit integrations. A cleanup or MCP failure must
|
|
26232
|
+
// not leave the next turn running with the parent's messages/session id.
|
|
26233
|
+
this.#rekeyJobManagerForSessionIdentity(previousSessionIdentity, previousSessionFile);
|
|
26234
|
+
await this.#runToolSessionTransitionCleanups();
|
|
26235
|
+
await this.#restoreMCPSelectionsForSessionContext(sessionContext);
|
|
24204
26236
|
// session_branch is the post-commit identity signal. Publish it only after
|
|
24205
26237
|
// the successor's messages and MCP selections are restored.
|
|
24206
26238
|
if (this.#extensionRunner) {
|
|
24207
|
-
await this.#
|
|
24208
|
-
|
|
24209
|
-
|
|
24210
|
-
|
|
26239
|
+
await this.#runCommittedSuccessorHook(() =>
|
|
26240
|
+
this.#extensionRunner!.emit({
|
|
26241
|
+
type: "session_branch",
|
|
26242
|
+
previousSessionFile,
|
|
26243
|
+
}),
|
|
26244
|
+
);
|
|
24211
26245
|
}
|
|
24212
26246
|
|
|
24213
26247
|
return { selectedText, cancelled: false };
|
|
24214
26248
|
} finally {
|
|
26249
|
+
this.#reconnectToAgent();
|
|
24215
26250
|
this.#endSessionTransition();
|
|
24216
26251
|
}
|
|
24217
26252
|
}
|
|
@@ -24246,22 +26281,14 @@ export class AgentSession {
|
|
|
24246
26281
|
this.#beginSessionTransition("navigate-tree");
|
|
24247
26282
|
try {
|
|
24248
26283
|
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
|
|
26284
|
+
if (targetId === oldLeafId) return { cancelled: false };
|
|
24256
26285
|
if (options.summarize && !this.model) {
|
|
24257
26286
|
throw new Error("No model available for summarization");
|
|
24258
26287
|
}
|
|
24259
|
-
|
|
24260
26288
|
const targetEntry = this.sessionManager.getEntryForFidelity(targetId);
|
|
24261
26289
|
if (!targetEntry) {
|
|
24262
26290
|
throw new Error(`Entry ${targetId} not found`);
|
|
24263
26291
|
}
|
|
24264
|
-
|
|
24265
26292
|
// Collect entries to summarize (from old leaf to common ancestor).
|
|
24266
26293
|
const { entries: collectedEntriesToSummarize, commonAncestorId } = collectEntriesForBranchSummary(
|
|
24267
26294
|
this.sessionManager,
|
|
@@ -24302,6 +26329,10 @@ export class AgentSession {
|
|
|
24302
26329
|
}
|
|
24303
26330
|
}
|
|
24304
26331
|
|
|
26332
|
+
this.#settleTrackedQueuedInputsBeforeAgentDisconnect();
|
|
26333
|
+
this.#disconnectFromAgent();
|
|
26334
|
+
await this.#settleActivePromptForHistoryTransition();
|
|
26335
|
+
|
|
24305
26336
|
// Run default summarizer if needed
|
|
24306
26337
|
let summaryText: string | undefined;
|
|
24307
26338
|
let summaryDetails: unknown;
|
|
@@ -24388,6 +26419,9 @@ export class AgentSession {
|
|
|
24388
26419
|
const queuedSdkWork = this.#queuedMessagesForSessionTransition();
|
|
24389
26420
|
this.#terminalizeQueuedSdkWorkForSessionTransition(queuedSdkWork);
|
|
24390
26421
|
this.#deferredSdkFollowUps = [];
|
|
26422
|
+
this.#pendingNextTurnMessages = [];
|
|
26423
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
26424
|
+
this.#resetActiveSdkRunOwnership();
|
|
24391
26425
|
this.agent.clearAllQueues();
|
|
24392
26426
|
this.#steeringMessages = [];
|
|
24393
26427
|
this.#followUpMessages = [];
|
|
@@ -24409,18 +26443,21 @@ export class AgentSession {
|
|
|
24409
26443
|
// the emit and the context rebuild when no handlers are registered (mirrors
|
|
24410
26444
|
// the session_before_tree guard above).
|
|
24411
26445
|
if (this.#extensionRunner?.hasHandlers("session_tree")) {
|
|
24412
|
-
await this.#
|
|
24413
|
-
|
|
24414
|
-
|
|
24415
|
-
|
|
24416
|
-
|
|
24417
|
-
|
|
24418
|
-
|
|
26446
|
+
await this.#runCommittedSuccessorHook(() =>
|
|
26447
|
+
this.#extensionRunner!.emit({
|
|
26448
|
+
type: "session_tree",
|
|
26449
|
+
newLeafId: this.sessionManager.getLeafId(),
|
|
26450
|
+
oldLeafId,
|
|
26451
|
+
summaryEntry,
|
|
26452
|
+
fromExtension: summaryText ? fromExtension : undefined,
|
|
26453
|
+
}),
|
|
26454
|
+
);
|
|
24419
26455
|
const refreshedContext = this.buildDisplaySessionContext();
|
|
24420
26456
|
return { editorText, cancelled: false, summaryEntry, sessionContext: refreshedContext };
|
|
24421
26457
|
}
|
|
24422
26458
|
return { editorText, cancelled: false, summaryEntry, sessionContext: displayContext };
|
|
24423
26459
|
} finally {
|
|
26460
|
+
this.#reconnectToAgent();
|
|
24424
26461
|
this.#endSessionTransition();
|
|
24425
26462
|
}
|
|
24426
26463
|
}
|