@gajae-code/coding-agent 0.10.2 → 0.11.0
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 +50 -2
- package/dist/types/append-only-mode.d.ts +1 -1
- package/dist/types/cli/args.d.ts +4 -5
- package/dist/types/cli/daemon-cli.d.ts +12 -1
- package/dist/types/cli/notify-cli.d.ts +21 -1
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +39 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/commands/notify.d.ts +28 -1
- package/dist/types/commands/sdk.d.ts +33 -0
- package/dist/types/config/atomic-yaml-patch.d.ts +85 -0
- package/dist/types/config/model-profile-activation.d.ts +35 -15
- package/dist/types/config/model-profiles.d.ts +11 -5
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +39 -6
- package/dist/types/config/model-selector-value.d.ts +17 -0
- package/dist/types/config/models-config-schema.d.ts +10 -6
- package/dist/types/config/settings-schema.d.ts +133 -6
- package/dist/types/config/settings.d.ts +63 -35
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -6
- package/dist/types/coordinator-mcp/server.d.ts +6 -49
- package/dist/types/daemon/builtin.d.ts +6 -5
- package/dist/types/daemon/control-types.d.ts +1 -1
- package/dist/types/discovery/mcp-json.d.ts +12 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/compact-handler.d.ts +4 -2
- package/dist/types/extensibility/extensions/get-commands-handler.d.ts +2 -2
- package/dist/types/extensibility/extensions/loader.d.ts +10 -1
- package/dist/types/extensibility/extensions/runner.d.ts +3 -3
- package/dist/types/extensibility/extensions/types.d.ts +141 -16
- package/dist/types/extensibility/hooks/types.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -2
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +3 -4
- package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +13 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -1
- package/dist/types/gjc-runtime/windows-powershell-command.d.ts +9 -0
- package/dist/types/harness-control-plane/operate.d.ts +5 -5
- package/dist/types/harness-control-plane/owner.d.ts +18 -4
- package/dist/types/harness-control-plane/receipts.d.ts +0 -1
- package/dist/types/harness-control-plane/sdk-transport.d.ts +43 -0
- package/dist/types/harness-control-plane/session-transport.d.ts +48 -0
- package/dist/types/harness-control-plane/types.d.ts +3 -4
- package/dist/types/lsp/index.d.ts +1 -1
- package/dist/types/main.d.ts +32 -38
- package/dist/types/modes/acp/acp-agent.d.ts +224 -53
- package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -0
- package/dist/types/modes/acp/acp-mode.d.ts +6 -15
- package/dist/types/modes/acp/permission-mode.d.ts +4 -0
- package/dist/types/modes/acp/startup-options.d.ts +6 -0
- package/dist/types/modes/components/agent-dashboard.d.ts +3 -0
- package/dist/types/modes/components/model-selector.d.ts +1 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +135 -0
- package/dist/types/modes/components/settings-selector.d.ts +6 -4
- package/dist/types/modes/components/welcome.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +35 -1
- package/dist/types/modes/index.d.ts +0 -4
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/runtime-init.d.ts +2 -2
- package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +6 -9
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -6
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +6 -6
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +4 -9
- package/dist/types/modes/shared/agent-wire/event-observation.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +169 -30
- package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +7 -7
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +105 -0
- package/dist/types/modes/types.d.ts +5 -0
- package/dist/types/runtime-mcp/client.d.ts +2 -0
- package/dist/types/runtime-mcp/config.d.ts +5 -1
- package/dist/types/runtime-mcp/manager.d.ts +8 -1
- package/dist/types/runtime-mcp/types.d.ts +4 -0
- package/dist/types/sdk/acp/adapter.d.ts +67 -0
- package/dist/types/sdk/acp/index.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +53 -0
- package/dist/types/sdk/broker/discovery.d.ts +30 -0
- package/dist/types/sdk/broker/ensure.d.ts +33 -0
- package/dist/types/sdk/broker/identity.d.ts +3 -0
- package/dist/types/sdk/broker/index.d.ts +8 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +95 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +85 -0
- package/dist/types/sdk/broker/process-incarnation.d.ts +14 -0
- package/dist/types/sdk/broker/runtime.d.ts +33 -0
- package/dist/types/sdk/broker/session-index.d.ts +57 -0
- package/dist/types/sdk/broker/state-version.d.ts +8 -0
- package/dist/types/sdk/broker/transport.d.ts +9 -0
- package/dist/types/sdk/bus/chat-command-policy.d.ts +49 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +21 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +111 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +77 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +84 -0
- package/dist/types/{notifications → sdk/bus}/config-commands.d.ts +7 -2
- package/dist/types/sdk/bus/config.d.ts +274 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +72 -0
- package/dist/types/sdk/bus/discord-conversation.d.ts +58 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +45 -0
- package/dist/types/sdk/bus/discord-live-provider.d.ts +70 -0
- package/dist/types/sdk/bus/discord-provider.d.ts +85 -0
- package/dist/types/{notifications → sdk/bus}/html-format.d.ts +2 -2
- package/dist/types/{notifications → sdk/bus}/index.d.ts +113 -13
- package/dist/types/{notifications → sdk/bus}/lifecycle-control-runtime.d.ts +4 -2
- package/dist/types/{notifications → sdk/bus}/lifecycle-orchestrator.d.ts +18 -21
- package/dist/types/{notifications → sdk/bus}/managed-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/native-runtime-compatibility.d.ts +13 -0
- package/dist/types/sdk/bus/notification-orchestration.d.ts +170 -0
- package/dist/types/{notifications → sdk/bus}/notification-service.d.ts +7 -1
- package/dist/types/sdk/bus/session-control.d.ts +83 -0
- package/dist/types/sdk/bus/slack-conversation.d.ts +56 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +60 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +59 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +55 -0
- package/dist/types/{notifications → sdk/bus}/telegram-cli.d.ts +4 -4
- package/dist/types/{notifications → sdk/bus}/telegram-daemon-cli.d.ts +14 -5
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -0
- package/dist/types/{notifications → sdk/bus}/telegram-daemon-control.d.ts +2 -2
- package/dist/types/{notifications → sdk/bus}/telegram-daemon.d.ts +55 -17
- package/dist/types/{notifications → sdk/bus}/telegram-reference.d.ts +2 -2
- package/dist/types/sdk/bus/telegram-setup.d.ts +128 -0
- package/dist/types/{notifications → sdk/bus}/topic-registry.d.ts +11 -0
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/session-cli.d.ts +19 -0
- package/dist/types/sdk/client/client.d.ts +1 -0
- package/dist/types/sdk/client/discovery.d.ts +27 -0
- package/dist/types/sdk/client/index.d.ts +2 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +34 -0
- package/dist/types/sdk/host/control/index.d.ts +2 -0
- package/dist/types/sdk/host/control/operations.d.ts +65 -0
- package/dist/types/sdk/host/events.d.ts +35 -0
- package/dist/types/sdk/host/host.d.ts +35 -0
- package/dist/types/sdk/host/index.d.ts +4 -0
- package/dist/types/sdk/host/query/cursor.d.ts +62 -0
- package/dist/types/sdk/host/query/handlers.d.ts +70 -0
- package/dist/types/sdk/host/query/index.d.ts +3 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +60 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +45 -0
- package/dist/types/sdk/host/types.d.ts +32 -0
- package/dist/types/sdk/index.d.ts +6 -0
- package/dist/types/sdk/lifecycle-session.d.ts +14 -0
- package/dist/types/sdk/mcp/index.d.ts +1 -0
- package/dist/types/sdk/mcp/server.d.ts +37 -0
- package/dist/types/sdk/models.d.ts +44 -0
- package/dist/types/sdk/protocol/adapter-validation.d.ts +14 -0
- package/dist/types/sdk/protocol/operation-registry.d.ts +24 -0
- package/dist/types/{sdk.d.ts → sdk/session.d.ts} +44 -38
- package/dist/types/sdk/startup-capability.d.ts +50 -0
- package/dist/types/session/agent-session.d.ts +114 -21
- package/dist/types/session/default-model-selection.d.ts +25 -0
- package/dist/types/session/fallback-chain-controller.d.ts +52 -0
- package/dist/types/session/session-manager.d.ts +58 -1
- package/dist/types/setup/credential-auto-import.d.ts +36 -6
- package/dist/types/tools/ask-answer-registry.d.ts +1 -1
- package/dist/types/tools/browser/benchmark/detector-report.d.ts +63 -0
- package/dist/types/tools/browser/benchmark/run-suite.d.ts +18 -0
- package/dist/types/tools/browser/launch.d.ts +22 -0
- package/dist/types/tools/browser/profile-discovery.d.ts +31 -0
- package/dist/types/tools/browser/profile-posture.d.ts +37 -0
- package/dist/types/tools/browser/profile-reuse.d.ts +39 -0
- package/dist/types/tools/browser/profile-warmup.d.ts +39 -0
- package/dist/types/tools/browser/registry.d.ts +19 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
- package/dist/types/tools/index.d.ts +8 -8
- package/package.json +34 -13
- package/scripts/build-sdk-package-smoke.ts +94 -0
- package/scripts/capture-notifications-settings-showcase.ts +435 -0
- package/scripts/compile-args.ts +4 -2
- package/scripts/g005-daemon-smoke.ts +4 -5
- package/scripts/g011-daemon-path-smoke.ts +5 -5
- package/scripts/generate-sdk-adapter-parity-manifest.ts +163 -0
- package/scripts/generate-sdk-operation-inventory.ts +867 -0
- package/scripts/generate-telegram-baseline-manifest.ts +128 -0
- package/scripts/manifest-schema.ts +124 -0
- package/scripts/run-test-manifest.ts +153 -0
- package/scripts/run-threat-model-check.ts +157 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +3467 -0
- package/scripts/verify-gjc-sdk-rename.ts +90 -0
- package/src/append-only-mode.ts +1 -1
- package/src/cli/args.ts +43 -12
- package/src/cli/auth-gateway-cli.ts +2 -2
- package/src/cli/config-cli.ts +2 -1
- package/src/cli/daemon-cli.ts +63 -14
- package/src/cli/list-models.ts +1 -1
- package/src/cli/notify-cli.ts +288 -286
- package/src/cli.ts +21 -2
- package/src/commands/daemon.ts +50 -6
- package/src/commands/harness.ts +93 -36
- package/src/commands/launch.ts +5 -2
- package/src/commands/mcp-serve.ts +19 -12
- package/src/commands/notify.ts +36 -3
- package/src/commands/sdk.ts +426 -0
- package/src/commit/agentic/agent.ts +1 -0
- package/src/config/atomic-yaml-patch.ts +423 -0
- package/src/config/model-profile-activation.ts +240 -89
- package/src/config/model-profiles.ts +18 -15
- package/src/config/model-registry.ts +51 -26
- package/src/config/model-resolver.ts +138 -51
- package/src/config/model-selector-value.ts +50 -0
- package/src/config/models-config-schema.ts +14 -21
- package/src/config/settings-schema.ts +128 -10
- package/src/config/settings.ts +1120 -158
- package/src/coordinator-mcp/model-preset.ts +74 -20
- package/src/coordinator-mcp/server.ts +1597 -3052
- package/src/daemon/builtin.ts +8 -5
- package/src/daemon/control-types.ts +1 -1
- package/src/defaults/gjc/skills/team/SKILL.md +4 -4
- package/src/discovery/mcp-json.ts +327 -20
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/compact-handler.ts +3 -3
- package/src/extensibility/extensions/get-commands-handler.ts +2 -2
- package/src/extensibility/extensions/loader.ts +69 -5
- package/src/extensibility/extensions/runner.ts +112 -4
- package/src/extensibility/extensions/types.ts +176 -17
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/gjc-runtime/launch-tmux.ts +170 -86
- package/src/gjc-runtime/psmux-detect.ts +82 -4
- package/src/gjc-runtime/session-state-sidecar.ts +41 -31
- package/src/gjc-runtime/team-runtime.ts +54 -35
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +53 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +1 -1
- package/src/gjc-runtime/tmux-sessions.ts +19 -13
- package/src/gjc-runtime/ultragoal-guard.ts +53 -6
- package/src/gjc-runtime/ultragoal-runtime.ts +155 -3
- package/src/gjc-runtime/windows-powershell-command.ts +58 -0
- package/src/harness-control-plane/operate.ts +9 -9
- package/src/harness-control-plane/owner.ts +283 -39
- package/src/harness-control-plane/receipts.ts +0 -1
- package/src/harness-control-plane/sdk-transport.ts +395 -0
- package/src/harness-control-plane/session-transport.ts +94 -0
- package/src/harness-control-plane/types.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +34 -35
- package/src/lsp/client.ts +42 -6
- package/src/lsp/index.ts +2 -2
- package/src/main.ts +299 -204
- package/src/modes/DESIGN.md +258 -0
- package/src/modes/acp/acp-agent.ts +1228 -2612
- package/src/modes/acp/acp-client-bridge.ts +2 -20
- package/src/modes/acp/acp-event-mapper.ts +18 -2
- package/src/modes/acp/acp-mode.ts +8 -36
- package/src/modes/acp/permission-mode.ts +25 -0
- package/src/modes/acp/startup-options.ts +6 -0
- package/src/modes/components/agent-dashboard.ts +37 -17
- package/src/modes/components/model-selector.ts +93 -35
- package/src/modes/components/notifications-settings-editor.ts +1379 -0
- package/src/modes/components/settings-selector.ts +60 -11
- package/src/modes/components/welcome.ts +4 -4
- package/src/modes/controllers/command-controller.ts +1 -2
- package/src/modes/controllers/event-controller.ts +11 -16
- package/src/modes/controllers/extension-ui-controller.ts +348 -4
- package/src/modes/controllers/selector-controller.ts +674 -36
- package/src/modes/index.ts +0 -22
- package/src/modes/interactive-mode.ts +59 -20
- package/src/modes/runtime-init.ts +298 -6
- package/src/modes/shared/agent-wire/approval-gate.ts +9 -11
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +10 -11
- package/src/modes/shared/agent-wire/event-contract.ts +7 -8
- package/src/modes/shared/agent-wire/event-envelope.ts +9 -17
- package/src/modes/shared/agent-wire/event-observation.ts +16 -7
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +1254 -87
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +15 -15
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +113 -0
- package/src/modes/types.ts +5 -0
- package/src/runtime-mcp/client.ts +94 -19
- package/src/runtime-mcp/config.ts +27 -15
- package/src/runtime-mcp/manager.ts +343 -105
- package/src/runtime-mcp/oauth-flow.ts +42 -12
- package/src/runtime-mcp/transports/http.ts +49 -19
- package/src/runtime-mcp/transports/stdio.ts +47 -28
- package/src/runtime-mcp/types.ts +10 -0
- package/src/sdk/acp/adapter.ts +418 -0
- package/src/sdk/acp/index.ts +2 -0
- package/src/sdk/broker/broker.ts +642 -0
- package/src/sdk/broker/discovery.ts +94 -0
- package/src/sdk/broker/ensure.ts +249 -0
- package/src/sdk/broker/identity.ts +42 -0
- package/src/sdk/broker/index.ts +8 -0
- package/src/sdk/broker/internal-source-marker-2178.txt +1 -0
- package/src/sdk/broker/internal-source.bunfig.toml +1 -0
- package/src/sdk/broker/lifecycle-ledger.ts +286 -0
- package/src/sdk/broker/lifecycle.ts +2157 -0
- package/src/sdk/broker/process-incarnation.ts +159 -0
- package/src/sdk/broker/runtime.ts +164 -0
- package/src/sdk/broker/session-index.ts +318 -0
- package/src/sdk/broker/state-version.ts +23 -0
- package/src/sdk/broker/transport.ts +125 -0
- package/src/sdk/bus/chat-command-policy.ts +107 -0
- package/src/sdk/bus/chat-daemon-cli.ts +214 -0
- package/src/sdk/bus/chat-daemon-control.ts +713 -0
- package/src/sdk/bus/chat-daemon-runtime.ts +526 -0
- package/src/sdk/bus/chat-effect-journal.ts +353 -0
- package/src/{notifications → sdk/bus}/config-commands.ts +39 -10
- package/src/sdk/bus/config.ts +433 -0
- package/src/sdk/bus/conversation-store.ts +391 -0
- package/src/sdk/bus/discord-conversation.ts +85 -0
- package/src/sdk/bus/discord-daemon.ts +2275 -0
- package/src/sdk/bus/discord-live-provider.ts +521 -0
- package/src/sdk/bus/discord-provider.ts +56 -0
- package/src/{notifications → sdk/bus}/html-format.ts +2 -2
- package/src/sdk/bus/index.ts +3943 -0
- package/src/{notifications → sdk/bus}/lifecycle-commands.ts +2 -0
- package/src/{notifications → sdk/bus}/lifecycle-control-runtime.ts +225 -196
- package/src/{notifications → sdk/bus}/lifecycle-orchestrator.ts +161 -74
- package/src/{notifications → sdk/bus}/managed-daemon.ts +2 -2
- package/src/sdk/bus/native-runtime-compatibility.ts +42 -0
- package/src/sdk/bus/natives-augment.d.ts +38 -0
- package/src/sdk/bus/notification-orchestration.ts +535 -0
- package/src/{notifications → sdk/bus}/notification-service.ts +106 -12
- package/src/sdk/bus/session-control.ts +333 -0
- package/src/sdk/bus/slack-conversation.ts +74 -0
- package/src/sdk/bus/slack-daemon.ts +2144 -0
- package/src/sdk/bus/slack-live-provider.ts +380 -0
- package/src/sdk/bus/slack-provider.ts +76 -0
- package/src/{notifications → sdk/bus}/telegram-cli.ts +8 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +330 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +14 -0
- package/src/{notifications → sdk/bus}/telegram-daemon-control.ts +4 -4
- package/src/{notifications → sdk/bus}/telegram-daemon.ts +502 -54
- package/src/{notifications → sdk/bus}/telegram-reference.ts +2 -2
- package/src/sdk/bus/telegram-setup.ts +663 -0
- package/src/{notifications → sdk/bus}/topic-registry.ts +32 -0
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/session-cli.ts +275 -0
- package/src/sdk/client/client.ts +1 -0
- package/src/sdk/client/discovery.ts +108 -0
- package/src/sdk/client/index.ts +2 -0
- package/src/sdk/host/control/dispatch.ts +367 -0
- package/src/sdk/host/control/index.ts +10 -0
- package/src/sdk/host/control/operations.ts +63 -0
- package/src/sdk/host/events.ts +91 -0
- package/src/sdk/host/host.ts +296 -0
- package/src/sdk/host/index.ts +4 -0
- package/src/sdk/host/query/cursor.ts +189 -0
- package/src/sdk/host/query/handlers.ts +533 -0
- package/src/sdk/host/query/index.ts +3 -0
- package/src/sdk/host/query/revision-store.ts +1126 -0
- package/src/sdk/host/reverse-leases.ts +325 -0
- package/src/sdk/host/types.ts +29 -0
- package/src/sdk/index.ts +13 -0
- package/src/sdk/lifecycle-session.ts +39 -0
- package/src/sdk/mcp/index.ts +1 -0
- package/src/sdk/mcp/server.ts +332 -0
- package/src/sdk/models.ts +168 -0
- package/src/sdk/protocol/adapter-validation.ts +48 -0
- package/src/sdk/protocol/operation-inventory.generated.json +5654 -0
- package/src/sdk/protocol/operation-registry.ts +312 -0
- package/src/{sdk.ts → sdk/session.ts} +339 -212
- package/src/sdk/startup-capability.ts +189 -0
- package/src/session/agent-session.ts +2453 -894
- package/src/session/default-model-selection.ts +93 -0
- package/src/session/fallback-chain-controller.ts +136 -0
- package/src/session/session-manager.ts +438 -11
- package/src/session/session-storage.ts +8 -1
- package/src/setup/credential-auto-import.ts +290 -35
- package/src/setup/hermes/templates/operator-instructions.v1.md +3 -5
- package/src/slash-commands/builtin-registry.ts +2 -1
- package/src/task/executor.ts +62 -13
- package/src/tools/ask-answer-registry.ts +1 -1
- package/src/tools/ask.ts +13 -8
- package/src/tools/browser/STEALTH-THIRD-PARTY-NOTICES.md +26 -0
- package/src/tools/browser/benchmark/detector-report.ts +157 -0
- package/src/tools/browser/benchmark/run-suite.ts +39 -0
- package/src/tools/browser/launch.ts +124 -19
- package/src/tools/browser/profile-discovery.ts +63 -0
- package/src/tools/browser/profile-posture.ts +59 -0
- package/src/tools/browser/profile-reuse.ts +102 -0
- package/src/tools/browser/profile-warmup.ts +109 -0
- package/src/tools/browser/registry.ts +87 -11
- package/src/tools/browser/tab-protocol.ts +1 -0
- package/src/tools/browser/tab-supervisor.ts +1 -0
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/browser.ts +6 -0
- package/src/tools/index.ts +8 -8
- package/src/tools/puppeteer/14_stealth_permissions.txt +57 -0
- package/src/tools/puppeteer/15_stealth_webrtc.txt +60 -0
- package/src/tools/telegram-send.ts +2 -2
- package/vendor/insane-search/MANIFEST.json +2 -1
- package/vendor/insane-search/engine/bias_check.py +4 -1
- package/vendor/insane-search/engine/phase0.py +26 -1
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +0 -69
- package/dist/types/modes/bridge/auth.d.ts +0 -12
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +0 -9
- package/dist/types/modes/bridge/bridge-mode.d.ts +0 -47
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +0 -88
- package/dist/types/modes/bridge/event-stream.d.ts +0 -8
- package/dist/types/modes/rpc/host-tools.d.ts +0 -1
- package/dist/types/modes/rpc/host-uris.d.ts +0 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +0 -299
- package/dist/types/modes/rpc/rpc-mode.d.ts +0 -86
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +0 -10
- package/dist/types/modes/rpc/rpc-types.d.ts +0 -810
- package/dist/types/modes/shared/agent-wire/command-contract.d.ts +0 -18
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +0 -37
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +0 -2
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +0 -56
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +0 -16
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +0 -17
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +0 -25
- package/dist/types/modes/shared/agent-wire/responses.d.ts +0 -4
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +0 -18
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +0 -25
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +0 -42
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +0 -27
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +0 -29
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +0 -68
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +0 -166
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +0 -127
- package/dist/types/notifications/config.d.ts +0 -132
- package/src/harness-control-plane/rpc-adapter.ts +0 -290
- package/src/modes/bridge/auth.ts +0 -41
- package/src/modes/bridge/bridge-client-bridge.ts +0 -47
- package/src/modes/bridge/bridge-mode.ts +0 -675
- package/src/modes/bridge/bridge-ui-context.ts +0 -200
- package/src/modes/bridge/event-stream.ts +0 -70
- package/src/modes/rpc/host-tools.ts +0 -1
- package/src/modes/rpc/host-uris.ts +0 -1
- package/src/modes/rpc/rpc-client.ts +0 -1166
- package/src/modes/rpc/rpc-mode.ts +0 -1029
- package/src/modes/rpc/rpc-socket-security.ts +0 -115
- package/src/modes/rpc/rpc-types.ts +0 -637
- package/src/modes/shared/agent-wire/command-contract.ts +0 -18
- package/src/modes/shared/agent-wire/command-dispatch.ts +0 -498
- package/src/modes/shared/agent-wire/command-validation.ts +0 -181
- package/src/modes/shared/agent-wire/handshake.ts +0 -157
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +0 -197
- package/src/modes/shared/agent-wire/host-uri-bridge.ts +0 -236
- package/src/modes/shared/agent-wire/protocol.ts +0 -46
- package/src/modes/shared/agent-wire/responses.ts +0 -17
- package/src/modes/shared/agent-wire/scopes.ts +0 -94
- package/src/modes/shared/agent-wire/session-registry.ts +0 -109
- package/src/modes/shared/agent-wire/ui-request-broker.ts +0 -150
- package/src/modes/shared/agent-wire/ui-result.ts +0 -48
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +0 -365
- package/src/modes/shared/agent-wire/unattended-audit.ts +0 -176
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +0 -426
- package/src/modes/shared/agent-wire/unattended-session.ts +0 -528
- package/src/notifications/config.ts +0 -219
- package/src/notifications/index.ts +0 -1835
- package/src/notifications/telegram-daemon-cli.ts +0 -243
- package/src/tools/puppeteer/09_stealth_locale.txt +0 -46
- /package/dist/types/{notifications → sdk/bus}/attachment-registry.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/chat-adapters.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/daemon-paths.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/engine.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/helpers.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/lifecycle-commands.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/operator-runtime.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/rate-limit-pool.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/recent-activity.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/reply-sent-store.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/rich-draft.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/rich-render.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/threaded-inbound.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/threaded-render.d.ts +0 -0
- /package/src/{notifications → sdk/bus}/attachment-registry.ts +0 -0
- /package/src/{notifications → sdk/bus}/chat-adapters.ts +0 -0
- /package/src/{notifications → sdk/bus}/daemon-paths.ts +0 -0
- /package/src/{notifications → sdk/bus}/engine.ts +0 -0
- /package/src/{notifications → sdk/bus}/helpers.ts +0 -0
- /package/src/{notifications → sdk/bus}/operator-runtime.ts +0 -0
- /package/src/{notifications → sdk/bus}/rate-limit-pool.ts +0 -0
- /package/src/{notifications → sdk/bus}/recent-activity.ts +0 -0
- /package/src/{notifications → sdk/bus}/reply-sent-store.ts +0 -0
- /package/src/{notifications → sdk/bus}/rich-draft.ts +0 -0
- /package/src/{notifications → sdk/bus}/rich-render.ts +0 -0
- /package/src/{notifications → sdk/bus}/threaded-inbound.ts +0 -0
- /package/src/{notifications → sdk/bus}/threaded-render.ts +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AgentSession - Core abstraction for agent lifecycle and session management.
|
|
3
3
|
*
|
|
4
|
-
* This class is shared
|
|
4
|
+
* This class is shared by interactive, print, ACP, and SDK-hosted session callers.
|
|
5
5
|
* It encapsulates:
|
|
6
6
|
* - Agent state access
|
|
7
7
|
* - Event subscription with automatic session persistence
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* Modes use this class and add their own I/O layer on top.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
16
17
|
import * as crypto from "node:crypto";
|
|
17
18
|
import * as fs from "node:fs";
|
|
18
19
|
import * as path from "node:path";
|
|
@@ -22,13 +23,18 @@ import {
|
|
|
22
23
|
type AfterToolCallResult,
|
|
23
24
|
type Agent,
|
|
24
25
|
AgentBusyError,
|
|
26
|
+
type AgentContext,
|
|
25
27
|
type AgentEvent,
|
|
28
|
+
type AgentLoopConfig,
|
|
26
29
|
type AgentMessage,
|
|
27
30
|
type AgentState,
|
|
28
31
|
type AgentTool,
|
|
29
32
|
assertImagePlaceholdersHavePayload,
|
|
30
33
|
canContinuePersistedHistory,
|
|
31
|
-
type
|
|
34
|
+
type ManagedAttemptContinuationOwnership,
|
|
35
|
+
type ManagedAttemptDecision,
|
|
36
|
+
type ManagedAttemptOutcome,
|
|
37
|
+
type MidRunMaintenanceOutcome,
|
|
32
38
|
resolveTelemetry,
|
|
33
39
|
type StablePrefixSnapshot,
|
|
34
40
|
ThinkingLevel,
|
|
@@ -75,20 +81,20 @@ import type {
|
|
|
75
81
|
TextContent,
|
|
76
82
|
ToolCall,
|
|
77
83
|
ToolChoice,
|
|
84
|
+
TransportFailureFacts,
|
|
78
85
|
Usage,
|
|
79
86
|
UsageReport,
|
|
80
87
|
} from "@gajae-code/ai";
|
|
81
88
|
import {
|
|
82
|
-
calculateRateLimitBackoffMs,
|
|
83
89
|
clearAnthropicFastModeFallback,
|
|
84
90
|
getSupportedEfforts,
|
|
85
91
|
isContextOverflow,
|
|
86
92
|
isUsageLimitError,
|
|
87
93
|
modelsAreEqual,
|
|
88
|
-
parseRateLimitReason,
|
|
89
94
|
resolveServiceTier,
|
|
90
95
|
streamSimple,
|
|
91
96
|
} from "@gajae-code/ai";
|
|
97
|
+
import { beginAttempt, classifyFallbackTrigger } from "@gajae-code/ai/utils/fallback-transport";
|
|
92
98
|
|
|
93
99
|
export interface ForkContextSeedMetadata {
|
|
94
100
|
sourceSessionId: string;
|
|
@@ -138,19 +144,28 @@ import { createAppendOnlyContextManager, resolveAppendOnlyMode } from "../append
|
|
|
138
144
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
139
145
|
import { reset as resetCapabilities } from "../capability";
|
|
140
146
|
import type { Rule } from "../capability/rule";
|
|
141
|
-
import {
|
|
147
|
+
import {
|
|
148
|
+
GJC_MODEL_ASSIGNMENT_TARGETS,
|
|
149
|
+
isAuthenticated,
|
|
150
|
+
MODEL_ROLE_IDS,
|
|
151
|
+
type ModelRegistry,
|
|
152
|
+
} from "../config/model-registry";
|
|
142
153
|
import {
|
|
143
154
|
extractExplicitThinkingSelector,
|
|
144
155
|
formatModelSelectorValue,
|
|
145
156
|
formatModelString,
|
|
157
|
+
managedCursorFallbackUnavailableReason,
|
|
146
158
|
parseModelString,
|
|
147
159
|
type ResolvedModelRoleValue,
|
|
160
|
+
resolveModelChainWithAuth,
|
|
148
161
|
resolveModelRoleValue,
|
|
149
162
|
type ScopedModelSelection,
|
|
150
163
|
} from "../config/model-resolver";
|
|
164
|
+
import { normalizeModelSelectorValue } from "../config/model-selector-value";
|
|
151
165
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
152
|
-
import type { Settings, SkillsSettings } from "../config/settings";
|
|
166
|
+
import type { GlobalDefaultModelRoleCommit, Settings, SkillsSettings } from "../config/settings";
|
|
153
167
|
import { onAppendOnlyModeChanged } from "../config/settings";
|
|
168
|
+
import { getDefault } from "../config/settings-schema";
|
|
154
169
|
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
155
170
|
import { loadCapability } from "../discovery";
|
|
156
171
|
import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
@@ -187,12 +202,15 @@ import type {
|
|
|
187
202
|
} from "../extensibility/extensions";
|
|
188
203
|
import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
|
|
189
204
|
import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
190
|
-
import
|
|
205
|
+
import {
|
|
206
|
+
type LoadedSubskillActivation,
|
|
207
|
+
resolveSubskillActivationForSkillInvocation,
|
|
208
|
+
} from "../extensibility/gjc-plugins";
|
|
191
209
|
import { resolveCurrentPhaseForParent } from "../extensibility/gjc-plugins/injection";
|
|
192
210
|
import { readActiveSubskillsForParent, toActiveSubskillEntry } from "../extensibility/gjc-plugins/state";
|
|
193
211
|
import { loadActiveSubskillTools } from "../extensibility/gjc-plugins/tools";
|
|
194
212
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
195
|
-
import type
|
|
213
|
+
import { buildSkillPromptMessage, type Skill, type SkillWarning } from "../extensibility/skills";
|
|
196
214
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
197
215
|
import { buildGjcRuntimeSessionEnv, consumePendingGoalModeRequest } from "../gjc-runtime/goal-mode-request";
|
|
198
216
|
import {
|
|
@@ -213,7 +231,12 @@ import { ensureWorkflowSkillActivationState } from "../hooks/skill-state";
|
|
|
213
231
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
214
232
|
import { shutdownAll as shutdownAllLspClients } from "../lsp/client";
|
|
215
233
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
216
|
-
import
|
|
234
|
+
import {
|
|
235
|
+
BrokerWorkflowGateEmitter,
|
|
236
|
+
FileGateStore,
|
|
237
|
+
MemoryGateStore,
|
|
238
|
+
type WorkflowGateEmitter,
|
|
239
|
+
} from "../modes/shared/agent-wire/workflow-gate-broker";
|
|
217
240
|
import { getCurrentThemeName, theme } from "../modes/theme/theme";
|
|
218
241
|
import type { PlanModeState } from "../plan-mode/state";
|
|
219
242
|
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
|
@@ -238,6 +261,7 @@ import {
|
|
|
238
261
|
selectDiscoverableMCPToolNamesByServer,
|
|
239
262
|
} from "../runtime-mcp/discoverable-tool-metadata";
|
|
240
263
|
import { MCPManager } from "../runtime-mcp/manager";
|
|
264
|
+
import type { NotificationSessionController } from "../sdk/bus/session-control";
|
|
241
265
|
import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
|
|
242
266
|
import { formatNoCredentialOnboardingError, formatNoModelOnboardingError } from "../setup/model-onboarding-guidance";
|
|
243
267
|
import {
|
|
@@ -279,7 +303,26 @@ import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-
|
|
|
279
303
|
import { buildWorkflowIntentDiff, WORKFLOW_INTENT_DIFF_CUSTOM_TYPE } from "../workflow/workflow-intent-diff";
|
|
280
304
|
import { buildWorkspaceTree, type WorkspaceTree } from "../workspace-tree";
|
|
281
305
|
import type { AuthStorage } from "./auth-storage";
|
|
282
|
-
import
|
|
306
|
+
import {
|
|
307
|
+
DefaultModelSelectionRecoveryError,
|
|
308
|
+
type DefaultModelSelectionResult,
|
|
309
|
+
type DefaultModelSelectionRollbackStage,
|
|
310
|
+
} from "./default-model-selection";
|
|
311
|
+
import {
|
|
312
|
+
type ConfiguredFallbackChain,
|
|
313
|
+
effectiveFallbackDelay,
|
|
314
|
+
FallbackChainController,
|
|
315
|
+
type FallbackTriggerClass,
|
|
316
|
+
} from "./fallback-chain-controller";
|
|
317
|
+
|
|
318
|
+
export { DefaultModelSelectionRecoveryError } from "./default-model-selection";
|
|
319
|
+
|
|
320
|
+
import type {
|
|
321
|
+
ClientBridge,
|
|
322
|
+
ClientBridgePermissionOption,
|
|
323
|
+
ClientBridgePermissionOutcome,
|
|
324
|
+
ClientBridgePermissionToolCall,
|
|
325
|
+
} from "./client-bridge";
|
|
283
326
|
import { computeNonMessageTokens } from "./context-estimation";
|
|
284
327
|
import {
|
|
285
328
|
type ContributionPrepOptions,
|
|
@@ -303,6 +346,7 @@ import { formatSessionDumpText } from "./session-dump-format";
|
|
|
303
346
|
import type {
|
|
304
347
|
BranchSummaryEntry,
|
|
305
348
|
CompactionEntry,
|
|
349
|
+
DefaultModelSelectionStage,
|
|
306
350
|
NewSessionOptions,
|
|
307
351
|
SessionContext,
|
|
308
352
|
SessionManager,
|
|
@@ -342,8 +386,18 @@ export type AgentSessionEvent =
|
|
|
342
386
|
unbounded?: boolean;
|
|
343
387
|
}
|
|
344
388
|
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
|
|
345
|
-
| {
|
|
346
|
-
|
|
389
|
+
| {
|
|
390
|
+
type: "model_fallback_switched";
|
|
391
|
+
eventId: string;
|
|
392
|
+
from: string;
|
|
393
|
+
to: string;
|
|
394
|
+
reason: string;
|
|
395
|
+
role: string;
|
|
396
|
+
scope: string;
|
|
397
|
+
activeIndex: number;
|
|
398
|
+
chainLength: number;
|
|
399
|
+
attemptsUsed: number;
|
|
400
|
+
}
|
|
347
401
|
| { type: "ttsr_triggered"; rules: Rule[] }
|
|
348
402
|
| { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
|
|
349
403
|
| { type: "todo_auto_clear" }
|
|
@@ -384,6 +438,8 @@ export interface AgentSessionConfig {
|
|
|
384
438
|
agent: Agent;
|
|
385
439
|
sessionManager: SessionManager;
|
|
386
440
|
settings: Settings;
|
|
441
|
+
/** Shared Gate-A-eligible notification session controller, when this host supports it. */
|
|
442
|
+
notificationSessionController?: NotificationSessionController;
|
|
387
443
|
/** Models to cycle through with Alt+N (from --models flag) */
|
|
388
444
|
scopedModels?: ScopedModelSelection[];
|
|
389
445
|
/** Initial session thinking selector. */
|
|
@@ -394,6 +450,7 @@ export interface AgentSessionConfig {
|
|
|
394
450
|
slashCommands?: FileSlashCommand[];
|
|
395
451
|
/** Extension runner (created in main.ts with wrapped tools) */
|
|
396
452
|
extensionRunner?: ExtensionRunner;
|
|
453
|
+
|
|
397
454
|
/** Loaded skills (already discovered by SDK) */
|
|
398
455
|
skills?: Skill[];
|
|
399
456
|
/** Skill loading warnings (already captured by SDK) */
|
|
@@ -422,7 +479,11 @@ export interface AgentSessionConfig {
|
|
|
422
479
|
/** Current session message-to-LLM conversion pipeline */
|
|
423
480
|
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
424
481
|
/** System prompt builder that can consider tool availability. Returns ordered provider-facing blocks. */
|
|
425
|
-
rebuildSystemPrompt?: (
|
|
482
|
+
rebuildSystemPrompt?: (
|
|
483
|
+
toolNames: string[],
|
|
484
|
+
tools: Map<string, AgentTool>,
|
|
485
|
+
candidateModel?: Model,
|
|
486
|
+
) => Promise<{ systemPrompt: string[] }>;
|
|
426
487
|
/** Initial workspace tree snapshot used for the first volatile per-turn context message. */
|
|
427
488
|
workspaceTree?: WorkspaceTree;
|
|
428
489
|
/** Rebuild the SSH tool from current capability discovery results. */
|
|
@@ -487,6 +548,14 @@ export interface AgentSessionConfig {
|
|
|
487
548
|
providerCacheSessionId?: string;
|
|
488
549
|
}
|
|
489
550
|
|
|
551
|
+
type MidRunMaintenanceLifecycle = Parameters<NonNullable<AgentLoopConfig["maintainContext"]>>[1];
|
|
552
|
+
|
|
553
|
+
type AutoCompactionTerminalStatus =
|
|
554
|
+
| { kind: "compacted" }
|
|
555
|
+
| { kind: "aborted"; source: "signal" | "hook" }
|
|
556
|
+
| { kind: "skipped" }
|
|
557
|
+
| { kind: "failed" };
|
|
558
|
+
|
|
490
559
|
/** Options for AgentSession.prompt() */
|
|
491
560
|
export interface PromptOptions {
|
|
492
561
|
/** Whether to expand file-based prompt templates (default: true) */
|
|
@@ -505,6 +574,21 @@ export interface PromptOptions {
|
|
|
505
574
|
attribution?: MessageAttribution;
|
|
506
575
|
/** Skip pre-send compaction checks for this prompt (internal use for maintenance flows). */
|
|
507
576
|
skipCompactionCheck?: boolean;
|
|
577
|
+
/**
|
|
578
|
+
* Invoked after all prompt preflight checks pass and immediately before agent execution begins.
|
|
579
|
+
* Cancellation before this callback rejects the prompt.
|
|
580
|
+
*/
|
|
581
|
+
onPreflightAccepted?: () => void;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function promptPreflightCancelledError(): Error {
|
|
585
|
+
const error = Object.assign(new Error("Prompt preflight was cancelled before execution."), { code: "busy" });
|
|
586
|
+
error.name = "PromptPreflightCancelledError";
|
|
587
|
+
return error;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function isPromptPreflightCancelledError(error: unknown): boolean {
|
|
591
|
+
return error instanceof Error && error.name === "PromptPreflightCancelledError";
|
|
508
592
|
}
|
|
509
593
|
|
|
510
594
|
/** Result from a handoff operation. */
|
|
@@ -526,6 +610,37 @@ export interface ModelCycleResult {
|
|
|
526
610
|
isScoped: boolean;
|
|
527
611
|
}
|
|
528
612
|
|
|
613
|
+
export type ModelChangeCause =
|
|
614
|
+
| "user-selection"
|
|
615
|
+
| "profile-activation"
|
|
616
|
+
| "fallback-switch"
|
|
617
|
+
| "restore"
|
|
618
|
+
| "rollback"
|
|
619
|
+
| "startup-override"
|
|
620
|
+
| "temporary-operation";
|
|
621
|
+
|
|
622
|
+
export type TemporaryModelReason =
|
|
623
|
+
| "plan-mode"
|
|
624
|
+
| "context-promotion"
|
|
625
|
+
| "temporary-cycle"
|
|
626
|
+
| "profile-preview"
|
|
627
|
+
| "extension-temporary"
|
|
628
|
+
| "other";
|
|
629
|
+
|
|
630
|
+
/** Opaque handle for a non-destructive temporary provider-session scope. */
|
|
631
|
+
export interface TemporaryProviderSessionScope {
|
|
632
|
+
readonly reason: TemporaryModelReason;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
interface TemporaryProviderSessionScopeRecord {
|
|
636
|
+
token: TemporaryProviderSessionScope;
|
|
637
|
+
autoOwned: boolean;
|
|
638
|
+
model: Model | undefined;
|
|
639
|
+
thinkingLevel: ThinkingLevel | undefined;
|
|
640
|
+
fallbackController: FallbackChainController | undefined;
|
|
641
|
+
providerSessionState: Map<string, ProviderSessionState>;
|
|
642
|
+
}
|
|
643
|
+
|
|
529
644
|
/** Result from cycleRoleModels() */
|
|
530
645
|
export interface RoleModelCycleResult {
|
|
531
646
|
model: Model;
|
|
@@ -533,12 +648,6 @@ export interface RoleModelCycleResult {
|
|
|
533
648
|
role: string;
|
|
534
649
|
}
|
|
535
650
|
|
|
536
|
-
export interface DefaultModelSelectionResult {
|
|
537
|
-
readonly provider: string;
|
|
538
|
-
readonly modelId: string;
|
|
539
|
-
readonly thinkingLevel: ResolvedThinkingLevel;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
651
|
/** Session statistics for /session command */
|
|
543
652
|
export interface SessionStats {
|
|
544
653
|
sessionFile: string | undefined;
|
|
@@ -567,9 +676,6 @@ export interface SessionStats {
|
|
|
567
676
|
|
|
568
677
|
/** Standard thinking levels */
|
|
569
678
|
|
|
570
|
-
type RetryFallbackChains = Record<string, string[]>;
|
|
571
|
-
|
|
572
|
-
type RetryFallbackRevertPolicy = "never" | "cooldown-expiry";
|
|
573
679
|
type RetryErrorClassification =
|
|
574
680
|
| "none"
|
|
575
681
|
| "overflow"
|
|
@@ -580,42 +686,6 @@ type RetryErrorClassification =
|
|
|
580
686
|
| "local_unavailable"
|
|
581
687
|
| "unknown";
|
|
582
688
|
|
|
583
|
-
interface RetryFallbackSelector {
|
|
584
|
-
raw: string;
|
|
585
|
-
provider: string;
|
|
586
|
-
id: string;
|
|
587
|
-
thinkingLevel: ThinkingLevel | undefined;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
interface ActiveRetryFallbackState {
|
|
591
|
-
role: string;
|
|
592
|
-
originalSelector: string;
|
|
593
|
-
originalThinkingLevel: ThinkingLevel | undefined;
|
|
594
|
-
lastAppliedFallbackThinkingLevel: ThinkingLevel | undefined;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function parseRetryFallbackSelector(selector: string): RetryFallbackSelector | undefined {
|
|
598
|
-
const trimmed = selector.trim();
|
|
599
|
-
if (!trimmed) return undefined;
|
|
600
|
-
const parsed = parseModelString(trimmed);
|
|
601
|
-
if (!parsed) return undefined;
|
|
602
|
-
return {
|
|
603
|
-
raw: trimmed,
|
|
604
|
-
provider: parsed.provider,
|
|
605
|
-
id: parsed.id,
|
|
606
|
-
thinkingLevel: parsed.thinkingLevel,
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function formatRetryFallbackSelector(model: Model, thinkingLevel: ThinkingLevel | undefined): string {
|
|
611
|
-
const selector = formatModelString(model);
|
|
612
|
-
return thinkingLevel ? `${selector}:${thinkingLevel}` : selector;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): string {
|
|
616
|
-
return `${selector.provider}/${selector.id}`;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
689
|
function isLocalModelEndpoint(model: Model | undefined): boolean {
|
|
620
690
|
if (!model) return false;
|
|
621
691
|
if (model.provider === "ollama" || model.provider === "lm-studio" || model.provider === "llama.cpp") {
|
|
@@ -1175,10 +1245,24 @@ export class StreamingEditFileCache {
|
|
|
1175
1245
|
return this.#totalBytes;
|
|
1176
1246
|
}
|
|
1177
1247
|
}
|
|
1248
|
+
type SessionAdmissionKind = "prompt" | "selection";
|
|
1249
|
+
|
|
1250
|
+
type SessionAdmissionEntry = {
|
|
1251
|
+
kind: SessionAdmissionKind;
|
|
1252
|
+
ready: PromiseWithResolvers<void>;
|
|
1253
|
+
settled: PromiseWithResolvers<void>;
|
|
1254
|
+
released: boolean;
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
type SessionAdmissionLease = {
|
|
1258
|
+
release(): void;
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1178
1261
|
export class AgentSession {
|
|
1179
1262
|
readonly agent: Agent;
|
|
1180
1263
|
readonly sessionManager: SessionManager;
|
|
1181
1264
|
readonly settings: Settings;
|
|
1265
|
+
readonly notificationSessionController: NotificationSessionController | undefined;
|
|
1182
1266
|
readonly taskDepth: number;
|
|
1183
1267
|
readonly yieldQueue: YieldQueue;
|
|
1184
1268
|
|
|
@@ -1189,7 +1273,11 @@ export class AgentSession {
|
|
|
1189
1273
|
#scopedModels: ScopedModelSelection[];
|
|
1190
1274
|
#thinkingLevel: ThinkingLevel | undefined;
|
|
1191
1275
|
#activeModelProfile: string | undefined;
|
|
1192
|
-
#
|
|
1276
|
+
#sessionAdmissionQueue: SessionAdmissionEntry[] = [];
|
|
1277
|
+
#activeSessionAdmission: SessionAdmissionEntry | undefined;
|
|
1278
|
+
#sessionAdmissionClosed = false;
|
|
1279
|
+
#sessionAdmissionContext = new AsyncLocalStorage<SessionAdmissionEntry>();
|
|
1280
|
+
#defaultModelSelectionMutationRevision = 0;
|
|
1193
1281
|
#promptTemplates: PromptTemplate[];
|
|
1194
1282
|
#slashCommands: FileSlashCommand[];
|
|
1195
1283
|
|
|
@@ -1200,6 +1288,8 @@ export class AgentSession {
|
|
|
1200
1288
|
#lastAppendOnlyResolution?: { enable: boolean; providerId: string | undefined };
|
|
1201
1289
|
#eventListeners: AgentSessionEventListener[] = [];
|
|
1202
1290
|
#eventListenerSnapshot: readonly AgentSessionEventListener[] = Object.freeze([]);
|
|
1291
|
+
/** Resolution-time switches that occur before a consumer can subscribe. */
|
|
1292
|
+
#pendingFallbackSwitches: Extract<AgentSessionEvent, { type: "model_fallback_switched" }>[] = [];
|
|
1203
1293
|
|
|
1204
1294
|
#rebuildEventListenerSnapshot(): void {
|
|
1205
1295
|
this.#eventListenerSnapshot = Object.freeze([...this.#eventListeners]);
|
|
@@ -1233,12 +1323,36 @@ export class AgentSession {
|
|
|
1233
1323
|
#allowAcpAgentInitiatedTurns = false;
|
|
1234
1324
|
/** Per-session memory of allow_always / reject_always decisions for gated tools. */
|
|
1235
1325
|
#acpPermissionDecisions: Map<string, "allow_always" | "reject_always"> = new Map();
|
|
1326
|
+
/** SDK-controlled permission policy applied before ACP client prompting. Defaults to `allow` so callers
|
|
1327
|
+
* without a reverse permission provider (TUI, print/headless) run guarded tools; ACP/SDK set this explicitly. */
|
|
1328
|
+
#sdkPermissionMode: "prompt" | "allow" | "deny" = "allow";
|
|
1329
|
+
/** Permission provider registered by a live SDK reverse lease. */
|
|
1330
|
+
#sdkPermissionProvider:
|
|
1331
|
+
| ((
|
|
1332
|
+
toolCall: ClientBridgePermissionToolCall,
|
|
1333
|
+
options: ClientBridgePermissionOption[],
|
|
1334
|
+
signal?: AbortSignal,
|
|
1335
|
+
) => Promise<ClientBridgePermissionOutcome>)
|
|
1336
|
+
| undefined;
|
|
1337
|
+
|
|
1236
1338
|
#guardedToolWrapperCache = new WeakMap<AgentTool, Map<string, AgentTool>>();
|
|
1237
1339
|
#acpPermissionWrapperVersion = 0;
|
|
1238
1340
|
|
|
1239
1341
|
// Compaction state
|
|
1240
1342
|
#compactionAbortController: AbortController | undefined = undefined;
|
|
1241
1343
|
#autoCompactionAbortController: AbortController | undefined = undefined;
|
|
1344
|
+
|
|
1345
|
+
/** Invocation-scoped EventStream drain barriers owned by active maintenance calls. */
|
|
1346
|
+
#activeMidRunBarrierControllers = new Set<AbortController>();
|
|
1347
|
+
/** Maintenance invocations that must settle before resources are torn down. */
|
|
1348
|
+
#activeMidRunMaintenancePromises = new Set<Promise<MidRunMaintenanceOutcome>>();
|
|
1349
|
+
// Anti-loop guard (#1662): signature of the assistant response that last
|
|
1350
|
+
// anchored a mid-run maintenance attempt. A given provider response drives at
|
|
1351
|
+
// most one attempt, so a compaction that cannot shrink further can't wedge the
|
|
1352
|
+
// loop into interrupt → resume → interrupt; a NEW response (fresh signature) is
|
|
1353
|
+
// required to re-trigger. A content signature (not object identity) is used so
|
|
1354
|
+
// it survives the message-array rebuild that compaction/prune perform.
|
|
1355
|
+
#lastMidRunMaintenanceAnchorSignature: string | undefined = undefined;
|
|
1242
1356
|
#resourceSampler: () => EmergencyCompactionSample = () => this.#defaultResourceSample();
|
|
1243
1357
|
#retainedMemorySampler: (() => RetainedMemorySample) | undefined;
|
|
1244
1358
|
#prePromptContextCheckPromise: Promise<void> | undefined = undefined;
|
|
@@ -1261,7 +1375,9 @@ export class AgentSession {
|
|
|
1261
1375
|
#retryAttempt = 0;
|
|
1262
1376
|
#retryPromise: Promise<void> | undefined = undefined;
|
|
1263
1377
|
#retryResolve: (() => void) | undefined = undefined;
|
|
1264
|
-
#
|
|
1378
|
+
#defaultFallbackController: FallbackChainController | undefined;
|
|
1379
|
+
#defaultFallbackExhaustedLastTurn = false;
|
|
1380
|
+
#fallbackInvocationId = 0;
|
|
1265
1381
|
// Todo completion reminder state
|
|
1266
1382
|
#todoReminderCount = 0;
|
|
1267
1383
|
#lastGoalReminderAssistantTimestamp: number | undefined = undefined;
|
|
@@ -1302,8 +1418,10 @@ export class AgentSession {
|
|
|
1302
1418
|
#providerSessionId: string | undefined;
|
|
1303
1419
|
#providerCacheSessionId: string | undefined;
|
|
1304
1420
|
#isDisposed = false;
|
|
1421
|
+
#disposePromise: Promise<void> | undefined;
|
|
1305
1422
|
// Extension system
|
|
1306
1423
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
1424
|
+
|
|
1307
1425
|
#turnIndex = 0;
|
|
1308
1426
|
#workerIntegrationScheduler = new WorkerIntegrationRequestScheduler(async () => {
|
|
1309
1427
|
await requestGjcWorkerIntegrationAttempt(this.sessionManager.getCwd(), process.env).catch(error => {
|
|
@@ -1336,7 +1454,11 @@ export class AgentSession {
|
|
|
1336
1454
|
#onSseEvent: SimpleStreamOptions["onSseEvent"] | undefined;
|
|
1337
1455
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
1338
1456
|
#rebuildSystemPrompt:
|
|
1339
|
-
| ((
|
|
1457
|
+
| ((
|
|
1458
|
+
toolNames: string[],
|
|
1459
|
+
tools: Map<string, AgentTool>,
|
|
1460
|
+
candidateModel?: Model,
|
|
1461
|
+
) => Promise<{ systemPrompt: string[] }>)
|
|
1340
1462
|
| undefined;
|
|
1341
1463
|
#getMcpServerInstructions: (() => Map<string, string> | undefined) | undefined;
|
|
1342
1464
|
#reloadSshTool: (() => Promise<AgentTool | null>) | undefined;
|
|
@@ -1353,6 +1475,9 @@ export class AgentSession {
|
|
|
1353
1475
|
* the dominant cause of prompt-cache invalidation in long sessions.
|
|
1354
1476
|
*/
|
|
1355
1477
|
#lastAppliedToolSignature: string | undefined;
|
|
1478
|
+
#pendingAppliedToolSignature: string | undefined;
|
|
1479
|
+
#baseSystemPromptGeneration = 0;
|
|
1480
|
+
#pendingBaseSystemPromptRebuilds = new Set<Promise<void>>();
|
|
1356
1481
|
#mcpDiscoveryEnabled = false;
|
|
1357
1482
|
#discoverableMCPTools = new Map<string, DiscoverableMCPTool>();
|
|
1358
1483
|
#discoverableMCPSearchIndex: DiscoverableMCPSearchIndex | null = null;
|
|
@@ -1361,7 +1486,6 @@ export class AgentSession {
|
|
|
1361
1486
|
#discoverableToolSearchIndex: DiscoverableToolSearchIndex | null = null;
|
|
1362
1487
|
#selectedDiscoveredToolNames = new Set<string>();
|
|
1363
1488
|
#discoverableToolAllowedNames: ReadonlySet<string> | undefined;
|
|
1364
|
-
#rpcHostToolNames = new Set<string>();
|
|
1365
1489
|
#gjcSubskillToolNames = new Set<string>();
|
|
1366
1490
|
#gjcSubskillToolSignature: string | undefined;
|
|
1367
1491
|
#defaultSelectedMCPServerNames = new Set<string>();
|
|
@@ -1431,15 +1555,17 @@ export class AgentSession {
|
|
|
1431
1555
|
nonEditDeterminations: 0,
|
|
1432
1556
|
};
|
|
1433
1557
|
#promptInFlightCount = 0;
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
//
|
|
1437
|
-
//
|
|
1438
|
-
//
|
|
1439
|
-
// this, a client that resumes on `agent_end` can fire its next `prompt` while
|
|
1440
|
-
// the session still reports busy and hit AgentBusyError. Flushed from both
|
|
1441
|
-
// #endInFlight (normal) and #resetInFlight (abort).
|
|
1558
|
+
#agentEventHandlersInFlight = 0;
|
|
1559
|
+
#queuedExtensionEventCount = 0;
|
|
1560
|
+
// Wire-level agent_end emission is deferred until both the prompt finalizer and
|
|
1561
|
+
// async event handlers settle. Subscribers treat agent_end as readiness, so
|
|
1562
|
+
// publishing it earlier lets a successor corrupt the prior prompt's lifecycle.
|
|
1442
1563
|
#pendingAgentEndEmit: AgentSessionEvent | undefined;
|
|
1564
|
+
// A scheduled continuation owns this terminal boundary until it either starts
|
|
1565
|
+
// the successor or proves it cannot. Holds prevent a false idle event while
|
|
1566
|
+
// preserving the predecessor for cancellation and preflight failures.
|
|
1567
|
+
#pendingAgentEndContinuationHolds = new Map<symbol, AgentSessionEvent>();
|
|
1568
|
+
|
|
1443
1569
|
#obfuscator: SecretObfuscator | undefined;
|
|
1444
1570
|
#checkpointState: CheckpointState | undefined = undefined;
|
|
1445
1571
|
#providerReplaySourceCache = new WeakMap<AgentMessage, ProviderReplaySourceCacheEntry>();
|
|
@@ -1448,7 +1574,11 @@ export class AgentSession {
|
|
|
1448
1574
|
#pendingRewindReport: string | undefined = undefined;
|
|
1449
1575
|
#lastSuccessfulYieldToolCallId: string | undefined = undefined;
|
|
1450
1576
|
#promptGeneration = 0;
|
|
1577
|
+
#promptPreflightAbortController = new AbortController();
|
|
1578
|
+
|
|
1451
1579
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
1580
|
+
#temporaryProviderSessionScopes: TemporaryProviderSessionScopeRecord[] = [];
|
|
1581
|
+
|
|
1452
1582
|
/**
|
|
1453
1583
|
* Provider keys for which the Anthropic fast-mode auto-fallback fired this
|
|
1454
1584
|
* session (the provider rejected `speed:"fast"` and we retried without it).
|
|
@@ -1495,6 +1625,75 @@ export class AgentSession {
|
|
|
1495
1625
|
}
|
|
1496
1626
|
}
|
|
1497
1627
|
|
|
1628
|
+
#sessionAdmissionBusyError(): AgentBusyError {
|
|
1629
|
+
return Object.assign(new AgentBusyError("Agent session admission is busy due to same-session reentrancy."), {
|
|
1630
|
+
code: "busy",
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
#activateNextSessionAdmission(): void {
|
|
1635
|
+
if (this.#activeSessionAdmission || this.#sessionAdmissionClosed) return;
|
|
1636
|
+
const next = this.#sessionAdmissionQueue.shift();
|
|
1637
|
+
if (!next) return;
|
|
1638
|
+
this.#activeSessionAdmission = next;
|
|
1639
|
+
next.ready.resolve();
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
async #withSessionAdmission<T>(
|
|
1643
|
+
kind: SessionAdmissionKind,
|
|
1644
|
+
body: (lease: SessionAdmissionLease) => Promise<T>,
|
|
1645
|
+
): Promise<T> {
|
|
1646
|
+
const owner = this.#sessionAdmissionContext.getStore();
|
|
1647
|
+
if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
|
|
1648
|
+
if (this.#sessionAdmissionClosed || this.#isDisposed) throw this.#sessionAdmissionBusyError();
|
|
1649
|
+
|
|
1650
|
+
const entry: SessionAdmissionEntry = {
|
|
1651
|
+
kind,
|
|
1652
|
+
ready: Promise.withResolvers<void>(),
|
|
1653
|
+
settled: Promise.withResolvers<void>(),
|
|
1654
|
+
released: false,
|
|
1655
|
+
};
|
|
1656
|
+
this.#sessionAdmissionQueue.push(entry);
|
|
1657
|
+
this.#activateNextSessionAdmission();
|
|
1658
|
+
await entry.ready.promise;
|
|
1659
|
+
if (this.#sessionAdmissionClosed || this.#isDisposed) {
|
|
1660
|
+
entry.released = true;
|
|
1661
|
+
entry.settled.resolve();
|
|
1662
|
+
if (this.#activeSessionAdmission === entry) this.#activeSessionAdmission = undefined;
|
|
1663
|
+
this.#activateNextSessionAdmission();
|
|
1664
|
+
throw this.#sessionAdmissionBusyError();
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
const release = () => {
|
|
1668
|
+
if (entry.released) return;
|
|
1669
|
+
entry.released = true;
|
|
1670
|
+
entry.settled.resolve();
|
|
1671
|
+
if (this.#activeSessionAdmission === entry) this.#activeSessionAdmission = undefined;
|
|
1672
|
+
this.#activateNextSessionAdmission();
|
|
1673
|
+
};
|
|
1674
|
+
try {
|
|
1675
|
+
return await this.#sessionAdmissionContext.run(entry, () => body({ release }));
|
|
1676
|
+
} finally {
|
|
1677
|
+
release();
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
async #closeSessionAdmission(): Promise<void> {
|
|
1682
|
+
this.#sessionAdmissionClosed = true;
|
|
1683
|
+
const queued = this.#sessionAdmissionQueue.splice(0);
|
|
1684
|
+
for (const entry of queued) {
|
|
1685
|
+
entry.released = true;
|
|
1686
|
+
entry.ready.reject(this.#sessionAdmissionBusyError());
|
|
1687
|
+
entry.settled.resolve();
|
|
1688
|
+
}
|
|
1689
|
+
const active = this.#activeSessionAdmission;
|
|
1690
|
+
if (active?.kind === "prompt") {
|
|
1691
|
+
this.#promptGeneration++;
|
|
1692
|
+
this.#promptPreflightAbortController.abort();
|
|
1693
|
+
}
|
|
1694
|
+
if (active) await active.settled.promise;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1498
1697
|
#beginInFlight(): void {
|
|
1499
1698
|
this.#promptInFlightCount++;
|
|
1500
1699
|
if (this.#promptInFlightCount === 1) {
|
|
@@ -1502,6 +1701,75 @@ export class AgentSession {
|
|
|
1502
1701
|
}
|
|
1503
1702
|
}
|
|
1504
1703
|
|
|
1704
|
+
#isPromptPreflightCancelled(generation: number, signal: AbortSignal): boolean {
|
|
1705
|
+
return signal.aborted || this.#promptGeneration !== generation;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
#throwIfPromptPreflightCancelled(generation: number, signal: AbortSignal): void {
|
|
1709
|
+
if (this.#isPromptPreflightCancelled(generation, signal)) {
|
|
1710
|
+
throw promptPreflightCancelledError();
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
async #awaitPromptPreflight<T>(generation: number, signal: AbortSignal, pending: Promise<T>): Promise<T> {
|
|
1715
|
+
this.#throwIfPromptPreflightCancelled(generation, signal);
|
|
1716
|
+
const cancellation = Promise.withResolvers<never>();
|
|
1717
|
+
const cancel = () => cancellation.reject(promptPreflightCancelledError());
|
|
1718
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
1719
|
+
try {
|
|
1720
|
+
const result = await Promise.race([pending, cancellation.promise]);
|
|
1721
|
+
this.#throwIfPromptPreflightCancelled(generation, signal);
|
|
1722
|
+
return result;
|
|
1723
|
+
} finally {
|
|
1724
|
+
signal.removeEventListener("abort", cancel);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
#reserveDeferredAgentEndForContinuation(): symbol | undefined {
|
|
1729
|
+
const pending = this.#pendingAgentEndEmit;
|
|
1730
|
+
if (!pending) return undefined;
|
|
1731
|
+
const hold = Symbol("deferred-agent-end-continuation");
|
|
1732
|
+
this.#pendingAgentEndContinuationHolds.set(hold, pending);
|
|
1733
|
+
return hold;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
#claimDeferredAgentEndForContinuation(hold: symbol | undefined): AgentSessionEvent | undefined {
|
|
1737
|
+
if (!hold) return undefined;
|
|
1738
|
+
const pending = this.#pendingAgentEndContinuationHolds.get(hold);
|
|
1739
|
+
this.#pendingAgentEndContinuationHolds.delete(hold);
|
|
1740
|
+
if (pending && this.#pendingAgentEndEmit === pending) {
|
|
1741
|
+
this.#pendingAgentEndEmit = undefined;
|
|
1742
|
+
for (const [candidate, candidatePending] of this.#pendingAgentEndContinuationHolds) {
|
|
1743
|
+
if (candidatePending === pending) this.#pendingAgentEndContinuationHolds.delete(candidate);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return pending;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
#restoreDeferredAgentEndAfterContinuationFailure(pending: AgentSessionEvent | undefined): void {
|
|
1750
|
+
if (pending && !this.#pendingAgentEndEmit) {
|
|
1751
|
+
this.#pendingAgentEndEmit = pending;
|
|
1752
|
+
}
|
|
1753
|
+
this.#flushPendingAgentEnd();
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
#releaseDeferredAgentEndContinuation(hold: symbol | undefined): void {
|
|
1757
|
+
if (!hold) return;
|
|
1758
|
+
const pending = this.#pendingAgentEndContinuationHolds.get(hold);
|
|
1759
|
+
this.#pendingAgentEndContinuationHolds.delete(hold);
|
|
1760
|
+
this.#restoreDeferredAgentEndAfterContinuationFailure(pending);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
#releaseDeferredAgentEndContinuations(): void {
|
|
1764
|
+
let pending: AgentSessionEvent | undefined;
|
|
1765
|
+
for (const candidate of this.#pendingAgentEndContinuationHolds.values()) {
|
|
1766
|
+
pending = candidate;
|
|
1767
|
+
break;
|
|
1768
|
+
}
|
|
1769
|
+
this.#pendingAgentEndContinuationHolds.clear();
|
|
1770
|
+
this.#restoreDeferredAgentEndAfterContinuationFailure(pending);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1505
1773
|
#endInFlight(): void {
|
|
1506
1774
|
this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
|
|
1507
1775
|
if (this.#promptInFlightCount === 0) {
|
|
@@ -1511,17 +1779,21 @@ export class AgentSession {
|
|
|
1511
1779
|
}
|
|
1512
1780
|
}
|
|
1513
1781
|
|
|
1514
|
-
#resetInFlight(): void {
|
|
1515
|
-
this.#promptInFlightCount = 0;
|
|
1516
|
-
this.#releasePowerAssertion();
|
|
1517
|
-
this.#flushPendingBackgroundExchanges();
|
|
1518
|
-
this.#flushPendingAgentEnd();
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
1782
|
#flushPendingAgentEnd(): void {
|
|
1783
|
+
if (
|
|
1784
|
+
this.#promptInFlightCount > 0 ||
|
|
1785
|
+
this.#agentEventHandlersInFlight > 0 ||
|
|
1786
|
+
this.#queuedExtensionEventCount > 0 ||
|
|
1787
|
+
this.#pendingAgentEndContinuationHolds.size > 0
|
|
1788
|
+
)
|
|
1789
|
+
return;
|
|
1522
1790
|
const pending = this.#pendingAgentEndEmit;
|
|
1523
1791
|
if (!pending) return;
|
|
1524
1792
|
this.#pendingAgentEndEmit = undefined;
|
|
1793
|
+
// Persist before notifying synchronous subscribers: a subscriber may start a
|
|
1794
|
+
// successor prompt from agent_end, whose running state must serialize after
|
|
1795
|
+
// this terminal boundary rather than be overwritten by it.
|
|
1796
|
+
this.#persistRuntimeStateInBackground(pending);
|
|
1525
1797
|
this.#emit(pending);
|
|
1526
1798
|
void this.#queueExtensionEvent(pending);
|
|
1527
1799
|
}
|
|
@@ -1530,6 +1802,7 @@ export class AgentSession {
|
|
|
1530
1802
|
this.agent = config.agent;
|
|
1531
1803
|
this.sessionManager = config.sessionManager;
|
|
1532
1804
|
this.settings = config.settings;
|
|
1805
|
+
this.notificationSessionController = config.notificationSessionController;
|
|
1533
1806
|
this.taskDepth = config.taskDepth ?? 0;
|
|
1534
1807
|
// Register this session with the process-wide resource GC (idle/RSS browser-tab eviction
|
|
1535
1808
|
// + stale screenshot cleanup). Session-keyed so concurrent sessions share one timer safely.
|
|
@@ -1563,7 +1836,6 @@ export class AgentSession {
|
|
|
1563
1836
|
if (config.providerSessionState) {
|
|
1564
1837
|
this.#providerSessionState = config.providerSessionState;
|
|
1565
1838
|
}
|
|
1566
|
-
this.#validateRetryFallbackChains();
|
|
1567
1839
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
1568
1840
|
this.#workflowGateToolSession = config.workflowGateToolSession;
|
|
1569
1841
|
this.#requestedToolNames = config.requestedToolNames;
|
|
@@ -1595,13 +1867,18 @@ export class AgentSession {
|
|
|
1595
1867
|
this.agent.setProviderResponseInterceptor(this.#onResponse);
|
|
1596
1868
|
this.agent.setRawSseEventInterceptor(this.#onSseEvent);
|
|
1597
1869
|
this.#setGuardedAgentTools(this.agent.state.tools);
|
|
1870
|
+
this.#bindWorkflowGateEmitter();
|
|
1598
1871
|
this.yieldQueue = new YieldQueue({
|
|
1599
1872
|
isStreaming: () => this.isStreaming,
|
|
1600
1873
|
injectStreaming: message => this.agent.followUp(message),
|
|
1601
1874
|
injectIdle: async messages => {
|
|
1602
1875
|
const first = messages[0];
|
|
1603
1876
|
if (!first) return;
|
|
1604
|
-
|
|
1877
|
+
if (messages.length === 1) {
|
|
1878
|
+
await this.agent.prompt(first, this.#managedFallbackPromptOptions());
|
|
1879
|
+
} else {
|
|
1880
|
+
await this.agent.prompt(messages, this.#managedFallbackPromptOptions());
|
|
1881
|
+
}
|
|
1605
1882
|
},
|
|
1606
1883
|
scheduleIdleFlush: run => {
|
|
1607
1884
|
this.#schedulePostPromptTask(
|
|
@@ -1613,6 +1890,9 @@ export class AgentSession {
|
|
|
1613
1890
|
},
|
|
1614
1891
|
});
|
|
1615
1892
|
this.agent.setOnBeforeYield(() => this.yieldQueue.flush("streaming"));
|
|
1893
|
+
this.agent.setMaintainContext((context, lifecycle) =>
|
|
1894
|
+
this.#trackMidRunMaintenance(this.#runMidRunMaintenance(context, lifecycle)),
|
|
1895
|
+
);
|
|
1616
1896
|
this.#convertToLlm = config.convertToLlm ?? convertToLlm;
|
|
1617
1897
|
this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
|
|
1618
1898
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
@@ -1695,9 +1975,16 @@ export class AgentSession {
|
|
|
1695
1975
|
|
|
1696
1976
|
// Always subscribe to agent events for internal handling
|
|
1697
1977
|
// (session persistence, hooks, auto-compaction, retry logic)
|
|
1698
|
-
this.#unsubscribeAgent = this.agent.subscribe(this.#
|
|
1978
|
+
this.#unsubscribeAgent = this.agent.subscribe(this.#trackAgentEvent);
|
|
1979
|
+
|
|
1699
1980
|
// Re-evaluate append-only context mode when the setting changes at runtime.
|
|
1700
1981
|
this.#unsubscribeAppendOnly = onAppendOnlyModeChanged(_value => this.#syncAppendOnlyContext(this.model));
|
|
1982
|
+
// SDK ToolSession callbacks capture the just-constructed session. Defer the
|
|
1983
|
+
// initial ask-tool attachment until that capture has been assigned by the
|
|
1984
|
+
// session factory, while retaining the durable emitter created above.
|
|
1985
|
+
queueMicrotask(() => {
|
|
1986
|
+
if (!this.#isDisposed) this.#ensureWorkflowGateAskTool();
|
|
1987
|
+
});
|
|
1701
1988
|
}
|
|
1702
1989
|
|
|
1703
1990
|
/** Model registry for API key resolution and model discovery */
|
|
@@ -1795,6 +2082,90 @@ export class AgentSession {
|
|
|
1795
2082
|
return this.#providerSessionState;
|
|
1796
2083
|
}
|
|
1797
2084
|
|
|
2085
|
+
/** Suspend provider state without closing it while a temporary model is active. */
|
|
2086
|
+
beginTemporaryProviderSessionScope(reason: TemporaryModelReason): TemporaryProviderSessionScope {
|
|
2087
|
+
return this.#beginTemporaryProviderSessionScope(reason, false);
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
#beginTemporaryProviderSessionScope(
|
|
2091
|
+
reason: TemporaryModelReason,
|
|
2092
|
+
autoOwned: boolean,
|
|
2093
|
+
): TemporaryProviderSessionScope {
|
|
2094
|
+
const token: TemporaryProviderSessionScope = Object.freeze({ reason });
|
|
2095
|
+
this.#temporaryProviderSessionScopes.push({
|
|
2096
|
+
token,
|
|
2097
|
+
autoOwned,
|
|
2098
|
+
model: this.model,
|
|
2099
|
+
thinkingLevel: this.#thinkingLevel,
|
|
2100
|
+
fallbackController: this.#defaultFallbackController,
|
|
2101
|
+
providerSessionState: this.#providerSessionState,
|
|
2102
|
+
});
|
|
2103
|
+
this.#rebindProviderSessionState(new Map());
|
|
2104
|
+
const temporaryModel = this.model;
|
|
2105
|
+
this.#defaultFallbackController = new FallbackChainController(
|
|
2106
|
+
{
|
|
2107
|
+
role: "default",
|
|
2108
|
+
entries: temporaryModel ? [formatModelString(temporaryModel)] : [],
|
|
2109
|
+
origin: "temporary-provider-scope",
|
|
2110
|
+
explicitHead: true,
|
|
2111
|
+
},
|
|
2112
|
+
this.settings.get("fallback.maxAttempts"),
|
|
2113
|
+
);
|
|
2114
|
+
return token;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
/** Returns the topmost auto-owned scope without retaining a separate ownership handle. */
|
|
2118
|
+
#currentAutoTemporaryProviderSessionScope(): TemporaryProviderSessionScopeRecord | undefined {
|
|
2119
|
+
return this.#temporaryProviderSessionScopes.findLast(scope => scope.autoOwned);
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
/** Restore a temporary scope, unwinding any auto-owned scopes above it. */
|
|
2123
|
+
restoreTemporaryProviderSessionScope(token: TemporaryProviderSessionScope): boolean {
|
|
2124
|
+
const scopeIndex = this.#temporaryProviderSessionScopes.findLastIndex(scope => scope.token === token);
|
|
2125
|
+
if (
|
|
2126
|
+
scopeIndex < 0 ||
|
|
2127
|
+
this.#temporaryProviderSessionScopes.slice(scopeIndex + 1).some(scope => !scope.autoOwned)
|
|
2128
|
+
) {
|
|
2129
|
+
return false;
|
|
2130
|
+
}
|
|
2131
|
+
while (this.#temporaryProviderSessionScopes.length > scopeIndex) {
|
|
2132
|
+
this.#restoreTopTemporaryProviderSessionScope();
|
|
2133
|
+
}
|
|
2134
|
+
return true;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
#restoreTopTemporaryProviderSessionScope(): void {
|
|
2138
|
+
const scope = this.#temporaryProviderSessionScopes.pop();
|
|
2139
|
+
if (!scope) return;
|
|
2140
|
+
this.#closeProviderSessionMap(this.#providerSessionState, "temporary scope restore");
|
|
2141
|
+
this.#rebindProviderSessionState(scope.providerSessionState);
|
|
2142
|
+
this.#defaultFallbackController = scope.fallbackController;
|
|
2143
|
+
const previousEditMode = this.#resolveActiveEditMode();
|
|
2144
|
+
if (scope.model) {
|
|
2145
|
+
this.agent.setModel(scope.model);
|
|
2146
|
+
this.#syncAppendOnlyContext(scope.model);
|
|
2147
|
+
}
|
|
2148
|
+
this.#thinkingLevel = scope.thinkingLevel;
|
|
2149
|
+
this.agent.setThinkingLevel(toReasoningEffort(scope.thinkingLevel));
|
|
2150
|
+
void this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
/** Promote a temporary scope. The suspended provider state is permanently closed. */
|
|
2154
|
+
commitTemporaryProviderSessionScope(token: TemporaryProviderSessionScope): boolean {
|
|
2155
|
+
const scope = this.#temporaryProviderSessionScopes.at(-1);
|
|
2156
|
+
if (!scope || scope.token !== token) return false;
|
|
2157
|
+
this.#temporaryProviderSessionScopes.pop();
|
|
2158
|
+
this.#closeProviderSessionMap(scope.providerSessionState, "temporary scope commit");
|
|
2159
|
+
return true;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
/** Permanently discard every suspended map while retaining the active map. */
|
|
2163
|
+
#commitAllTemporaryProviderSessionScopes(): void {
|
|
2164
|
+
const scopes = this.#temporaryProviderSessionScopes;
|
|
2165
|
+
this.#temporaryProviderSessionScopes = [];
|
|
2166
|
+
for (const scope of scopes) this.#closeProviderSessionMap(scope.providerSessionState, "permanent model change");
|
|
2167
|
+
}
|
|
2168
|
+
|
|
1798
2169
|
async buildForkContextSeed(options: ForkContextSeedOptions): Promise<ForkContextSeed> {
|
|
1799
2170
|
const maxMessages = Math.min(500, Math.max(0, Math.trunc(options.maxMessages)));
|
|
1800
2171
|
const maxTokens = Math.max(0, Math.trunc(options.maxTokens));
|
|
@@ -2107,14 +2478,32 @@ export class AgentSession {
|
|
|
2107
2478
|
#queuedExtensionEvents: Promise<void> = Promise.resolve();
|
|
2108
2479
|
|
|
2109
2480
|
#queueExtensionEvent(event: AgentSessionEvent): Promise<void> {
|
|
2481
|
+
this.#queuedExtensionEventCount++;
|
|
2110
2482
|
const emit = async () => {
|
|
2111
2483
|
await this.#emitExtensionEvent(event);
|
|
2112
2484
|
};
|
|
2113
2485
|
const queued = this.#queuedExtensionEvents.then(emit, emit);
|
|
2114
2486
|
this.#queuedExtensionEvents = queued.catch(() => {});
|
|
2487
|
+
const settled = () => {
|
|
2488
|
+
this.#queuedExtensionEventCount = Math.max(0, this.#queuedExtensionEventCount - 1);
|
|
2489
|
+
this.#flushPendingAgentEnd();
|
|
2490
|
+
};
|
|
2491
|
+
void queued.then(settled, settled);
|
|
2115
2492
|
return queued;
|
|
2116
2493
|
}
|
|
2117
2494
|
|
|
2495
|
+
#trackAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
2496
|
+
this.#agentEventHandlersInFlight++;
|
|
2497
|
+
try {
|
|
2498
|
+
await this.#handleAgentEvent(event);
|
|
2499
|
+
} catch (error) {
|
|
2500
|
+
logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
|
|
2501
|
+
} finally {
|
|
2502
|
+
this.#agentEventHandlersInFlight = Math.max(0, this.#agentEventHandlersInFlight - 1);
|
|
2503
|
+
this.#flushPendingAgentEnd();
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
|
|
2118
2507
|
#persistRuntimeStateInBackground(event: AgentSessionEvent): void {
|
|
2119
2508
|
void persistCoordinatorRuntimeStateFromEvent(event, {
|
|
2120
2509
|
sessionId: this.sessionId,
|
|
@@ -2136,21 +2525,34 @@ export class AgentSession {
|
|
|
2136
2525
|
}
|
|
2137
2526
|
return;
|
|
2138
2527
|
}
|
|
2528
|
+
// A maintenance agent_end is an internal checkpoint only while another
|
|
2529
|
+
// continuation will follow. An aborted maintenance run is its terminal
|
|
2530
|
+
// settlement and must reach public subscribers.
|
|
2531
|
+
if (event.type === "agent_end" && event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted")
|
|
2532
|
+
return;
|
|
2533
|
+
|
|
2139
2534
|
const persistRuntimeState = () => this.#persistRuntimeStateInBackground(event);
|
|
2140
|
-
// Hold the
|
|
2141
|
-
//
|
|
2142
|
-
//
|
|
2143
|
-
//
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
if (event.type === "agent_end" && this.#promptInFlightCount > 0) {
|
|
2149
|
-
void persistRuntimeState();
|
|
2535
|
+
// Hold agent_end until the prompt's finally and all earlier async event work
|
|
2536
|
+
// have unwound. Subscribers treat this event as the ready signal; flushing it
|
|
2537
|
+
// from abort while either barrier is active permits a successor to race the
|
|
2538
|
+
// prior prompt's cleanup.
|
|
2539
|
+
if (
|
|
2540
|
+
event.type === "agent_end" &&
|
|
2541
|
+
(this.#promptInFlightCount > 0 || this.#agentEventHandlersInFlight > 0 || this.#queuedExtensionEventCount > 0)
|
|
2542
|
+
) {
|
|
2150
2543
|
this.#pendingAgentEndEmit = event;
|
|
2151
2544
|
return;
|
|
2152
2545
|
}
|
|
2153
2546
|
|
|
2547
|
+
if (event.type === "agent_end") {
|
|
2548
|
+
// Start the durable terminal write before synchronous subscribers can
|
|
2549
|
+
// re-enter prompt(), so a successor's running transition serializes after it.
|
|
2550
|
+
void persistRuntimeState();
|
|
2551
|
+
this.#emit(event);
|
|
2552
|
+
await this.#emitExtensionEvent(event);
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2154
2556
|
// Local subscribers are part of the AgentSession control path: retryNow(),
|
|
2155
2557
|
// auto-continuation gates, goal reminders, and tests all observe these events
|
|
2156
2558
|
// synchronously. Coordinator sidecar writes and extension hooks are secondary
|
|
@@ -2165,6 +2567,18 @@ export class AgentSession {
|
|
|
2165
2567
|
|
|
2166
2568
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
2167
2569
|
#handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
2570
|
+
// Record a successful final yield before any asynchronous extension work so a
|
|
2571
|
+
// concurrently delivered agent_end cannot start post-turn maintenance first.
|
|
2572
|
+
if (event.type === "tool_execution_end" && event.toolName === "yield" && !event.isError) {
|
|
2573
|
+
this.#lastSuccessfulYieldToolCallId = event.toolCallId;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
// Agent listeners run synchronously, but this handler yields while emitting
|
|
2577
|
+
// session events. Capture the maintenance run identity before that yield so
|
|
2578
|
+
// a later raw listener cannot revive a cancelled/replaced continuation.
|
|
2579
|
+
const maintenanceGeneration =
|
|
2580
|
+
event.type === "agent_end" && event.stopReason === "maintenance" ? this.#promptGeneration : undefined;
|
|
2581
|
+
const maintenanceWasDisposed = this.#isDisposed;
|
|
2168
2582
|
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
2169
2583
|
// This ensures the UI sees the updated queue state
|
|
2170
2584
|
if (event.type === "message_start" && event.message.role === "user") {
|
|
@@ -2229,9 +2643,36 @@ export class AgentSession {
|
|
|
2229
2643
|
this.#silentAbortPending = false;
|
|
2230
2644
|
}
|
|
2231
2645
|
|
|
2646
|
+
// Canonical persistence must happen synchronously before listener work can
|
|
2647
|
+
// await: the EventStream FIFO drain then guarantees tool results and every
|
|
2648
|
+
// steering message are in the branch before a maintenance rewrite starts.
|
|
2649
|
+
if (event.type === "message_end") {
|
|
2650
|
+
if (event.message.role === "hookMessage" || event.message.role === "custom") {
|
|
2651
|
+
const isRosterReminder = event.message.role === "custom" && event.message.customType === "irc-peer-roster";
|
|
2652
|
+
if (!isRosterReminder) {
|
|
2653
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
2654
|
+
event.message.customType,
|
|
2655
|
+
event.message.content,
|
|
2656
|
+
event.message.display,
|
|
2657
|
+
event.message.details,
|
|
2658
|
+
event.message.attribution ?? "agent",
|
|
2659
|
+
getSessionMessageObservationId(event.message),
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
} else if (
|
|
2663
|
+
event.message.role === "user" ||
|
|
2664
|
+
event.message.role === "developer" ||
|
|
2665
|
+
event.message.role === "assistant" ||
|
|
2666
|
+
event.message.role === "toolResult" ||
|
|
2667
|
+
event.message.role === "fileMention"
|
|
2668
|
+
) {
|
|
2669
|
+
this.sessionManager.appendMessage(event.message);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2232
2673
|
// Deobfuscate assistant message content for display emission — the LLM echoes back
|
|
2233
2674
|
// obfuscated placeholders, but listeners (TUI, extensions, exporters) must see real
|
|
2234
|
-
// values. The original event.message stays obfuscated so the persistence path
|
|
2675
|
+
// values. The original event.message stays obfuscated so the canonical persistence path above
|
|
2235
2676
|
// writes `#HASH#` tokens to the session file; convertToLlm re-obfuscates outbound
|
|
2236
2677
|
// traffic on the next turn. Walks text, thinking, and toolCall arguments/intent.
|
|
2237
2678
|
let displayEvent: AgentEvent = event;
|
|
@@ -2298,9 +2739,6 @@ export class AgentSession {
|
|
|
2298
2739
|
await this.#activatePendingGjcGoalModeRequest();
|
|
2299
2740
|
}
|
|
2300
2741
|
}
|
|
2301
|
-
if (event.type === "tool_execution_end" && event.toolName === "yield" && !event.isError) {
|
|
2302
|
-
this.#lastSuccessfulYieldToolCallId = event.toolCallId;
|
|
2303
|
-
}
|
|
2304
2742
|
if (event.type === "turn_end" && this.#pendingRewindReport) {
|
|
2305
2743
|
const report = this.#pendingRewindReport;
|
|
2306
2744
|
this.#pendingRewindReport = undefined;
|
|
@@ -2431,38 +2869,11 @@ export class AgentSession {
|
|
|
2431
2869
|
this.#maybeAbortStreamingEdit(event, this.#promptGeneration);
|
|
2432
2870
|
}
|
|
2433
2871
|
|
|
2434
|
-
// Handle
|
|
2872
|
+
// Handle post-persistence message side effects.
|
|
2435
2873
|
if (event.type === "message_end") {
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
// Roster reminders are request-context-only: never persist them, or a
|
|
2439
|
-
// reload would resurrect stale rosters into agent history.
|
|
2440
|
-
const isRosterReminder = event.message.role === "custom" && event.message.customType === "irc-peer-roster";
|
|
2441
|
-
if (!isRosterReminder) {
|
|
2442
|
-
// Persist as CustomMessageEntry
|
|
2443
|
-
this.sessionManager.appendCustomMessageEntry(
|
|
2444
|
-
event.message.customType,
|
|
2445
|
-
event.message.content,
|
|
2446
|
-
event.message.display,
|
|
2447
|
-
event.message.details,
|
|
2448
|
-
event.message.attribution ?? "agent",
|
|
2449
|
-
getSessionMessageObservationId(event.message),
|
|
2450
|
-
);
|
|
2451
|
-
}
|
|
2452
|
-
if (event.message.role === "custom" && event.message.customType === "ttsr-injection") {
|
|
2453
|
-
this.#markTtsrInjected(this.#extractTtsrRuleNames(event.message.details));
|
|
2454
|
-
}
|
|
2455
|
-
} else if (
|
|
2456
|
-
event.message.role === "user" ||
|
|
2457
|
-
event.message.role === "developer" ||
|
|
2458
|
-
event.message.role === "assistant" ||
|
|
2459
|
-
event.message.role === "toolResult" ||
|
|
2460
|
-
event.message.role === "fileMention"
|
|
2461
|
-
) {
|
|
2462
|
-
// Regular LLM message - persist as SessionMessageEntry
|
|
2463
|
-
this.sessionManager.appendMessage(event.message);
|
|
2874
|
+
if (event.message.role === "custom" && event.message.customType === "ttsr-injection") {
|
|
2875
|
+
this.#markTtsrInjected(this.#extractTtsrRuleNames(event.message.details));
|
|
2464
2876
|
}
|
|
2465
|
-
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
2466
2877
|
|
|
2467
2878
|
// Track assistant message for auto-compaction (checked on agent_end)
|
|
2468
2879
|
if (event.message.role === "assistant") {
|
|
@@ -2500,13 +2911,6 @@ export class AgentSession {
|
|
|
2500
2911
|
assistantMsg.stopReason !== "aborted" &&
|
|
2501
2912
|
this.#retryAttempt > 0
|
|
2502
2913
|
) {
|
|
2503
|
-
if (this.#activeRetryFallback && this.model) {
|
|
2504
|
-
await this.#emitSessionEvent({
|
|
2505
|
-
type: "retry_fallback_succeeded",
|
|
2506
|
-
model: formatRetryFallbackSelector(this.model, this.thinkingLevel),
|
|
2507
|
-
role: this.#activeRetryFallback.role,
|
|
2508
|
-
});
|
|
2509
|
-
}
|
|
2510
2914
|
await this.#emitSessionEvent({
|
|
2511
2915
|
type: "auto_retry_end",
|
|
2512
2916
|
success: true,
|
|
@@ -2580,6 +2984,29 @@ export class AgentSession {
|
|
|
2580
2984
|
|
|
2581
2985
|
// Check auto-retry and auto-compaction after agent completes
|
|
2582
2986
|
if (event.type === "agent_end") {
|
|
2987
|
+
// Cooperative mid-run maintenance interruption (issue #2035). The loop
|
|
2988
|
+
// ended the run losslessly after #runMidRunMaintenance did prune/
|
|
2989
|
+
// compact/promote; this handler is the SINGLE continuation owner. Resume
|
|
2990
|
+
// the same run on the rewritten context (no synthetic prompt), skipping
|
|
2991
|
+
// the goal-runtime / skill-state / retry / compaction finalization a
|
|
2992
|
+
// normal agent_end runs — none of that applies to an in-progress run.
|
|
2993
|
+
// "aborted" settles without resuming; the generation guard drops the
|
|
2994
|
+
// continuation if a newer prompt/abort has moved the run on.
|
|
2995
|
+
if (event.stopReason === "maintenance") {
|
|
2996
|
+
this.#lastAssistantMessage = undefined;
|
|
2997
|
+
const outcome = event.maintenanceOutcome;
|
|
2998
|
+
if (
|
|
2999
|
+
outcome &&
|
|
3000
|
+
outcome !== "aborted" &&
|
|
3001
|
+
!maintenanceWasDisposed &&
|
|
3002
|
+
!this.#isDisposed &&
|
|
3003
|
+
maintenanceGeneration !== undefined &&
|
|
3004
|
+
this.#promptGeneration === maintenanceGeneration
|
|
3005
|
+
) {
|
|
3006
|
+
this.#scheduleAgentContinue({ generation: maintenanceGeneration, skipCompactionCheck: true });
|
|
3007
|
+
}
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
2583
3010
|
const usage = this.getSessionStats().tokens;
|
|
2584
3011
|
await this.#goalRuntime.onAgentEnd({
|
|
2585
3012
|
currentUsage: {
|
|
@@ -2606,6 +3033,7 @@ export class AgentSession {
|
|
|
2606
3033
|
this.#lastAssistantMessage = undefined;
|
|
2607
3034
|
if (!msg) {
|
|
2608
3035
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
3036
|
+
this.#resolveRetry();
|
|
2609
3037
|
return;
|
|
2610
3038
|
}
|
|
2611
3039
|
|
|
@@ -2736,6 +3164,7 @@ export class AgentSession {
|
|
|
2736
3164
|
try {
|
|
2737
3165
|
await scheduler.wait(delayMs, { signal });
|
|
2738
3166
|
} catch {
|
|
3167
|
+
options?.onSkip?.();
|
|
2739
3168
|
return;
|
|
2740
3169
|
}
|
|
2741
3170
|
}
|
|
@@ -2757,40 +3186,82 @@ export class AgentSession {
|
|
|
2757
3186
|
delayMs?: number;
|
|
2758
3187
|
generation?: number;
|
|
2759
3188
|
skipCompactionCheck?: boolean;
|
|
3189
|
+
suppressPredecessorAgentEnd?: boolean;
|
|
2760
3190
|
shouldContinue?: () => boolean;
|
|
2761
3191
|
onSkip?: (reason: "generation_changed" | "aborted_signal" | "queue_drained") => void;
|
|
2762
3192
|
onError?: (error: unknown) => void;
|
|
2763
3193
|
}): Promise<void> {
|
|
3194
|
+
const predecessorAgentEndHold = options?.suppressPredecessorAgentEnd
|
|
3195
|
+
? this.#reserveDeferredAgentEndForContinuation()
|
|
3196
|
+
: undefined;
|
|
3197
|
+
let terminalized = false;
|
|
3198
|
+
const skip = (reason: "generation_changed" | "aborted_signal" | "queue_drained") => {
|
|
3199
|
+
if (terminalized) return;
|
|
3200
|
+
terminalized = true;
|
|
3201
|
+
this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
3202
|
+
options?.onSkip?.(reason);
|
|
3203
|
+
};
|
|
3204
|
+
const fail = (error: unknown) => {
|
|
3205
|
+
if (terminalized) return;
|
|
3206
|
+
terminalized = true;
|
|
3207
|
+
this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
3208
|
+
logger.warn("agent.continue failed after scheduling", {
|
|
3209
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3210
|
+
});
|
|
3211
|
+
options?.onError?.(error);
|
|
3212
|
+
};
|
|
2764
3213
|
const scheduledGeneration = options?.generation;
|
|
2765
3214
|
const signal = this.#postPromptTasksAbortController.signal;
|
|
2766
3215
|
return this.#schedulePostPromptTask(
|
|
2767
3216
|
async () => {
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
options.
|
|
2778
|
-
|
|
2779
|
-
|
|
3217
|
+
const canContinue = (): boolean => {
|
|
3218
|
+
if (signal.aborted || this.#isDisposed) {
|
|
3219
|
+
skip("aborted_signal");
|
|
3220
|
+
return false;
|
|
3221
|
+
}
|
|
3222
|
+
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
3223
|
+
skip("generation_changed");
|
|
3224
|
+
return false;
|
|
3225
|
+
}
|
|
3226
|
+
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
3227
|
+
skip("queue_drained");
|
|
3228
|
+
return false;
|
|
3229
|
+
}
|
|
3230
|
+
return true;
|
|
3231
|
+
};
|
|
3232
|
+
if (!canContinue()) return;
|
|
2780
3233
|
try {
|
|
2781
|
-
await this.#maybeRestoreRetryFallbackPrimary();
|
|
2782
3234
|
if (!options?.skipCompactionCheck) {
|
|
2783
3235
|
await this.#checkEstimatedContextBeforePrompt();
|
|
3236
|
+
if (!canContinue()) return;
|
|
3237
|
+
}
|
|
3238
|
+
if (signal.aborted) {
|
|
3239
|
+
skip("aborted_signal");
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
3243
|
+
skip("generation_changed");
|
|
3244
|
+
return;
|
|
3245
|
+
}
|
|
3246
|
+
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
3247
|
+
skip("queue_drained");
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
|
|
3251
|
+
try {
|
|
3252
|
+
await this.agent.continue(this.#managedFallbackPromptOptions());
|
|
3253
|
+
} catch (error) {
|
|
3254
|
+
this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
|
|
3255
|
+
throw error;
|
|
2784
3256
|
}
|
|
2785
|
-
await this.agent.continue();
|
|
2786
3257
|
} catch (error) {
|
|
2787
|
-
|
|
2788
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2789
|
-
});
|
|
2790
|
-
options?.onError?.(error);
|
|
3258
|
+
fail(error);
|
|
2791
3259
|
}
|
|
2792
3260
|
},
|
|
2793
|
-
{
|
|
3261
|
+
{
|
|
3262
|
+
delayMs: options?.delayMs,
|
|
3263
|
+
onSkip: () => skip("aborted_signal"),
|
|
3264
|
+
},
|
|
2794
3265
|
);
|
|
2795
3266
|
}
|
|
2796
3267
|
|
|
@@ -2838,6 +3309,8 @@ export class AgentSession {
|
|
|
2838
3309
|
this.#scheduleAgentContinue({
|
|
2839
3310
|
delayMs: 100,
|
|
2840
3311
|
generation,
|
|
3312
|
+
suppressPredecessorAgentEnd: true,
|
|
3313
|
+
|
|
2841
3314
|
onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
|
|
2842
3315
|
onError: error => this.#logCompactionContinuationError("overflow_retry", error),
|
|
2843
3316
|
});
|
|
@@ -2854,6 +3327,7 @@ export class AgentSession {
|
|
|
2854
3327
|
}
|
|
2855
3328
|
|
|
2856
3329
|
#scheduleAutoContinuePrompt(generation: number): void {
|
|
3330
|
+
const predecessorAgentEndHold = this.#reserveDeferredAgentEndForContinuation();
|
|
2857
3331
|
const continuePrompt = async () => {
|
|
2858
3332
|
await this.#promptWithMessage(
|
|
2859
3333
|
{
|
|
@@ -2863,26 +3337,32 @@ export class AgentSession {
|
|
|
2863
3337
|
timestamp: Date.now(),
|
|
2864
3338
|
},
|
|
2865
3339
|
autoContinuePrompt,
|
|
2866
|
-
{
|
|
3340
|
+
{
|
|
3341
|
+
skipPostPromptRecoveryWait: true,
|
|
3342
|
+
skipCompactionCheck: true,
|
|
3343
|
+
predecessorAgentEndHold,
|
|
3344
|
+
},
|
|
2867
3345
|
);
|
|
2868
3346
|
};
|
|
2869
3347
|
const scheduledGeneration = generation;
|
|
2870
3348
|
const signal = this.#postPromptTasksAbortController.signal;
|
|
2871
3349
|
this.#trackPostPromptTask(
|
|
2872
3350
|
(async () => {
|
|
2873
|
-
await Promise.resolve();
|
|
2874
|
-
if (signal.aborted) {
|
|
2875
|
-
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
2876
|
-
return;
|
|
2877
|
-
}
|
|
2878
|
-
if (this.#promptGeneration !== scheduledGeneration) {
|
|
2879
|
-
this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
|
|
2880
|
-
return;
|
|
2881
|
-
}
|
|
2882
3351
|
try {
|
|
3352
|
+
await Promise.resolve();
|
|
3353
|
+
if (signal.aborted) {
|
|
3354
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
if (this.#promptGeneration !== scheduledGeneration) {
|
|
3358
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
2883
3361
|
await continuePrompt();
|
|
2884
3362
|
} catch (error) {
|
|
2885
3363
|
this.#logCompactionContinuationError("auto_continue_prompt", error);
|
|
3364
|
+
} finally {
|
|
3365
|
+
this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
2886
3366
|
}
|
|
2887
3367
|
})(),
|
|
2888
3368
|
);
|
|
@@ -2895,11 +3375,13 @@ export class AgentSession {
|
|
|
2895
3375
|
|
|
2896
3376
|
const pendingTasks = Array.from(this.#postPromptTasks);
|
|
2897
3377
|
if (pendingTasks.length === 0) {
|
|
3378
|
+
this.#releaseDeferredAgentEndContinuations();
|
|
2898
3379
|
this.#resolvePostPromptTasks();
|
|
2899
3380
|
return;
|
|
2900
3381
|
}
|
|
2901
3382
|
|
|
2902
3383
|
await Promise.allSettled(pendingTasks);
|
|
3384
|
+
this.#releaseDeferredAgentEndContinuations();
|
|
2903
3385
|
if (this.#postPromptTasks.size === 0) {
|
|
2904
3386
|
this.#resolvePostPromptTasks();
|
|
2905
3387
|
}
|
|
@@ -2909,6 +3391,7 @@ export class AgentSession {
|
|
|
2909
3391
|
this.#postPromptTasksAbortController.abort();
|
|
2910
3392
|
this.#postPromptTasksAbortController = new AbortController();
|
|
2911
3393
|
this.#postPromptTasks.clear();
|
|
3394
|
+
this.#releaseDeferredAgentEndContinuations();
|
|
2912
3395
|
this.#resolveTtsrResume();
|
|
2913
3396
|
this.#resolvePostPromptTasks();
|
|
2914
3397
|
}
|
|
@@ -3733,6 +4216,7 @@ export class AgentSession {
|
|
|
3733
4216
|
subscribe(listener: AgentSessionEventListener): () => void {
|
|
3734
4217
|
this.#eventListeners.push(listener);
|
|
3735
4218
|
this.#rebuildEventListenerSnapshot();
|
|
4219
|
+
for (const event of this.#pendingFallbackSwitches.splice(0)) listener(event);
|
|
3736
4220
|
|
|
3737
4221
|
// Return unsubscribe function for this specific listener
|
|
3738
4222
|
return () => {
|
|
@@ -3750,6 +4234,7 @@ export class AgentSession {
|
|
|
3750
4234
|
* Used internally during operations that need to pause event processing.
|
|
3751
4235
|
*/
|
|
3752
4236
|
#disconnectFromAgent(): void {
|
|
4237
|
+
this.#abortActiveMidRunBarriers();
|
|
3753
4238
|
if (this.#unsubscribeAgent) {
|
|
3754
4239
|
this.#unsubscribeAgent();
|
|
3755
4240
|
this.#unsubscribeAgent = undefined;
|
|
@@ -3762,7 +4247,7 @@ export class AgentSession {
|
|
|
3762
4247
|
*/
|
|
3763
4248
|
#reconnectToAgent(): void {
|
|
3764
4249
|
if (this.#unsubscribeAgent) return; // Already connected
|
|
3765
|
-
this.#unsubscribeAgent = this.agent.subscribe(this.#
|
|
4250
|
+
this.#unsubscribeAgent = this.agent.subscribe(this.#trackAgentEvent);
|
|
3766
4251
|
}
|
|
3767
4252
|
|
|
3768
4253
|
/**
|
|
@@ -3803,12 +4288,51 @@ export class AgentSession {
|
|
|
3803
4288
|
* Remove all listeners, flush pending writes, and disconnect from agent.
|
|
3804
4289
|
* Call this when completely done with the session.
|
|
3805
4290
|
*/
|
|
3806
|
-
|
|
4291
|
+
dispose(): Promise<void> {
|
|
4292
|
+
this.#evalExecutionDisposing = true;
|
|
4293
|
+
if (this.#disposePromise) return this.#disposePromise;
|
|
4294
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
4295
|
+
this.#disposePromise = promise;
|
|
4296
|
+
void this.#dispose().then(resolve, reject);
|
|
4297
|
+
return promise;
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4300
|
+
async #dispose(): Promise<void> {
|
|
4301
|
+
const admissionClosed = this.#closeSessionAdmission();
|
|
3807
4302
|
this.#isDisposed = true;
|
|
4303
|
+
// Reject new direct Python starts as soon as disposal begins (synchronously,
|
|
4304
|
+
// before any await) so callers cannot race a start against teardown.
|
|
4305
|
+
this.#evalExecutionDisposing = true;
|
|
4306
|
+
this.#abortActiveMidRunBarriers();
|
|
4307
|
+
this.abortCompaction();
|
|
4308
|
+
this.agent.abort();
|
|
4309
|
+
// Disconnect the Agent event bridge NOW — before the maintenance join and the
|
|
4310
|
+
// bounded idle / forceAbort below — so no agent_end emitted during teardown
|
|
4311
|
+
// (including the one forceAbort emits) can re-enter #handleAgentEvent and start
|
|
4312
|
+
// fresh post-turn maintenance or mutate the closing session. Maintenance promises
|
|
4313
|
+
// are joined directly (not via events), so this does not affect the join.
|
|
4314
|
+
this.#disconnectFromAgent();
|
|
4315
|
+
// R2-5: join any in-flight mid-run maintenance invocation before teardown so the
|
|
4316
|
+
// abort-aware maintenance promise (already aborted above) settles and cannot touch
|
|
4317
|
+
// torn-down state afterward.
|
|
4318
|
+
await this.#waitForActiveMidRunMaintenance();
|
|
4319
|
+
// R2-5: give the aborted Agent run a bounded chance to settle, then force-
|
|
4320
|
+
// invalidate its run id so an abort-ignoring provider/tool cannot emit a late
|
|
4321
|
+
// message_end after teardown. Mirrors AgentSession.abort({ timeoutMs }).
|
|
4322
|
+
const disposeIdleSettled = await Promise.race([
|
|
4323
|
+
this.agent.waitForIdle().then(
|
|
4324
|
+
() => true,
|
|
4325
|
+
() => true,
|
|
4326
|
+
),
|
|
4327
|
+
Bun.sleep(2_000).then(() => false),
|
|
4328
|
+
]);
|
|
4329
|
+
if (!disposeIdleSettled) this.agent.forceAbort("Session disposed");
|
|
4330
|
+
await admissionClosed;
|
|
4331
|
+
this.#workflowGateEmitter?.fence?.();
|
|
3808
4332
|
this.#pendingBackgroundExchanges = [];
|
|
3809
4333
|
this.yieldQueue.clear();
|
|
4334
|
+
|
|
3810
4335
|
this.agent.setOnBeforeYield(undefined);
|
|
3811
|
-
this.#evalExecutionDisposing = true;
|
|
3812
4336
|
try {
|
|
3813
4337
|
if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
|
|
3814
4338
|
await this.#extensionRunner.emit({ type: "session_shutdown" });
|
|
@@ -3868,12 +4392,14 @@ export class AgentSession {
|
|
|
3868
4392
|
await disposeKernelSessionsByOwner(this.#evalKernelOwnerId);
|
|
3869
4393
|
await disposeVmContextsByOwner(this.#evalKernelOwnerId);
|
|
3870
4394
|
this.#releasePowerAssertion();
|
|
4395
|
+
// Disconnect the agent event listener BEFORE closing session resources so a late
|
|
4396
|
+
// provider/tool message_end cannot append to the closing SessionManager.
|
|
4397
|
+
this.#disconnectFromAgent();
|
|
3871
4398
|
await this.sessionManager.close();
|
|
3872
4399
|
this.#closeAllProviderSessions("dispose");
|
|
3873
4400
|
const hindsightState = this.setHindsightSessionState(undefined);
|
|
3874
4401
|
await hindsightState?.flushRetainQueue();
|
|
3875
4402
|
hindsightState?.dispose();
|
|
3876
|
-
this.#disconnectFromAgent();
|
|
3877
4403
|
if (this.#unsubscribeAppendOnly) {
|
|
3878
4404
|
this.#unsubscribeAppendOnly();
|
|
3879
4405
|
this.#unsubscribeAppendOnly = undefined;
|
|
@@ -3926,20 +4452,27 @@ export class AgentSession {
|
|
|
3926
4452
|
await Promise.race([work, Bun.sleep(timeoutMs)]);
|
|
3927
4453
|
}
|
|
3928
4454
|
|
|
3929
|
-
#
|
|
3930
|
-
|
|
4455
|
+
#rebindProviderSessionState(providerSessionState: Map<string, ProviderSessionState>): void {
|
|
4456
|
+
this.#providerSessionState = providerSessionState;
|
|
4457
|
+
this.agent.providerSessionState = providerSessionState;
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
#closeProviderSessionMap(providerSessionState: Map<string, ProviderSessionState>, reason: string): void {
|
|
4461
|
+
for (const [providerKey, state] of providerSessionState) {
|
|
3931
4462
|
try {
|
|
3932
4463
|
state.close();
|
|
3933
4464
|
} catch (error) {
|
|
3934
|
-
logger.warn("Failed to close provider session state", {
|
|
3935
|
-
providerKey,
|
|
3936
|
-
reason,
|
|
3937
|
-
error: String(error),
|
|
3938
|
-
});
|
|
4465
|
+
logger.warn("Failed to close provider session state", { providerKey, reason, error: String(error) });
|
|
3939
4466
|
}
|
|
3940
4467
|
}
|
|
4468
|
+
providerSessionState.clear();
|
|
4469
|
+
}
|
|
3941
4470
|
|
|
3942
|
-
|
|
4471
|
+
#closeAllProviderSessions(reason: string): void {
|
|
4472
|
+
const maps = new Set<Map<string, ProviderSessionState>>([this.#providerSessionState]);
|
|
4473
|
+
for (const scope of this.#temporaryProviderSessionScopes) maps.add(scope.providerSessionState);
|
|
4474
|
+
this.#temporaryProviderSessionScopes = [];
|
|
4475
|
+
for (const providerSessionState of maps) this.#closeProviderSessionMap(providerSessionState, reason);
|
|
3943
4476
|
}
|
|
3944
4477
|
|
|
3945
4478
|
// =========================================================================
|
|
@@ -4031,8 +4564,8 @@ export class AgentSession {
|
|
|
4031
4564
|
}
|
|
4032
4565
|
|
|
4033
4566
|
/** Single point for invalidating cached discovery indices. Call after any change that can
|
|
4034
|
-
* affect which tools should be discoverable: registry mutations (refreshMCPTools
|
|
4035
|
-
*
|
|
4567
|
+
* affect which tools should be discoverable: registry mutations (refreshMCPTools)
|
|
4568
|
+
* or active-tool mutations (#applyActiveToolsByName). */
|
|
4036
4569
|
#invalidateDiscoveryCaches(): void {
|
|
4037
4570
|
this.#discoverableMCPSearchIndex = null;
|
|
4038
4571
|
this.#discoverableToolSearchIndex = null;
|
|
@@ -4136,6 +4669,35 @@ export class AgentSession {
|
|
|
4136
4669
|
return this.#foregroundBashBackgroundRequestHandler !== undefined;
|
|
4137
4670
|
}
|
|
4138
4671
|
|
|
4672
|
+
/** Set the SDK permission policy used by guarded ACP tool execution. */
|
|
4673
|
+
setSdkPermissionMode(mode: "prompt" | "allow" | "deny"): void {
|
|
4674
|
+
this.#sdkPermissionMode = mode;
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
/** Current SDK permission policy for guarded ACP tool execution. */
|
|
4678
|
+
get sdkPermissionMode(): "prompt" | "allow" | "deny" {
|
|
4679
|
+
return this.#sdkPermissionMode;
|
|
4680
|
+
}
|
|
4681
|
+
|
|
4682
|
+
/** Register or clear the SDK reverse permission provider for this session. */
|
|
4683
|
+
setSdkPermissionProvider(
|
|
4684
|
+
provider:
|
|
4685
|
+
| ((
|
|
4686
|
+
toolCall: ClientBridgePermissionToolCall,
|
|
4687
|
+
options: ClientBridgePermissionOption[],
|
|
4688
|
+
signal?: AbortSignal,
|
|
4689
|
+
) => Promise<ClientBridgePermissionOutcome>)
|
|
4690
|
+
| undefined,
|
|
4691
|
+
): void {
|
|
4692
|
+
this.#sdkPermissionProvider = provider;
|
|
4693
|
+
this.#acpPermissionDecisions.clear();
|
|
4694
|
+
this.#acpPermissionWrapperVersion++;
|
|
4695
|
+
const activeTools = this.getActiveToolNames()
|
|
4696
|
+
.map(name => this.#toolRegistry.get(name))
|
|
4697
|
+
.filter((tool): tool is AgentTool => tool !== undefined);
|
|
4698
|
+
this.#setGuardedAgentTools(activeTools);
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4139
4701
|
/**
|
|
4140
4702
|
* Ask the active managed foreground bash call to return as a background job.
|
|
4141
4703
|
* Returns false when no supported foreground tool is currently backgroundable.
|
|
@@ -4165,6 +4727,55 @@ export class AgentSession {
|
|
|
4165
4727
|
return resolveEditMode(this.#getEditModeSession());
|
|
4166
4728
|
}
|
|
4167
4729
|
|
|
4730
|
+
#resolveEditModeForModel(model: Model): EditMode {
|
|
4731
|
+
return resolveEditMode({
|
|
4732
|
+
settings: this.settings,
|
|
4733
|
+
getActiveModelString: () => formatModelString(model),
|
|
4734
|
+
});
|
|
4735
|
+
}
|
|
4736
|
+
|
|
4737
|
+
async #prepareDefaultModelSelectionPrompt(model: Model): Promise<string[] | undefined> {
|
|
4738
|
+
if (!this.#rebuildSystemPrompt) return undefined;
|
|
4739
|
+
if (!this.getActiveToolNames().includes("edit")) return undefined;
|
|
4740
|
+
if (this.#resolveActiveEditMode() === this.#resolveEditModeForModel(model)) return undefined;
|
|
4741
|
+
const built = await this.#rebuildSystemPrompt(this.getActiveToolNames(), this.#toolRegistry, model);
|
|
4742
|
+
return built.systemPrompt;
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
#reserveBaseSystemPromptGeneration(): number {
|
|
4746
|
+
this.#baseSystemPromptGeneration++;
|
|
4747
|
+
return this.#baseSystemPromptGeneration;
|
|
4748
|
+
}
|
|
4749
|
+
|
|
4750
|
+
async #runAdmittedBaseSystemPromptRebuild<T>(build: () => Promise<T>): Promise<T> {
|
|
4751
|
+
const completion = Promise.withResolvers<void>();
|
|
4752
|
+
this.#pendingBaseSystemPromptRebuilds.add(completion.promise);
|
|
4753
|
+
try {
|
|
4754
|
+
return await build();
|
|
4755
|
+
} finally {
|
|
4756
|
+
completion.resolve();
|
|
4757
|
+
this.#pendingBaseSystemPromptRebuilds.delete(completion.promise);
|
|
4758
|
+
}
|
|
4759
|
+
}
|
|
4760
|
+
|
|
4761
|
+
async #waitForAdmittedBaseSystemPromptRebuilds(): Promise<void> {
|
|
4762
|
+
while (this.#pendingBaseSystemPromptRebuilds.size > 0) {
|
|
4763
|
+
await Promise.all(this.#pendingBaseSystemPromptRebuilds);
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
|
|
4767
|
+
#applyPreparedDefaultModelSelectionPrompt(systemPrompt: string[] | undefined): void {
|
|
4768
|
+
if (!systemPrompt) return;
|
|
4769
|
+
this.#reserveBaseSystemPromptGeneration();
|
|
4770
|
+
this.#baseSystemPrompt = systemPrompt;
|
|
4771
|
+
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
4772
|
+
const activeToolNames = this.getActiveToolNames();
|
|
4773
|
+
const activeTools = activeToolNames
|
|
4774
|
+
.map(name => this.#toolRegistry.get(name))
|
|
4775
|
+
.filter((tool): tool is AgentTool => tool != null);
|
|
4776
|
+
this.#lastAppliedToolSignature = this.#computeAppliedToolSignature(activeToolNames, activeTools);
|
|
4777
|
+
}
|
|
4778
|
+
|
|
4168
4779
|
async #syncEditToolModeAfterModelChange(previousEditMode: EditMode): Promise<void> {
|
|
4169
4780
|
const currentEditMode = this.#resolveActiveEditMode();
|
|
4170
4781
|
if (previousEditMode !== currentEditMode && this.getActiveToolNames().includes("edit")) {
|
|
@@ -4335,16 +4946,20 @@ export class AgentSession {
|
|
|
4335
4946
|
return [...new Set(activated)];
|
|
4336
4947
|
}
|
|
4337
4948
|
|
|
4338
|
-
/**
|
|
4339
|
-
* Wrap a tool with a permission-gate proxy when an ACP client is connected.
|
|
4340
|
-
* Only wraps tools whose name is in PERMISSION_REQUIRED_TOOLS and only when
|
|
4341
|
-
* the bridge exposes `requestPermission`. No-ops for all other cases.
|
|
4342
|
-
*/
|
|
4949
|
+
/** Wrap guarded tools so SDK permission modes remain fail-closed without a reverse provider. */
|
|
4343
4950
|
#wrapToolForAcpPermission<T extends AgentTool>(tool: T): T {
|
|
4344
4951
|
const bridge = this.#clientBridge;
|
|
4345
|
-
|
|
4346
|
-
|
|
4952
|
+
const requestPermission =
|
|
4953
|
+
this.#sdkPermissionProvider ??
|
|
4954
|
+
(bridge?.capabilities.requestPermission && bridge.requestPermission
|
|
4955
|
+
? (
|
|
4956
|
+
toolCall: ClientBridgePermissionToolCall,
|
|
4957
|
+
options: ClientBridgePermissionOption[],
|
|
4958
|
+
signal?: AbortSignal,
|
|
4959
|
+
) => bridge.requestPermission!(toolCall, options, signal)
|
|
4960
|
+
: undefined);
|
|
4347
4961
|
if (!PERMISSION_REQUIRED_TOOLS.has(tool.name)) return tool;
|
|
4962
|
+
|
|
4348
4963
|
return new Proxy(tool, {
|
|
4349
4964
|
get: (target, prop) => {
|
|
4350
4965
|
if (prop !== "execute") return Reflect.get(target, prop, target);
|
|
@@ -4367,6 +4982,17 @@ export class AgentSession {
|
|
|
4367
4982
|
const commandContent = command
|
|
4368
4983
|
? [{ type: "content" as const, content: { type: "text" as const, text: `$ ${command}` } }]
|
|
4369
4984
|
: undefined;
|
|
4985
|
+
if (this.#sdkPermissionMode === "allow") {
|
|
4986
|
+
return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
|
|
4987
|
+
}
|
|
4988
|
+
if (this.#sdkPermissionMode === "deny") {
|
|
4989
|
+
throw new ToolError(`Tool call rejected by session permission policy (${target.name})`);
|
|
4990
|
+
}
|
|
4991
|
+
if (!requestPermission) {
|
|
4992
|
+
throw new ToolError(
|
|
4993
|
+
`Tool call rejected because no permission provider is connected (${target.name})`,
|
|
4994
|
+
);
|
|
4995
|
+
}
|
|
4370
4996
|
// Short-circuit on persisted decisions.
|
|
4371
4997
|
const persisted = this.#acpPermissionDecisions.get(permissionIntent.cacheKey);
|
|
4372
4998
|
if (persisted === "allow_always") {
|
|
@@ -4386,7 +5012,7 @@ export class AgentSession {
|
|
|
4386
5012
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4387
5013
|
let raced: PermissionRaceResult;
|
|
4388
5014
|
try {
|
|
4389
|
-
const permissionPromise =
|
|
5015
|
+
const permissionPromise = requestPermission(
|
|
4390
5016
|
{
|
|
4391
5017
|
toolCallId,
|
|
4392
5018
|
toolName: target.name,
|
|
@@ -4465,13 +5091,14 @@ export class AgentSession {
|
|
|
4465
5091
|
#guardedToolWrapperCacheKey(): string {
|
|
4466
5092
|
const bridge = this.#clientBridge;
|
|
4467
5093
|
const acpEnabled = Boolean(bridge?.capabilities.requestPermission && bridge.requestPermission);
|
|
5094
|
+
const sdkEnabled = this.#sdkPermissionProvider !== undefined;
|
|
4468
5095
|
const activeSkill = this.#activeSkillState?.skill ?? "";
|
|
4469
5096
|
const activeSkillSession = this.#activeSkillState?.sessionId ?? "";
|
|
4470
5097
|
return [
|
|
4471
5098
|
"deep-interview-mutation-v1",
|
|
4472
5099
|
"ultragoal-ask-v1",
|
|
4473
5100
|
`active=${activeSkill}:${activeSkillSession}`,
|
|
4474
|
-
`acp=${acpEnabled ? "on" : "off"}:${this.#acpPermissionWrapperVersion}`,
|
|
5101
|
+
`acp=${acpEnabled ? "on" : "off"}:sdk=${sdkEnabled ? "on" : "off"}:${this.#acpPermissionWrapperVersion}`,
|
|
4475
5102
|
].join("|");
|
|
4476
5103
|
}
|
|
4477
5104
|
|
|
@@ -4532,6 +5159,13 @@ export class AgentSession {
|
|
|
4532
5159
|
this.#selectedDiscoveredToolNames.delete(name);
|
|
4533
5160
|
}
|
|
4534
5161
|
}
|
|
5162
|
+
const signature = this.#computeAppliedToolSignature(validToolNames, tools);
|
|
5163
|
+
const promptRelevantToolsChanged =
|
|
5164
|
+
signature !== (this.#pendingAppliedToolSignature ?? this.#lastAppliedToolSignature);
|
|
5165
|
+
if (promptRelevantToolsChanged) {
|
|
5166
|
+
this.#defaultModelSelectionMutationRevision++;
|
|
5167
|
+
this.#pendingAppliedToolSignature = signature;
|
|
5168
|
+
}
|
|
4535
5169
|
this.#setGuardedAgentTools(tools);
|
|
4536
5170
|
|
|
4537
5171
|
// Active tool set changed → discoverable tool list (which excludes already-active tools)
|
|
@@ -4543,14 +5177,27 @@ export class AgentSession {
|
|
|
4543
5177
|
// `refreshMCPTools` -> `#applyActiveToolsByName` even though the resulting
|
|
4544
5178
|
// tool list is byte-identical. Skipping the rebuild keeps the system prompt
|
|
4545
5179
|
// stable, which is required for Anthropic prompt caching to keep hitting.
|
|
4546
|
-
if (this.#rebuildSystemPrompt) {
|
|
4547
|
-
const
|
|
4548
|
-
|
|
4549
|
-
const built = await this.#
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
this.#
|
|
5180
|
+
if (promptRelevantToolsChanged && this.#rebuildSystemPrompt) {
|
|
5181
|
+
const generation = this.#reserveBaseSystemPromptGeneration();
|
|
5182
|
+
try {
|
|
5183
|
+
const built = await this.#runAdmittedBaseSystemPromptRebuild(() =>
|
|
5184
|
+
this.#rebuildSystemPrompt!(validToolNames, this.#toolRegistry),
|
|
5185
|
+
);
|
|
5186
|
+
if (generation === this.#baseSystemPromptGeneration) {
|
|
5187
|
+
this.#baseSystemPrompt = built.systemPrompt;
|
|
5188
|
+
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
5189
|
+
this.#lastAppliedToolSignature = signature;
|
|
5190
|
+
this.#pendingAppliedToolSignature = undefined;
|
|
5191
|
+
}
|
|
5192
|
+
} catch (error) {
|
|
5193
|
+
if (generation === this.#baseSystemPromptGeneration) {
|
|
5194
|
+
this.#pendingAppliedToolSignature = undefined;
|
|
5195
|
+
}
|
|
5196
|
+
throw error;
|
|
4553
5197
|
}
|
|
5198
|
+
} else if (promptRelevantToolsChanged) {
|
|
5199
|
+
this.#lastAppliedToolSignature = signature;
|
|
5200
|
+
this.#pendingAppliedToolSignature = undefined;
|
|
4554
5201
|
}
|
|
4555
5202
|
if (options?.persistMCPSelection !== false) {
|
|
4556
5203
|
this.#persistSelectedMCPToolNamesIfChanged(previousSelectedMCPToolNames);
|
|
@@ -4629,7 +5276,20 @@ export class AgentSession {
|
|
|
4629
5276
|
async refreshBaseSystemPrompt(): Promise<void> {
|
|
4630
5277
|
if (!this.#rebuildSystemPrompt) return;
|
|
4631
5278
|
const activeToolNames = this.getActiveToolNames();
|
|
4632
|
-
const
|
|
5279
|
+
const generation = this.#reserveBaseSystemPromptGeneration();
|
|
5280
|
+
this.#defaultModelSelectionMutationRevision++;
|
|
5281
|
+
let built: { systemPrompt: string[] };
|
|
5282
|
+
try {
|
|
5283
|
+
built = await this.#runAdmittedBaseSystemPromptRebuild(() =>
|
|
5284
|
+
this.#rebuildSystemPrompt!(activeToolNames, this.#toolRegistry),
|
|
5285
|
+
);
|
|
5286
|
+
} catch (error) {
|
|
5287
|
+
if (generation === this.#baseSystemPromptGeneration) {
|
|
5288
|
+
this.#pendingAppliedToolSignature = undefined;
|
|
5289
|
+
}
|
|
5290
|
+
throw error;
|
|
5291
|
+
}
|
|
5292
|
+
if (generation !== this.#baseSystemPromptGeneration) return;
|
|
4633
5293
|
this.#baseSystemPrompt = built.systemPrompt;
|
|
4634
5294
|
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
4635
5295
|
// Refresh the cached signature so a subsequent `#applyActiveToolsByName` with
|
|
@@ -4639,6 +5299,7 @@ export class AgentSession {
|
|
|
4639
5299
|
.map(name => this.#toolRegistry.get(name))
|
|
4640
5300
|
.filter((tool): tool is AgentTool => tool != null);
|
|
4641
5301
|
this.#lastAppliedToolSignature = this.#computeAppliedToolSignature(activeToolNames, activeTools);
|
|
5302
|
+
this.#pendingAppliedToolSignature = undefined;
|
|
4642
5303
|
}
|
|
4643
5304
|
|
|
4644
5305
|
async #buildSystemPromptForAgentStart(promptText: string): Promise<string[]> {
|
|
@@ -4686,7 +5347,7 @@ export class AgentSession {
|
|
|
4686
5347
|
* cache per-tool strings without preserving this property.
|
|
4687
5348
|
*
|
|
4688
5349
|
* Inputs NOT covered: tool input schemas; memory instructions read from disk;
|
|
4689
|
-
* and SDK-init-time closure constants in `sdk.ts` (`repeatToolDescriptions`,
|
|
5350
|
+
* and SDK-init-time closure constants in `sdk/session.ts` (`repeatToolDescriptions`,
|
|
4690
5351
|
* `eagerTasks`, `intentField`, `mcpDiscoveryEnabled`, `secretsEnabled`). The
|
|
4691
5352
|
* closure-captured ones cannot change at runtime regardless of skip behavior.
|
|
4692
5353
|
* For everything else, callers must explicitly call `refreshBaseSystemPrompt()`
|
|
@@ -4784,57 +5445,6 @@ export class AgentSession {
|
|
|
4784
5445
|
await this.#applyActiveToolsByName(nextActive, { previousSelectedMCPToolNames });
|
|
4785
5446
|
}
|
|
4786
5447
|
|
|
4787
|
-
/**
|
|
4788
|
-
* Replace RPC host-owned tools and refresh the active tool set before the next model call.
|
|
4789
|
-
*/
|
|
4790
|
-
async refreshRpcHostTools(rpcTools: AgentTool[]): Promise<void> {
|
|
4791
|
-
const nextToolNames = rpcTools.map(tool => tool.name);
|
|
4792
|
-
const uniqueToolNames = new Set(nextToolNames);
|
|
4793
|
-
if (uniqueToolNames.size !== nextToolNames.length) {
|
|
4794
|
-
throw new Error("RPC host tool names must be unique");
|
|
4795
|
-
}
|
|
4796
|
-
if (uniqueToolNames.has("ask")) {
|
|
4797
|
-
throw new Error('RPC host tool "ask" is reserved and cannot be supplied by the host');
|
|
4798
|
-
}
|
|
4799
|
-
|
|
4800
|
-
for (const name of uniqueToolNames) {
|
|
4801
|
-
if (this.#toolRegistry.has(name) && !this.#rpcHostToolNames.has(name)) {
|
|
4802
|
-
throw new Error(`RPC host tool "${name}" conflicts with an existing tool`);
|
|
4803
|
-
}
|
|
4804
|
-
}
|
|
4805
|
-
|
|
4806
|
-
const previousRpcHostToolNames = new Set(this.#rpcHostToolNames);
|
|
4807
|
-
const previousActiveToolNames = this.getActiveToolNames();
|
|
4808
|
-
for (const name of previousRpcHostToolNames) {
|
|
4809
|
-
this.#toolRegistry.delete(name);
|
|
4810
|
-
}
|
|
4811
|
-
this.#rpcHostToolNames.clear();
|
|
4812
|
-
|
|
4813
|
-
for (const tool of rpcTools) {
|
|
4814
|
-
const finalTool = (
|
|
4815
|
-
this.#extensionRunner ? new ExtensionToolWrapper(tool, this.#extensionRunner) : tool
|
|
4816
|
-
) as AgentTool;
|
|
4817
|
-
this.#toolRegistry.set(finalTool.name, finalTool);
|
|
4818
|
-
this.#rpcHostToolNames.add(finalTool.name);
|
|
4819
|
-
}
|
|
4820
|
-
|
|
4821
|
-
// Registry contents changed — invalidate discovery caches so the next BM25 lookup sees
|
|
4822
|
-
// the new RPC-host tool set. (#applyActiveToolsByName below also invalidates, but doing
|
|
4823
|
-
// it here too keeps the contract local to "registry mutated".)
|
|
4824
|
-
this.#invalidateDiscoveryCaches();
|
|
4825
|
-
|
|
4826
|
-
const activeNonRpcToolNames = previousActiveToolNames.filter(name => !previousRpcHostToolNames.has(name));
|
|
4827
|
-
const preservedRpcToolNames = previousActiveToolNames.filter(
|
|
4828
|
-
name => previousRpcHostToolNames.has(name) && this.#rpcHostToolNames.has(name),
|
|
4829
|
-
);
|
|
4830
|
-
const autoActivatedRpcToolNames = rpcTools
|
|
4831
|
-
.filter(tool => !tool.hidden && !previousRpcHostToolNames.has(tool.name))
|
|
4832
|
-
.map(tool => tool.name);
|
|
4833
|
-
await this.#applyActiveToolsByName(
|
|
4834
|
-
Array.from(new Set([...activeNonRpcToolNames, ...preservedRpcToolNames, ...autoActivatedRpcToolNames])),
|
|
4835
|
-
);
|
|
4836
|
-
}
|
|
4837
|
-
|
|
4838
5448
|
async #hasActiveGjcSubskillTools(parent: string, sessionId: string | undefined): Promise<boolean> {
|
|
4839
5449
|
if (!parent.trim()) return false;
|
|
4840
5450
|
const cwd = this.sessionManager.getCwd();
|
|
@@ -4979,7 +5589,13 @@ export class AgentSession {
|
|
|
4979
5589
|
if (!canContinuePersistedHistory(this.agent.state.messages)) {
|
|
4980
5590
|
throw new Error("Cannot continue from persisted message history");
|
|
4981
5591
|
}
|
|
4982
|
-
|
|
5592
|
+
this.#beginInFlight();
|
|
5593
|
+
try {
|
|
5594
|
+
await this.agent.continue(this.#managedFallbackPromptOptions());
|
|
5595
|
+
await this.#waitForPostPromptRecovery();
|
|
5596
|
+
} finally {
|
|
5597
|
+
this.#endInFlight();
|
|
5598
|
+
}
|
|
4983
5599
|
}
|
|
4984
5600
|
|
|
4985
5601
|
buildDisplaySessionContext(): SessionContext {
|
|
@@ -5090,6 +5706,19 @@ export class AgentSession {
|
|
|
5090
5706
|
return this.#planModeState;
|
|
5091
5707
|
}
|
|
5092
5708
|
|
|
5709
|
+
/** Live SDK configuration values exposed through the session query surface. */
|
|
5710
|
+
getSdkConfigItems(): Record<string, string> {
|
|
5711
|
+
const model = this.model;
|
|
5712
|
+
return {
|
|
5713
|
+
mode: this.#planModeState?.enabled ? "plan" : "default",
|
|
5714
|
+
...(model ? { model: `${model.provider}/${model.id}` } : {}),
|
|
5715
|
+
thinking: this.#thinkingLevel ?? "off",
|
|
5716
|
+
steeringMode: this.steeringMode,
|
|
5717
|
+
followUpMode: this.followUpMode,
|
|
5718
|
+
interruptMode: this.interruptMode,
|
|
5719
|
+
};
|
|
5720
|
+
}
|
|
5721
|
+
|
|
5093
5722
|
setPlanModeState(state: PlanModeState | undefined): void {
|
|
5094
5723
|
this.#planModeState = state;
|
|
5095
5724
|
if (state?.enabled) {
|
|
@@ -5098,6 +5727,114 @@ export class AgentSession {
|
|
|
5098
5727
|
}
|
|
5099
5728
|
}
|
|
5100
5729
|
|
|
5730
|
+
async invokeSkill(
|
|
5731
|
+
name: string,
|
|
5732
|
+
args = "",
|
|
5733
|
+
): Promise<{ name: string; path: string; args?: string; lineCount?: number }> {
|
|
5734
|
+
const skillName = name.trim();
|
|
5735
|
+
if (!skillName) throw Object.assign(new Error("skill.invoke requires a skill name."), { code: "invalid_input" });
|
|
5736
|
+
if (typeof args !== "string")
|
|
5737
|
+
throw Object.assign(new Error("skill.invoke args must be a string."), { code: "invalid_input" });
|
|
5738
|
+
const skill = this.skills.find(candidate => candidate.name === skillName);
|
|
5739
|
+
if (!skill) throw Object.assign(new Error(`Skill ${skillName} was not found.`), { code: "invalid_input" });
|
|
5740
|
+
const activation = await resolveSubskillActivationForSkillInvocation({
|
|
5741
|
+
cwd: this.sessionManager.getCwd(),
|
|
5742
|
+
sessionId: this.sessionId,
|
|
5743
|
+
skillName: skill.name,
|
|
5744
|
+
args,
|
|
5745
|
+
});
|
|
5746
|
+
const built = await buildSkillPromptMessage(skill, activation.cleanedArgs, {
|
|
5747
|
+
subskillActivation: activation.activation,
|
|
5748
|
+
subskillActivationSet: activation.activeSubskillsToPersist,
|
|
5749
|
+
cwd: this.sessionManager.getCwd(),
|
|
5750
|
+
sessionId: this.sessionId,
|
|
5751
|
+
});
|
|
5752
|
+
await this.promptCustomMessage({
|
|
5753
|
+
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
5754
|
+
content: built.message,
|
|
5755
|
+
display: true,
|
|
5756
|
+
details: built.details,
|
|
5757
|
+
attribution: "user",
|
|
5758
|
+
});
|
|
5759
|
+
return {
|
|
5760
|
+
name: skill.name,
|
|
5761
|
+
path: skill.filePath,
|
|
5762
|
+
args: activation.cleanedArgs || undefined,
|
|
5763
|
+
lineCount: built.details.lineCount,
|
|
5764
|
+
};
|
|
5765
|
+
}
|
|
5766
|
+
|
|
5767
|
+
setSdkPlanMode(on: boolean): PlanModeState | undefined {
|
|
5768
|
+
if (typeof on !== "boolean")
|
|
5769
|
+
throw Object.assign(new Error("mode.plan.set requires a boolean on value."), { code: "invalid_input" });
|
|
5770
|
+
if (!on) {
|
|
5771
|
+
this.setPlanModeState(undefined);
|
|
5772
|
+
return undefined;
|
|
5773
|
+
}
|
|
5774
|
+
const state: PlanModeState = { enabled: true, planFilePath: "local://PLAN.md", workflow: "parallel" };
|
|
5775
|
+
this.setPlanModeState(state);
|
|
5776
|
+
return state;
|
|
5777
|
+
}
|
|
5778
|
+
|
|
5779
|
+
async operateGoal(
|
|
5780
|
+
op: "create" | "get" | "resume" | "pause" | "complete" | "drop",
|
|
5781
|
+
objective?: string,
|
|
5782
|
+
): Promise<unknown> {
|
|
5783
|
+
try {
|
|
5784
|
+
switch (op) {
|
|
5785
|
+
case "create":
|
|
5786
|
+
return await this.#goalRuntime.createGoal({ objective: objective ?? "" });
|
|
5787
|
+
case "get":
|
|
5788
|
+
return this.getGoalModeState();
|
|
5789
|
+
case "resume":
|
|
5790
|
+
return await this.#goalRuntime.resumeGoal();
|
|
5791
|
+
case "pause":
|
|
5792
|
+
return await this.#goalRuntime.pauseGoal();
|
|
5793
|
+
case "complete":
|
|
5794
|
+
return await this.#goalRuntime.completeGoalFromTool();
|
|
5795
|
+
case "drop":
|
|
5796
|
+
return await this.#goalRuntime.dropGoal();
|
|
5797
|
+
}
|
|
5798
|
+
} catch (error) {
|
|
5799
|
+
throw Object.assign(new Error(error instanceof Error ? error.message : "Goal operation failed."), {
|
|
5800
|
+
code: "invalid_input",
|
|
5801
|
+
});
|
|
5802
|
+
}
|
|
5803
|
+
}
|
|
5804
|
+
|
|
5805
|
+
getTranscript(): Array<{ id: string; role: string; textSummary: string; ts: string; body: string }> {
|
|
5806
|
+
return this.sessionManager.getEntries().flatMap(entry => {
|
|
5807
|
+
if (entry.type !== "message") return [];
|
|
5808
|
+
const message = entry.message as unknown as { role?: unknown; content?: unknown };
|
|
5809
|
+
const body =
|
|
5810
|
+
typeof message.content === "string"
|
|
5811
|
+
? message.content
|
|
5812
|
+
: Array.isArray(message.content)
|
|
5813
|
+
? message.content
|
|
5814
|
+
.map(part =>
|
|
5815
|
+
typeof part === "object" && part !== null && "text" in part && typeof part.text === "string"
|
|
5816
|
+
? part.text
|
|
5817
|
+
: "",
|
|
5818
|
+
)
|
|
5819
|
+
.filter(Boolean)
|
|
5820
|
+
.join("\n")
|
|
5821
|
+
: "";
|
|
5822
|
+
return [
|
|
5823
|
+
{
|
|
5824
|
+
id: entry.id,
|
|
5825
|
+
role: typeof message.role === "string" ? message.role : "unknown",
|
|
5826
|
+
textSummary: body.slice(0, 500),
|
|
5827
|
+
ts: entry.timestamp,
|
|
5828
|
+
body,
|
|
5829
|
+
},
|
|
5830
|
+
];
|
|
5831
|
+
});
|
|
5832
|
+
}
|
|
5833
|
+
|
|
5834
|
+
getTranscriptBody(entryId: string): string | undefined {
|
|
5835
|
+
return this.getTranscript().find(entry => entry.id === entryId)?.body;
|
|
5836
|
+
}
|
|
5837
|
+
|
|
5101
5838
|
getGoalModeState(): GoalModeState | undefined {
|
|
5102
5839
|
return this.#goalModeState;
|
|
5103
5840
|
}
|
|
@@ -5114,6 +5851,36 @@ export class AgentSession {
|
|
|
5114
5851
|
return getAskAnswerSourceFromRegistry(this.sessionId);
|
|
5115
5852
|
}
|
|
5116
5853
|
|
|
5854
|
+
#bindWorkflowGateEmitter(previousSessionId?: string, previousEmitter = this.#workflowGateEmitter): void {
|
|
5855
|
+
const sessionId = this.sessionManager.getSessionId();
|
|
5856
|
+
assertNonEmptyGjcSessionId(sessionId, "AgentSession workflow-gate session");
|
|
5857
|
+
const gateStore = this.sessionManager.isPersisted()
|
|
5858
|
+
? new FileGateStore(path.join(sessionStateDir(this.sessionManager.getCwd(), sessionId), "workflow-gates.json"))
|
|
5859
|
+
: new MemoryGateStore();
|
|
5860
|
+
const successorEmitter = new BrokerWorkflowGateEmitter(sessionId, gateStore);
|
|
5861
|
+
previousEmitter?.fence?.();
|
|
5862
|
+
if (previousEmitter && !previousEmitter.fence) {
|
|
5863
|
+
for (const gate of previousEmitter.listPendingGates?.() ?? []) previousEmitter.quarantineGate?.(gate.gate_id);
|
|
5864
|
+
}
|
|
5865
|
+
if (previousSessionId) notifyWorkflowGateEmitterChanged(previousSessionId, undefined);
|
|
5866
|
+
this.setWorkflowGateEmitter(successorEmitter);
|
|
5867
|
+
}
|
|
5868
|
+
|
|
5869
|
+
#suspendWorkflowGateEmitter(sessionId: string): WorkflowGateEmitter | undefined {
|
|
5870
|
+
const emitter = this.#workflowGateEmitter;
|
|
5871
|
+
if (!emitter) return undefined;
|
|
5872
|
+
emitter.suspend?.();
|
|
5873
|
+
this.#workflowGateEmitter = undefined;
|
|
5874
|
+
notifyWorkflowGateEmitterChanged(sessionId, undefined);
|
|
5875
|
+
return emitter;
|
|
5876
|
+
}
|
|
5877
|
+
|
|
5878
|
+
#restoreWorkflowGateEmitter(emitter: WorkflowGateEmitter | undefined): void {
|
|
5879
|
+
if (!emitter) return;
|
|
5880
|
+
emitter.resume?.();
|
|
5881
|
+
this.setWorkflowGateEmitter(emitter);
|
|
5882
|
+
}
|
|
5883
|
+
|
|
5117
5884
|
setWorkflowGateEmitter(emitter: WorkflowGateEmitter | undefined): void {
|
|
5118
5885
|
this.#workflowGateEmitter = emitter;
|
|
5119
5886
|
notifyWorkflowGateEmitterChanged(this.sessionId, emitter);
|
|
@@ -5504,48 +6271,60 @@ export class AgentSession {
|
|
|
5504
6271
|
if (workflowIntentDiff) {
|
|
5505
6272
|
this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
|
|
5506
6273
|
}
|
|
6274
|
+
options?.onPreflightAccepted?.();
|
|
5507
6275
|
return;
|
|
5508
6276
|
}
|
|
5509
6277
|
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
6278
|
+
const admissionGeneration = this.#promptGeneration;
|
|
6279
|
+
const admissionSignal = this.#promptPreflightAbortController.signal;
|
|
6280
|
+
await this.#withSessionAdmission("prompt", async admission => {
|
|
6281
|
+
this.#throwIfPromptPreflightCancelled(admissionGeneration, admissionSignal);
|
|
6282
|
+
if (workflowIntentDiff) {
|
|
6283
|
+
this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
|
|
6284
|
+
}
|
|
5513
6285
|
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
6286
|
+
// Skip eager todo prelude when the user has already queued a directive
|
|
6287
|
+
const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
|
|
6288
|
+
const eagerTodoPrelude =
|
|
6289
|
+
!options?.synthetic && !hasPendingUserDirective ? this.#createEagerTodoPrelude(expandedText) : undefined;
|
|
5518
6290
|
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
6291
|
+
const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
|
|
6292
|
+
if (options?.images) {
|
|
6293
|
+
userContent.push(...options.images);
|
|
6294
|
+
}
|
|
5523
6295
|
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
6296
|
+
const promptAttribution = options?.attribution ?? (options?.synthetic ? "agent" : "user");
|
|
6297
|
+
const message = options?.synthetic
|
|
6298
|
+
? {
|
|
6299
|
+
role: "developer" as const,
|
|
6300
|
+
content: userContent,
|
|
6301
|
+
attribution: promptAttribution,
|
|
6302
|
+
timestamp: Date.now(),
|
|
6303
|
+
}
|
|
6304
|
+
: { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
|
|
6305
|
+
await this.refreshGjcSubskillTools();
|
|
5529
6306
|
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
6307
|
+
if (eagerTodoPrelude?.toolChoice) {
|
|
6308
|
+
this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
|
|
6309
|
+
label: "eager-todo",
|
|
6310
|
+
});
|
|
6311
|
+
}
|
|
5535
6312
|
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
6313
|
+
try {
|
|
6314
|
+
await this.#promptWithMessage(message, expandedText, {
|
|
6315
|
+
...options,
|
|
6316
|
+
prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
|
|
6317
|
+
admissionLease: admission,
|
|
6318
|
+
});
|
|
6319
|
+
} finally {
|
|
6320
|
+
// Clean up residual eager-todo directive if the prompt never consumed it
|
|
6321
|
+
// (e.g., compaction aborted, validation failed).
|
|
6322
|
+
this.#toolChoiceQueue.removeByLabel("eager-todo");
|
|
6323
|
+
}
|
|
6324
|
+
if (!options?.synthetic) {
|
|
6325
|
+
await this.#enforcePlanModeToolDecision();
|
|
6326
|
+
}
|
|
6327
|
+
});
|
|
5549
6328
|
}
|
|
5550
6329
|
|
|
5551
6330
|
async #syncSkillPromptActiveState(
|
|
@@ -5636,36 +6415,53 @@ export class AgentSession {
|
|
|
5636
6415
|
return;
|
|
5637
6416
|
}
|
|
5638
6417
|
|
|
5639
|
-
const
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
6418
|
+
const admissionGeneration = this.#promptGeneration;
|
|
6419
|
+
const admissionSignal = this.#promptPreflightAbortController.signal;
|
|
6420
|
+
await this.#withSessionAdmission("prompt", async admission => {
|
|
6421
|
+
this.#throwIfPromptPreflightCancelled(admissionGeneration, admissionSignal);
|
|
6422
|
+
const customMessage: CustomMessage<T> = {
|
|
6423
|
+
role: "custom",
|
|
6424
|
+
customType: message.customType,
|
|
6425
|
+
content: message.content,
|
|
6426
|
+
display: message.display,
|
|
6427
|
+
details: message.details,
|
|
6428
|
+
attribution: message.attribution ?? "agent",
|
|
6429
|
+
timestamp: Date.now(),
|
|
6430
|
+
};
|
|
5648
6431
|
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
6432
|
+
await this.#syncSkillPromptActiveStateSafely(customMessage, true);
|
|
6433
|
+
try {
|
|
6434
|
+
await this.#promptWithMessage(customMessage, textContent, { ...options, admissionLease: admission });
|
|
6435
|
+
} finally {
|
|
6436
|
+
await this.#syncSkillPromptActiveStateSafely(customMessage, false);
|
|
6437
|
+
}
|
|
6438
|
+
});
|
|
5655
6439
|
}
|
|
5656
6440
|
|
|
5657
6441
|
async #promptWithMessage(
|
|
5658
6442
|
message: AgentMessage,
|
|
5659
6443
|
expandedText: string,
|
|
5660
|
-
options?: Pick<PromptOptions, "toolChoice" | "images" | "skipCompactionCheck"> & {
|
|
6444
|
+
options?: Pick<PromptOptions, "toolChoice" | "images" | "skipCompactionCheck" | "onPreflightAccepted"> & {
|
|
5661
6445
|
prependMessages?: AgentMessage[];
|
|
5662
6446
|
skipPostPromptRecoveryWait?: boolean;
|
|
6447
|
+
predecessorAgentEndHold?: symbol;
|
|
6448
|
+
admissionLease?: SessionAdmissionLease;
|
|
5663
6449
|
},
|
|
5664
6450
|
): Promise<void> {
|
|
5665
6451
|
this.#beginInFlight();
|
|
6452
|
+
const predecessorAgentEndHold =
|
|
6453
|
+
options?.predecessorAgentEndHold ?? this.#reserveDeferredAgentEndForContinuation();
|
|
5666
6454
|
const generation = this.#promptGeneration;
|
|
6455
|
+
const preflightSignal = this.#promptPreflightAbortController.signal;
|
|
5667
6456
|
const rosterClaim = this.#claimIrcRosterCandidate();
|
|
5668
6457
|
try {
|
|
6458
|
+
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
6459
|
+
if (message.role === "user") {
|
|
6460
|
+
await this.#resetDefaultFallbackForNewTurn();
|
|
6461
|
+
await this.#ensureDefaultFallbackResolution();
|
|
6462
|
+
this.#defaultFallbackChain().resetAttemptBudget();
|
|
6463
|
+
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
6464
|
+
}
|
|
5669
6465
|
// Flush any pending bash messages before the new prompt
|
|
5670
6466
|
this.#flushPendingBashMessages();
|
|
5671
6467
|
this.#flushPendingPythonMessages();
|
|
@@ -5674,8 +6470,6 @@ export class AgentSession {
|
|
|
5674
6470
|
// Reset todo reminder count on new user prompt
|
|
5675
6471
|
this.#todoReminderCount = 0;
|
|
5676
6472
|
|
|
5677
|
-
await this.#maybeRestoreRetryFallbackPrimary();
|
|
5678
|
-
|
|
5679
6473
|
// Validate model
|
|
5680
6474
|
if (!this.model) {
|
|
5681
6475
|
throw new Error(formatNoModelOnboardingError());
|
|
@@ -5727,10 +6521,14 @@ export class AgentSession {
|
|
|
5727
6521
|
|
|
5728
6522
|
messages.push(message);
|
|
5729
6523
|
|
|
5730
|
-
// Early bail-out:
|
|
5731
|
-
//
|
|
5732
|
-
if (this.#
|
|
6524
|
+
// Early bail-out: a generation change or cancellation during setup must
|
|
6525
|
+
// terminate preflight rather than retaining SDK prompt authority.
|
|
6526
|
+
if (this.#isPromptPreflightCancelled(generation, preflightSignal)) {
|
|
5733
6527
|
this.#resetInjectedContextSignatures();
|
|
6528
|
+
// A newer abort/prompt cycle superseded this preflight. Callers awaiting
|
|
6529
|
+
// acceptance (onPreflightAccepted) must be told it never ran; direct
|
|
6530
|
+
// callers (e.g. prompt() aborted during a TTSR wait) resolve gracefully.
|
|
6531
|
+
if (options?.onPreflightAccepted) throw promptPreflightCancelledError();
|
|
5734
6532
|
return;
|
|
5735
6533
|
}
|
|
5736
6534
|
|
|
@@ -5774,12 +6572,13 @@ export class AgentSession {
|
|
|
5774
6572
|
const promptAttribution: "user" | "agent" | undefined =
|
|
5775
6573
|
"attribution" in message ? message.attribution : undefined;
|
|
5776
6574
|
|
|
5777
|
-
// Emit before_agent_start extension event
|
|
6575
|
+
// Emit before_agent_start extension event. Race hook completion with prompt
|
|
6576
|
+
// cancellation so a wedged hook cannot retain SDK prompt authority.
|
|
5778
6577
|
if (this.#extensionRunner) {
|
|
5779
|
-
const result = await this.#
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
beforeAgentStartSystemPrompt,
|
|
6578
|
+
const result = await this.#awaitPromptPreflight(
|
|
6579
|
+
generation,
|
|
6580
|
+
preflightSignal,
|
|
6581
|
+
this.#extensionRunner.emitBeforeAgentStart(expandedText, options?.images, beforeAgentStartSystemPrompt),
|
|
5783
6582
|
);
|
|
5784
6583
|
if (result?.messages) {
|
|
5785
6584
|
this.#appendBeforeAgentStartCustomMessages(messages, result.messages, promptAttribution, message.role);
|
|
@@ -5801,30 +6600,43 @@ export class AgentSession {
|
|
|
5801
6600
|
const contributed: BeforeAgentStartInternalMessage[] = [];
|
|
5802
6601
|
for (const contributor of this.#beforeAgentStartContributors) {
|
|
5803
6602
|
try {
|
|
5804
|
-
const msg = await
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
6603
|
+
const msg = await this.#awaitPromptPreflight(
|
|
6604
|
+
generation,
|
|
6605
|
+
preflightSignal,
|
|
6606
|
+
contributor({
|
|
6607
|
+
prompt: expandedText,
|
|
6608
|
+
images: options?.images,
|
|
6609
|
+
sessionId: this.sessionId,
|
|
6610
|
+
}),
|
|
6611
|
+
);
|
|
5809
6612
|
if (msg) contributed.push(msg);
|
|
5810
6613
|
} catch (err) {
|
|
6614
|
+
if (this.#isPromptPreflightCancelled(generation, preflightSignal))
|
|
6615
|
+
throw promptPreflightCancelledError();
|
|
5811
6616
|
logger.debug("before_agent_start contributor failed", { error: String(err) });
|
|
5812
6617
|
}
|
|
5813
6618
|
}
|
|
5814
6619
|
this.#appendBeforeAgentStartCustomMessages(messages, contributed, promptAttribution, message.role);
|
|
5815
6620
|
}
|
|
5816
6621
|
|
|
5817
|
-
//
|
|
5818
|
-
//
|
|
5819
|
-
|
|
5820
|
-
// aborted turn, so reset them here so the next real turn re-injects once.
|
|
5821
|
-
if (this.#promptGeneration !== generation) {
|
|
6622
|
+
// Abort can race asynchronous preflight work. The injection signatures were
|
|
6623
|
+
// consumed while building context, but no prompt was accepted, so reset them.
|
|
6624
|
+
if (this.#isPromptPreflightCancelled(generation, preflightSignal)) {
|
|
5822
6625
|
this.#resetInjectedContextSignatures();
|
|
6626
|
+
// Ack-waiting callers are told the preflight never ran; direct callers
|
|
6627
|
+
// (aborted after setup) resolve gracefully as before f24f46ff5.
|
|
6628
|
+
if (options?.onPreflightAccepted) throw promptPreflightCancelledError();
|
|
5823
6629
|
return;
|
|
5824
6630
|
}
|
|
5825
6631
|
|
|
5826
|
-
const agentPromptOptions =
|
|
5827
|
-
|
|
6632
|
+
const agentPromptOptions = {
|
|
6633
|
+
...(options?.toolChoice ? { toolChoice: options.toolChoice } : undefined),
|
|
6634
|
+
...this.#managedFallbackPromptOptions(),
|
|
6635
|
+
...(options?.admissionLease ? { onRunAccepted: options.admissionLease.release } : undefined),
|
|
6636
|
+
};
|
|
6637
|
+
options?.onPreflightAccepted?.();
|
|
6638
|
+
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
6639
|
+
await this.#promptAgentWithIdleRetry(messages, agentPromptOptions, predecessorAgentEndHold);
|
|
5828
6640
|
const terminalAssistant = this.#findLastAssistantMessage();
|
|
5829
6641
|
if (
|
|
5830
6642
|
rosterClaim &&
|
|
@@ -5837,6 +6649,12 @@ export class AgentSession {
|
|
|
5837
6649
|
if (!options?.skipPostPromptRecoveryWait) {
|
|
5838
6650
|
await this.#waitForPostPromptRecovery();
|
|
5839
6651
|
}
|
|
6652
|
+
} catch (error) {
|
|
6653
|
+
// Session identity changes historically cancel local setup silently. Only SDK
|
|
6654
|
+
// submissions provide an acceptance callback and require an explicit terminal
|
|
6655
|
+
// preflight failure for their remote request authority.
|
|
6656
|
+
if (isPromptPreflightCancelledError(error) && !options?.onPreflightAccepted) return;
|
|
6657
|
+
throw error;
|
|
5840
6658
|
} finally {
|
|
5841
6659
|
if (rosterClaim) {
|
|
5842
6660
|
this.agent.replaceMessages(
|
|
@@ -5846,6 +6664,7 @@ export class AgentSession {
|
|
|
5846
6664
|
);
|
|
5847
6665
|
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
5848
6666
|
}
|
|
6667
|
+
this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
5849
6668
|
this.#endInFlight();
|
|
5850
6669
|
}
|
|
5851
6670
|
}
|
|
@@ -5898,6 +6717,50 @@ export class AgentSession {
|
|
|
5898
6717
|
void this.abort();
|
|
5899
6718
|
},
|
|
5900
6719
|
hasPendingMessages: () => this.queuedMessageCount > 0,
|
|
6720
|
+
getPendingMessageCounts: () => this.pendingMessageCounts,
|
|
6721
|
+
getTranscript: () => this.getTranscript(),
|
|
6722
|
+
getTranscriptBody: entryId => this.getTranscriptBody(entryId),
|
|
6723
|
+
getGoalState: () => this.getGoalModeState(),
|
|
6724
|
+
getTodoState: () => this.getTodoPhases(),
|
|
6725
|
+
getQueuedMessages: () => this.getQueuedMessageEntries(),
|
|
6726
|
+
getActiveTools: () => this.getActiveToolNames(),
|
|
6727
|
+
getAllTools: () => this.getAllToolNames(),
|
|
6728
|
+
cycleModel: () => this.cycleModel(),
|
|
6729
|
+
cycleThinkingLevel: () => this.cycleThinkingLevel(),
|
|
6730
|
+
setQueueMode: (kind, mode) => {
|
|
6731
|
+
if (kind === "steering" && (mode === "all" || mode === "one-at-a-time")) {
|
|
6732
|
+
this.setSteeringMode(mode);
|
|
6733
|
+
return true;
|
|
6734
|
+
}
|
|
6735
|
+
if (kind === "follow_up" && (mode === "all" || mode === "one-at-a-time")) {
|
|
6736
|
+
this.setFollowUpMode(mode);
|
|
6737
|
+
return true;
|
|
6738
|
+
}
|
|
6739
|
+
if (kind === "interrupt" && (mode === "immediate" || mode === "wait")) {
|
|
6740
|
+
this.setInterruptMode(mode);
|
|
6741
|
+
return true;
|
|
6742
|
+
}
|
|
6743
|
+
return false;
|
|
6744
|
+
},
|
|
6745
|
+
invokeSkill: (name, args) => this.invokeSkill(name, args),
|
|
6746
|
+
setPlanMode: on => this.setSdkPlanMode(on),
|
|
6747
|
+
operateGoal: (op, objective) => this.operateGoal(op, objective),
|
|
6748
|
+
getSkillState: () => this.skills.map(skill => ({ name: skill.name, description: skill.description })),
|
|
6749
|
+
getConfigItems: () => this.getSdkConfigItems(),
|
|
6750
|
+
getBranchCandidates: () => this.sessionManager.getTree(),
|
|
6751
|
+
getExtensions: () => this.#extensionRunner?.getExtensionPaths() ?? [],
|
|
6752
|
+
getArtifact: () => undefined,
|
|
6753
|
+
getJobs: () => undefined,
|
|
6754
|
+
sdkBindings: () => [
|
|
6755
|
+
"cycleModel",
|
|
6756
|
+
"cycleThinkingLevel",
|
|
6757
|
+
"setQueueMode",
|
|
6758
|
+
"getSkillState",
|
|
6759
|
+
"getConfigItems",
|
|
6760
|
+
"getBranchCandidates",
|
|
6761
|
+
"getExtensions",
|
|
6762
|
+
],
|
|
6763
|
+
clearContext: () => this.clearContext(),
|
|
5901
6764
|
shutdown: () => {
|
|
5902
6765
|
void this.dispose();
|
|
5903
6766
|
process.exit(0);
|
|
@@ -6354,7 +7217,7 @@ export class AgentSession {
|
|
|
6354
7217
|
*/
|
|
6355
7218
|
async sendUserMessage(
|
|
6356
7219
|
content: string | (TextContent | ImageContent)[],
|
|
6357
|
-
options?: { deliverAs?: "steer" | "followUp" },
|
|
7220
|
+
options?: { deliverAs?: "steer" | "followUp"; onPreflightAccepted?: () => void },
|
|
6358
7221
|
): Promise<void> {
|
|
6359
7222
|
// Normalize content to text string + optional images
|
|
6360
7223
|
let text: string;
|
|
@@ -6378,10 +7241,12 @@ export class AgentSession {
|
|
|
6378
7241
|
|
|
6379
7242
|
if (options?.deliverAs === "followUp") {
|
|
6380
7243
|
await this.#queueFollowUp(text, images);
|
|
7244
|
+
options.onPreflightAccepted?.();
|
|
6381
7245
|
return;
|
|
6382
7246
|
}
|
|
6383
7247
|
if (options?.deliverAs === "steer") {
|
|
6384
7248
|
await this.#queueSteer(text, images);
|
|
7249
|
+
options.onPreflightAccepted?.();
|
|
6385
7250
|
return;
|
|
6386
7251
|
}
|
|
6387
7252
|
|
|
@@ -6392,6 +7257,7 @@ export class AgentSession {
|
|
|
6392
7257
|
// the message in the steering queue with no turn to consume it.
|
|
6393
7258
|
if (this.isStreaming) {
|
|
6394
7259
|
await this.#queueSteer(text, images);
|
|
7260
|
+
options?.onPreflightAccepted?.();
|
|
6395
7261
|
return;
|
|
6396
7262
|
}
|
|
6397
7263
|
|
|
@@ -6399,6 +7265,7 @@ export class AgentSession {
|
|
|
6399
7265
|
await this.prompt(text, {
|
|
6400
7266
|
expandPromptTemplates: false,
|
|
6401
7267
|
images,
|
|
7268
|
+
onPreflightAccepted: options?.onPreflightAccepted,
|
|
6402
7269
|
});
|
|
6403
7270
|
}
|
|
6404
7271
|
|
|
@@ -6419,6 +7286,14 @@ export class AgentSession {
|
|
|
6419
7286
|
get queuedMessageCount(): number {
|
|
6420
7287
|
return this.#steeringMessages.length + this.#followUpMessages.length + this.#pendingNextTurnMessages.length;
|
|
6421
7288
|
}
|
|
7289
|
+
/** Typed pending-message counts per queue (steering, follow-up, next-turn). */
|
|
7290
|
+
get pendingMessageCounts(): { steering: number; followUp: number; nextTurn: number } {
|
|
7291
|
+
return {
|
|
7292
|
+
steering: this.#steeringMessages.length,
|
|
7293
|
+
followUp: this.#followUpMessages.length,
|
|
7294
|
+
nextTurn: this.#pendingNextTurnMessages.length,
|
|
7295
|
+
};
|
|
7296
|
+
}
|
|
6422
7297
|
|
|
6423
7298
|
/** Whether the agent has queued steering messages that a `user_interrupt`
|
|
6424
7299
|
* abort would resume into (steer-on-interrupt). Drives the Esc-on-steer UX:
|
|
@@ -6596,6 +7471,32 @@ export class AgentSession {
|
|
|
6596
7471
|
return this.#applyCompactionPostAppend(compactionEntryId, firstKeptEntryId, fromExtension);
|
|
6597
7472
|
}
|
|
6598
7473
|
|
|
7474
|
+
/** Read-only test seam for active mid-run EventStream drain barriers. */
|
|
7475
|
+
get activeMidRunBarrierCountForTests(): number {
|
|
7476
|
+
return this.#activeMidRunBarrierControllers.size;
|
|
7477
|
+
}
|
|
7478
|
+
|
|
7479
|
+
/** Read-only test seam for active mid-run maintenance invocations. */
|
|
7480
|
+
get activeMidRunMaintenanceCountForTests(): number {
|
|
7481
|
+
return this.#activeMidRunMaintenancePromises.size;
|
|
7482
|
+
}
|
|
7483
|
+
|
|
7484
|
+
/** Test seam: drive the cooperative mid-run maintenance checkpoint directly. */
|
|
7485
|
+
runMidRunMaintenanceForTests(
|
|
7486
|
+
context: AgentContext,
|
|
7487
|
+
lifecycle: MidRunMaintenanceLifecycle = {
|
|
7488
|
+
signal: new AbortController().signal,
|
|
7489
|
+
awaitEventDrain: async () => {},
|
|
7490
|
+
},
|
|
7491
|
+
): Promise<MidRunMaintenanceOutcome> {
|
|
7492
|
+
return this.#trackMidRunMaintenance(this.#runMidRunMaintenance(context, lifecycle));
|
|
7493
|
+
}
|
|
7494
|
+
|
|
7495
|
+
/** Test seam: estimate mid-run context tokens for a given context view. */
|
|
7496
|
+
estimateMidRunContextTokensForTests(messages: readonly AgentMessage[]): number {
|
|
7497
|
+
return this.#estimateMidRunContextTokens(messages);
|
|
7498
|
+
}
|
|
7499
|
+
|
|
6599
7500
|
#cloneTodoPhases(phases: TodoPhase[]): TodoPhase[] {
|
|
6600
7501
|
return phases.map(phase => ({
|
|
6601
7502
|
name: phase.name,
|
|
@@ -6633,6 +7534,7 @@ export class AgentSession {
|
|
|
6633
7534
|
* hand-off rather than a failure. */
|
|
6634
7535
|
silent?: boolean;
|
|
6635
7536
|
}): Promise<void> {
|
|
7537
|
+
this.#abortActiveMidRunBarriers();
|
|
6636
7538
|
if (options?.silent) {
|
|
6637
7539
|
this.#silentAbortPending = true;
|
|
6638
7540
|
} else {
|
|
@@ -6640,12 +7542,16 @@ export class AgentSession {
|
|
|
6640
7542
|
}
|
|
6641
7543
|
this.abortRetry();
|
|
6642
7544
|
this.#promptGeneration++;
|
|
7545
|
+
this.#promptPreflightAbortController.abort();
|
|
7546
|
+
this.#promptPreflightAbortController = new AbortController();
|
|
6643
7547
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
6644
7548
|
this.abortCompaction();
|
|
6645
7549
|
this.abortHandoff();
|
|
6646
7550
|
this.abortBash();
|
|
6647
7551
|
this.abortEval();
|
|
6648
7552
|
const postPromptDrain = this.#cancelPostPromptTasks();
|
|
7553
|
+
const managedLogicalRunId =
|
|
7554
|
+
this.#defaultFallbackChain().chain.entries.length > 1 ? this.agent.currentManagedLogicalRunId : undefined;
|
|
6649
7555
|
this.agent.abort();
|
|
6650
7556
|
const cleanup = Promise.all([postPromptDrain, this.agent.waitForIdle()]).then(
|
|
6651
7557
|
() => ({ type: "settled" as const }),
|
|
@@ -6673,10 +7579,16 @@ export class AgentSession {
|
|
|
6673
7579
|
}
|
|
6674
7580
|
}
|
|
6675
7581
|
await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
7582
|
+
if (managedLogicalRunId !== undefined) {
|
|
7583
|
+
// Agent-core owns terminalization for the logical managed run, which
|
|
7584
|
+
// remains stable across fallback attempt retries.
|
|
7585
|
+
this.agent.requestRunTerminal(managedLogicalRunId, { stopReason: "cancelled" });
|
|
7586
|
+
}
|
|
7587
|
+
// waitForIdle resolves before #promptWithMessage's finally and asynchronous
|
|
7588
|
+
// event handlers necessarily unwind. Keep their counters intact: #endInFlight
|
|
7589
|
+
// and the event barrier publish deferred readiness only when they actually settle.
|
|
7590
|
+
this.#flushPendingBackgroundExchanges();
|
|
7591
|
+
this.#flushPendingAgentEnd();
|
|
6680
7592
|
// Safety net: clear the silent-abort flag if it was never consumed (the
|
|
6681
7593
|
// abort produced no aborted assistant message_end to stamp). Prevents the
|
|
6682
7594
|
// marker from leaking onto a later, unrelated abort.
|
|
@@ -6711,6 +7623,7 @@ export class AgentSession {
|
|
|
6711
7623
|
*/
|
|
6712
7624
|
async newSession(options?: NewSessionOptions): Promise<boolean> {
|
|
6713
7625
|
const previousSessionFile = this.sessionFile;
|
|
7626
|
+
const previousWorkflowGateSessionId = this.sessionId;
|
|
6714
7627
|
const nextDiscoverySessionToolNames = this.#mcpDiscoveryEnabled
|
|
6715
7628
|
? [
|
|
6716
7629
|
...this.#getActiveNonMCPToolNames(),
|
|
@@ -6734,6 +7647,7 @@ export class AgentSession {
|
|
|
6734
7647
|
await this.abort();
|
|
6735
7648
|
this.#cancelOwnAsyncJobs();
|
|
6736
7649
|
this.#closeAllProviderSessions("new session");
|
|
7650
|
+
this.#rebindProviderSessionState(new Map());
|
|
6737
7651
|
this.agent.reset();
|
|
6738
7652
|
if (options?.drop && previousSessionFile) {
|
|
6739
7653
|
try {
|
|
@@ -6747,6 +7661,7 @@ export class AgentSession {
|
|
|
6747
7661
|
await this.sessionManager.newSession(options);
|
|
6748
7662
|
this.setTodoPhases([]);
|
|
6749
7663
|
this.#syncAgentSessionId();
|
|
7664
|
+
this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
|
|
6750
7665
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6751
7666
|
this.#resetHindsightConversationTrackingIfHindsight();
|
|
6752
7667
|
this.#steeringMessages = [];
|
|
@@ -6754,7 +7669,10 @@ export class AgentSession {
|
|
|
6754
7669
|
this.#pendingNextTurnMessages = [];
|
|
6755
7670
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
6756
7671
|
|
|
6757
|
-
this.
|
|
7672
|
+
const inheritedThinkingLevel = resolveThinkingLevelForModel(this.model, this.#getInheritedThinkingLevel());
|
|
7673
|
+
this.#thinkingLevel = inheritedThinkingLevel;
|
|
7674
|
+
this.agent.setThinkingLevel(toReasoningEffort(inheritedThinkingLevel));
|
|
7675
|
+
this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
|
|
6758
7676
|
if (this.model) {
|
|
6759
7677
|
this.sessionManager.appendModelChange(`${this.model.provider}/${this.model.id}`);
|
|
6760
7678
|
}
|
|
@@ -6839,6 +7757,7 @@ export class AgentSession {
|
|
|
6839
7757
|
*/
|
|
6840
7758
|
async fork(): Promise<boolean> {
|
|
6841
7759
|
const previousSessionFile = this.sessionFile;
|
|
7760
|
+
const previousWorkflowGateSessionId = this.sessionId;
|
|
6842
7761
|
|
|
6843
7762
|
// Emit session_before_switch event with reason "fork" (can be cancelled)
|
|
6844
7763
|
if (this.#extensionRunner?.hasHandlers("session_before_switch")) {
|
|
@@ -6882,6 +7801,7 @@ export class AgentSession {
|
|
|
6882
7801
|
|
|
6883
7802
|
// Update agent session ID
|
|
6884
7803
|
this.#syncAgentSessionId();
|
|
7804
|
+
this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
|
|
6885
7805
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6886
7806
|
|
|
6887
7807
|
this.#resetIrcRosterDeliveryState();
|
|
@@ -6910,7 +7830,7 @@ export class AgentSession {
|
|
|
6910
7830
|
async setModel(
|
|
6911
7831
|
model: Model,
|
|
6912
7832
|
role: string = "default",
|
|
6913
|
-
options?: { selector?: string; thinkingLevel?: ThinkingLevel },
|
|
7833
|
+
options?: { selector?: string; thinkingLevel?: ThinkingLevel; cause?: ModelChangeCause },
|
|
6914
7834
|
): Promise<void> {
|
|
6915
7835
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
6916
7836
|
const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
|
|
@@ -6918,15 +7838,39 @@ export class AgentSession {
|
|
|
6918
7838
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
6919
7839
|
}
|
|
6920
7840
|
|
|
6921
|
-
this.#
|
|
6922
|
-
this.#setModelWithProviderSessionReset(model);
|
|
7841
|
+
this.#setModelAuthoritatively(model, options?.cause ?? "user-selection");
|
|
6923
7842
|
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, role);
|
|
6924
7843
|
this.settings.setModelRole(
|
|
6925
7844
|
role,
|
|
6926
7845
|
this.#formatRoleModelValue(role, model, options?.selector, options?.thinkingLevel),
|
|
6927
7846
|
);
|
|
7847
|
+
if (role === "default") {
|
|
7848
|
+
this.#defaultFallbackController = undefined;
|
|
7849
|
+
this.#defaultFallbackExhaustedLastTurn = false;
|
|
7850
|
+
}
|
|
6928
7851
|
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
6929
7852
|
|
|
7853
|
+
// Persist configured intent rather than a transient controller index. A pick
|
|
7854
|
+
// inside an existing chain keeps its deterministic suffix; a different
|
|
7855
|
+
// thinking choice for the same concrete model becomes the new head followed
|
|
7856
|
+
// by that concrete entry's tail. Picks outside the chain are one-entry intent.
|
|
7857
|
+
const configuredChain = this.getConfiguredModelChain(role);
|
|
7858
|
+
if (configuredChain) {
|
|
7859
|
+
const selectedSelector = this.#canonicalSelector(model, options?.selector, options?.thinkingLevel);
|
|
7860
|
+
const exactIndex = configuredChain.indexOf(selectedSelector);
|
|
7861
|
+
const concreteIndex = configuredChain.findIndex(entry => {
|
|
7862
|
+
const parsed = parseModelString(entry);
|
|
7863
|
+
return parsed?.provider === model.provider && parsed.id === model.id;
|
|
7864
|
+
});
|
|
7865
|
+
const entries =
|
|
7866
|
+
exactIndex !== -1
|
|
7867
|
+
? configuredChain.slice(exactIndex)
|
|
7868
|
+
: concreteIndex !== -1
|
|
7869
|
+
? [selectedSelector, ...configuredChain.slice(concreteIndex + 1)]
|
|
7870
|
+
: [selectedSelector];
|
|
7871
|
+
this.setConfiguredModelChain(role, entries, "model_selection");
|
|
7872
|
+
}
|
|
7873
|
+
|
|
6930
7874
|
// Apply the explicitly selected thinking level when the selector supplies one;
|
|
6931
7875
|
// otherwise prefer the model's configured defaultLevel, then preserve the current level.
|
|
6932
7876
|
this.setThinkingLevel(options?.thinkingLevel ?? model.thinking?.defaultLevel ?? this.thinkingLevel);
|
|
@@ -6941,21 +7885,67 @@ export class AgentSession {
|
|
|
6941
7885
|
return this.#activeModelProfile;
|
|
6942
7886
|
}
|
|
6943
7887
|
|
|
6944
|
-
/**
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
* Model-profile activation snapshots this before mutating the session so a
|
|
6948
|
-
* failed-activation rollback can restore the pre-activation resume default
|
|
6949
|
-
* instead of promoting a transient runtime model to the resume default.
|
|
6950
|
-
*/
|
|
6951
|
-
getSessionDefaultModelSelector(): string | undefined {
|
|
6952
|
-
return this.sessionManager.buildSessionContext().models.default;
|
|
7888
|
+
/** Return the persisted configured fallback selectors for a model role. */
|
|
7889
|
+
getConfiguredModelChain(role: string): readonly string[] | undefined {
|
|
7890
|
+
return this.sessionManager.buildSessionContext().configuredModelChains[role]?.entries;
|
|
6953
7891
|
}
|
|
6954
7892
|
|
|
6955
|
-
/**
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
7893
|
+
/** Persist the configured fallback selectors for a model role. */
|
|
7894
|
+
setConfiguredModelChain(
|
|
7895
|
+
role: string,
|
|
7896
|
+
entries: readonly string[],
|
|
7897
|
+
origin: string,
|
|
7898
|
+
identity?: string,
|
|
7899
|
+
explicitHead = true,
|
|
7900
|
+
): void {
|
|
7901
|
+
this.sessionManager.appendConfiguredModelChain({
|
|
7902
|
+
role,
|
|
7903
|
+
entries: [...entries],
|
|
7904
|
+
origin,
|
|
7905
|
+
identity,
|
|
7906
|
+
explicitHead,
|
|
7907
|
+
cleared: entries.length === 0,
|
|
7908
|
+
});
|
|
7909
|
+
}
|
|
7910
|
+
|
|
7911
|
+
/**
|
|
7912
|
+
* Replace only the in-memory default fallback controller. Used when startup
|
|
7913
|
+
* falls through an unavailable persisted chain to the global default, which
|
|
7914
|
+
* must not mutate the persisted configured intent.
|
|
7915
|
+
*/
|
|
7916
|
+
setDefaultFallbackRuntimeModel(selector: string): void {
|
|
7917
|
+
this.#defaultFallbackController = new FallbackChainController(
|
|
7918
|
+
{ role: "default", entries: [selector], origin: "runtime", explicitHead: true },
|
|
7919
|
+
this.settings.get("fallback.maxAttempts"),
|
|
7920
|
+
);
|
|
7921
|
+
this.#defaultFallbackExhaustedLastTurn = false;
|
|
7922
|
+
}
|
|
7923
|
+
|
|
7924
|
+
/**
|
|
7925
|
+
* Seed default fallback state after guarded auth-aware model resolution skips chain entries.
|
|
7926
|
+
* The configured chain's role, origin, and identity are retained by the controller.
|
|
7927
|
+
*/
|
|
7928
|
+
seedDefaultFallbackResolution(activeIndex: number, skips: Array<{ selector: string; reason: string }>): void {
|
|
7929
|
+
const controller = this.#defaultFallbackChain();
|
|
7930
|
+
controller.seedResolution(activeIndex, skips);
|
|
7931
|
+
this.#emitResolutionFallbackSwitch(controller);
|
|
7932
|
+
}
|
|
7933
|
+
|
|
7934
|
+
/**
|
|
7935
|
+
* The model selector ("provider/id") that resume restores as the session
|
|
7936
|
+
* default — the latest session-log `model_change` with role="default".
|
|
7937
|
+
* Model-profile activation snapshots this before mutating the session so a
|
|
7938
|
+
* failed-activation rollback can restore the pre-activation resume default
|
|
7939
|
+
* instead of promoting a transient runtime model to the resume default.
|
|
7940
|
+
*/
|
|
7941
|
+
getSessionDefaultModelSelector(): string | undefined {
|
|
7942
|
+
return this.sessionManager.buildSessionContext().models.default;
|
|
7943
|
+
}
|
|
7944
|
+
|
|
7945
|
+
/**
|
|
7946
|
+
* Re-assert the session resume default ("provider/id") in the session log
|
|
7947
|
+
* WITHOUT touching the live runtime model. Appends a `model_change` with
|
|
7948
|
+
* role="default"; never writes to global settings (apply-for-this-session
|
|
6959
7949
|
* semantics). Used by model-profile activation rollback to neutralize the
|
|
6960
7950
|
* profile main model the failed activation already recorded as the default.
|
|
6961
7951
|
*/
|
|
@@ -6979,37 +7969,179 @@ export class AgentSession {
|
|
|
6979
7969
|
async setModelTemporary(
|
|
6980
7970
|
model: Model,
|
|
6981
7971
|
thinkingLevel?: ThinkingLevel,
|
|
6982
|
-
options?: {
|
|
6983
|
-
|
|
7972
|
+
options?: {
|
|
7973
|
+
persistAsSessionDefault?: boolean;
|
|
7974
|
+
cause?: ModelChangeCause;
|
|
7975
|
+
reason?: TemporaryModelReason;
|
|
7976
|
+
providerSessionScope?: TemporaryProviderSessionScope;
|
|
7977
|
+
signal?: AbortSignal;
|
|
7978
|
+
},
|
|
7979
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: Existing session adapters return Promise<void>; a scope is optional.
|
|
7980
|
+
): Promise<TemporaryProviderSessionScope | void> {
|
|
7981
|
+
if (options?.signal?.aborted) return;
|
|
7982
|
+
const suppliedScope = options?.providerSessionScope;
|
|
7983
|
+
if (suppliedScope && this.#temporaryProviderSessionScopes.at(-1)?.token !== suppliedScope) return;
|
|
6984
7984
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
6985
|
-
const
|
|
7985
|
+
const expectedSessionId = this.sessionId;
|
|
7986
|
+
const apiKey = await this.#modelRegistry.getApiKey(model, expectedSessionId);
|
|
7987
|
+
if (options?.signal?.aborted) return;
|
|
7988
|
+
if (this.sessionId !== expectedSessionId) {
|
|
7989
|
+
throw new Error("Session changed while selecting model");
|
|
7990
|
+
}
|
|
6986
7991
|
if (!apiKey) {
|
|
6987
7992
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
6988
7993
|
}
|
|
7994
|
+
if (suppliedScope && this.#temporaryProviderSessionScopes.at(-1)?.token !== suppliedScope) return;
|
|
7995
|
+
|
|
7996
|
+
const isTemporaryOperation = options?.cause === undefined || options.cause === "temporary-operation";
|
|
7997
|
+
const autoCreateScope = isTemporaryOperation && !suppliedScope;
|
|
7998
|
+
const currentAutoScope = this.#currentAutoTemporaryProviderSessionScope();
|
|
7999
|
+
const replaceAutoScope =
|
|
8000
|
+
autoCreateScope &&
|
|
8001
|
+
currentAutoScope !== undefined &&
|
|
8002
|
+
this.#temporaryProviderSessionScopes.at(-1) === currentAutoScope;
|
|
8003
|
+
if (replaceAutoScope && currentAutoScope) {
|
|
8004
|
+
this.#restoreTopTemporaryProviderSessionScope();
|
|
8005
|
+
}
|
|
8006
|
+
const scope = isTemporaryOperation
|
|
8007
|
+
? (suppliedScope ??
|
|
8008
|
+
(replaceAutoScope && this.model && modelsAreEqual(this.model, model)
|
|
8009
|
+
? undefined
|
|
8010
|
+
: this.#beginTemporaryProviderSessionScope(options?.reason ?? "other", true)))
|
|
8011
|
+
: undefined;
|
|
8012
|
+
const ownsScope = scope !== undefined && !suppliedScope;
|
|
8013
|
+
try {
|
|
8014
|
+
if (isTemporaryOperation) {
|
|
8015
|
+
this.agent.setModel(model);
|
|
8016
|
+
this.#syncAppendOnlyContext(model);
|
|
8017
|
+
} else {
|
|
8018
|
+
this.#setModelAuthoritatively(model, options?.cause ?? "temporary-operation");
|
|
8019
|
+
}
|
|
8020
|
+
this.sessionManager.appendModelChange(
|
|
8021
|
+
`${model.provider}/${model.id}`,
|
|
8022
|
+
options?.persistAsSessionDefault ? "default" : "temporary",
|
|
8023
|
+
);
|
|
8024
|
+
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
8025
|
+
|
|
8026
|
+
// Apply explicit thinking level if given; otherwise prefer the model's
|
|
8027
|
+
// configured defaultLevel; otherwise re-clamp the current level.
|
|
8028
|
+
this.setThinkingLevel(thinkingLevel ?? model.thinking?.defaultLevel ?? this.thinkingLevel);
|
|
8029
|
+
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
8030
|
+
} catch (error) {
|
|
8031
|
+
if (ownsScope) this.restoreTemporaryProviderSessionScope(scope);
|
|
8032
|
+
throw error;
|
|
8033
|
+
}
|
|
8034
|
+
return scope;
|
|
8035
|
+
}
|
|
6989
8036
|
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
8037
|
+
async #restoreDefaultModelSelectionCommit(
|
|
8038
|
+
commit: GlobalDefaultModelRoleCommit,
|
|
8039
|
+
): Promise<{ readonly stage: DefaultModelSelectionRollbackStage; readonly message: string } | undefined> {
|
|
8040
|
+
try {
|
|
8041
|
+
if (await this.settings.restoreGlobalDefaultModelRoleIfCurrent(commit)) return undefined;
|
|
8042
|
+
return {
|
|
8043
|
+
stage: "durable",
|
|
8044
|
+
message: "A newer default selection prevented durable recovery.",
|
|
8045
|
+
};
|
|
8046
|
+
} catch {
|
|
8047
|
+
logger.warn("Failed to restore durable default model selection after session promotion failure", {
|
|
8048
|
+
code: "default_model_selection_recovery_failed",
|
|
8049
|
+
rollbackStage: "durable",
|
|
8050
|
+
});
|
|
8051
|
+
return {
|
|
8052
|
+
stage: "durable",
|
|
8053
|
+
message: "Durable default selection recovery could not be completed.",
|
|
8054
|
+
};
|
|
8055
|
+
}
|
|
8056
|
+
}
|
|
8057
|
+
|
|
8058
|
+
async #discardDefaultModelSelectionStage(
|
|
8059
|
+
stage: DefaultModelSelectionStage,
|
|
8060
|
+
): Promise<{ readonly stage: DefaultModelSelectionRollbackStage; readonly message: string } | undefined> {
|
|
8061
|
+
try {
|
|
8062
|
+
await this.sessionManager.discardDefaultModelSelectionStage(stage);
|
|
8063
|
+
return undefined;
|
|
8064
|
+
} catch {
|
|
8065
|
+
logger.warn("Failed to discard staged default model selection after session promotion failure", {
|
|
8066
|
+
code: "default_model_selection_recovery_failed",
|
|
8067
|
+
rollbackStage: "session",
|
|
8068
|
+
});
|
|
8069
|
+
return {
|
|
8070
|
+
stage: "session",
|
|
8071
|
+
message: "Session replacement recovery could not be completed.",
|
|
8072
|
+
};
|
|
8073
|
+
}
|
|
8074
|
+
}
|
|
8075
|
+
|
|
8076
|
+
async #throwDefaultModelSelectionRecovery(
|
|
8077
|
+
error: Error,
|
|
8078
|
+
stage: DefaultModelSelectionStage,
|
|
8079
|
+
commit: GlobalDefaultModelRoleCommit,
|
|
8080
|
+
): Promise<never> {
|
|
8081
|
+
const sessionFailure = await this.#discardDefaultModelSelectionStage(stage);
|
|
8082
|
+
const durableFailure = await this.#restoreDefaultModelSelectionCommit(commit);
|
|
8083
|
+
const failures = [sessionFailure, durableFailure].filter(
|
|
8084
|
+
(failure): failure is { readonly stage: DefaultModelSelectionRollbackStage; readonly message: string } =>
|
|
8085
|
+
failure !== undefined,
|
|
6995
8086
|
);
|
|
6996
|
-
|
|
8087
|
+
throw new DefaultModelSelectionRecoveryError(error.message, {
|
|
8088
|
+
message: error.message,
|
|
8089
|
+
rollback: {
|
|
8090
|
+
disposition: failures.length === 0 ? "restored" : "partial",
|
|
8091
|
+
failures,
|
|
8092
|
+
},
|
|
8093
|
+
});
|
|
8094
|
+
}
|
|
6997
8095
|
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
this
|
|
7001
|
-
|
|
8096
|
+
#publishDefaultModelSelection(model: Model, thinkingLevel: ThinkingLevel, systemPrompt: string[] | undefined): void {
|
|
8097
|
+
this.#clearActiveRetryFallback();
|
|
8098
|
+
this.#setModelWithProviderSessionReset(model);
|
|
8099
|
+
const thinkingLevelChanged = this.#thinkingLevel !== thinkingLevel;
|
|
8100
|
+
this.#thinkingLevel = thinkingLevel;
|
|
8101
|
+
this.agent.setThinkingLevel(toReasoningEffort(thinkingLevel));
|
|
8102
|
+
if (thinkingLevelChanged) {
|
|
8103
|
+
const event: AgentSessionEvent = { type: "thinking_level_changed", thinkingLevel };
|
|
8104
|
+
for (const listener of this.#eventListenerSnapshot) {
|
|
8105
|
+
try {
|
|
8106
|
+
listener(event);
|
|
8107
|
+
} catch {
|
|
8108
|
+
logger.warn("Default model selection event listener failed", {
|
|
8109
|
+
code: "default_model_selection_listener_failed",
|
|
8110
|
+
disposition: "continue",
|
|
8111
|
+
});
|
|
8112
|
+
}
|
|
8113
|
+
}
|
|
8114
|
+
}
|
|
8115
|
+
this.#applyPreparedDefaultModelSelectionPrompt(systemPrompt);
|
|
8116
|
+
try {
|
|
8117
|
+
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
8118
|
+
} catch {
|
|
8119
|
+
logger.warn("Failed to record model usage after default model selection", {
|
|
8120
|
+
code: "default_model_selection_model_usage_record_failed",
|
|
8121
|
+
disposition: "continue",
|
|
8122
|
+
});
|
|
8123
|
+
}
|
|
7002
8124
|
}
|
|
7003
8125
|
|
|
8126
|
+
/** Set a durable per-session model from a control surface without exposing credential errors. */
|
|
8127
|
+
async setModelTemporaryForControl(model: Model, expectedSessionId: string = this.sessionId): Promise<boolean> {
|
|
8128
|
+
if (expectedSessionId !== this.sessionId) return false;
|
|
8129
|
+
try {
|
|
8130
|
+
await this.setModelTemporary(model, undefined, {
|
|
8131
|
+
persistAsSessionDefault: true,
|
|
8132
|
+
cause: "user-selection",
|
|
8133
|
+
});
|
|
8134
|
+
return expectedSessionId === this.sessionId;
|
|
8135
|
+
} catch {
|
|
8136
|
+
logger.warn("session: model control failed");
|
|
8137
|
+
return false;
|
|
8138
|
+
}
|
|
8139
|
+
}
|
|
7004
8140
|
async setDefaultModelSelection(
|
|
7005
8141
|
model: Model,
|
|
7006
8142
|
thinkingLevel: ThinkingLevel | undefined,
|
|
7007
8143
|
): Promise<DefaultModelSelectionResult> {
|
|
7008
|
-
|
|
7009
|
-
const transaction = Promise.withResolvers<void>();
|
|
7010
|
-
this.#defaultModelSelectionTail = transaction.promise;
|
|
7011
|
-
try {
|
|
7012
|
-
await predecessor;
|
|
8144
|
+
return this.#withSessionAdmission("selection", async () => {
|
|
7013
8145
|
if (thinkingLevel === ThinkingLevel.Inherit) {
|
|
7014
8146
|
throw new Error("Default model selection cannot inherit a thinking level");
|
|
7015
8147
|
}
|
|
@@ -7023,72 +8155,64 @@ export class AgentSession {
|
|
|
7023
8155
|
resolveThinkingLevelForModel(model, model.thinking?.defaultLevel ?? this.thinkingLevel) ??
|
|
7024
8156
|
ThinkingLevel.Off;
|
|
7025
8157
|
await this.waitForIdle();
|
|
7026
|
-
const previousDefaultModelRole = this.settings.getGlobal("modelRoles")?.default;
|
|
7027
8158
|
await this.sessionManager.flush();
|
|
7028
|
-
|
|
7029
|
-
const
|
|
7030
|
-
const
|
|
7031
|
-
const
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
8159
|
+
await this.#waitForAdmittedBaseSystemPromptRebuilds();
|
|
8160
|
+
const expectedMutationRevision = this.#defaultModelSelectionMutationRevision;
|
|
8161
|
+
const preparedSystemPrompt = await this.#prepareDefaultModelSelectionPrompt(model);
|
|
8162
|
+
const stage = await this.sessionManager.stageDefaultModelSelection(
|
|
8163
|
+
`${model.provider}/${model.id}`,
|
|
8164
|
+
effectiveLevel,
|
|
8165
|
+
{ appendThinkingLevel: true },
|
|
7035
8166
|
);
|
|
8167
|
+
let durableCommit: GlobalDefaultModelRoleCommit;
|
|
7036
8168
|
try {
|
|
7037
|
-
await this.
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "default");
|
|
8169
|
+
durableCommit = await this.settings.setGlobalModelRoleAndFlush(
|
|
8170
|
+
"default",
|
|
8171
|
+
formatModelSelectorValue(`${model.provider}/${model.id}`, effectiveLevel),
|
|
8172
|
+
);
|
|
7042
8173
|
} catch (error) {
|
|
7043
|
-
await this.
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
if (hasCompatibleModelTransport) {
|
|
7059
|
-
this.agent.setModel(previousModel);
|
|
7060
|
-
this.#syncAppendOnlyContext(previousModel);
|
|
7061
|
-
} else {
|
|
7062
|
-
this.#setModelWithProviderSessionReset(previousModel);
|
|
7063
|
-
}
|
|
7064
|
-
this.#thinkingLevel = previousThinkingLevel;
|
|
7065
|
-
this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
|
|
7066
|
-
await this.#syncEditToolModeAfterModelChange(failedEditMode);
|
|
7067
|
-
restoredLiveState = true;
|
|
7068
|
-
} catch (rollbackError) {
|
|
7069
|
-
logger.warn("Failed to restore live default model selection after live apply failure", {
|
|
7070
|
-
error: String(rollbackError),
|
|
7071
|
-
});
|
|
8174
|
+
await this.sessionManager.discardDefaultModelSelectionStage(stage);
|
|
8175
|
+
throw error;
|
|
8176
|
+
}
|
|
8177
|
+
if (this.#defaultModelSelectionMutationRevision !== expectedMutationRevision) {
|
|
8178
|
+
await this.#throwDefaultModelSelectionRecovery(
|
|
8179
|
+
new Error("Default model selection was superseded before session promotion"),
|
|
8180
|
+
stage,
|
|
8181
|
+
durableCommit,
|
|
8182
|
+
);
|
|
8183
|
+
}
|
|
8184
|
+
const promotion = this.sessionManager.promoteDefaultModelSelection(stage);
|
|
8185
|
+
switch (promotion.kind) {
|
|
8186
|
+
case "promoted": {
|
|
8187
|
+
this.#publishDefaultModelSelection(model, effectiveLevel, preparedSystemPrompt);
|
|
8188
|
+
break;
|
|
7072
8189
|
}
|
|
7073
|
-
|
|
7074
|
-
this.#
|
|
8190
|
+
case "not_promoted": {
|
|
8191
|
+
return this.#throwDefaultModelSelectionRecovery(
|
|
8192
|
+
promotion.error ?? new Error("Default model selection was superseded before session promotion"),
|
|
8193
|
+
stage,
|
|
8194
|
+
durableCommit,
|
|
8195
|
+
);
|
|
7075
8196
|
}
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
8197
|
+
case "unknown": {
|
|
8198
|
+
const message = "Session replacement outcome could not be determined.";
|
|
8199
|
+
throw new DefaultModelSelectionRecoveryError(message, {
|
|
8200
|
+
message,
|
|
8201
|
+
rollback: {
|
|
8202
|
+
disposition: "unknown",
|
|
8203
|
+
failures: [
|
|
8204
|
+
{
|
|
8205
|
+
stage: "session",
|
|
8206
|
+
message: "Session replacement outcome could not be determined.",
|
|
8207
|
+
},
|
|
8208
|
+
],
|
|
8209
|
+
},
|
|
7082
8210
|
});
|
|
7083
8211
|
}
|
|
7084
|
-
throw error;
|
|
7085
8212
|
}
|
|
7086
8213
|
return { provider: model.provider, modelId: model.id, thinkingLevel: effectiveLevel };
|
|
7087
|
-
}
|
|
7088
|
-
transaction.resolve();
|
|
7089
|
-
}
|
|
8214
|
+
});
|
|
7090
8215
|
}
|
|
7091
|
-
|
|
7092
8216
|
/**
|
|
7093
8217
|
* Cycle to next/previous model.
|
|
7094
8218
|
* Uses scoped models (from --models flag) if available, otherwise all available models.
|
|
@@ -7160,9 +8284,12 @@ export class AgentSession {
|
|
|
7160
8284
|
const next = roleModels[nextIndex];
|
|
7161
8285
|
|
|
7162
8286
|
if (options?.temporary) {
|
|
7163
|
-
await this.setModelTemporary(next.model, next.explicitThinkingLevel ? next.thinkingLevel : undefined
|
|
8287
|
+
await this.setModelTemporary(next.model, next.explicitThinkingLevel ? next.thinkingLevel : undefined, {
|
|
8288
|
+
cause: "temporary-operation",
|
|
8289
|
+
reason: "temporary-cycle",
|
|
8290
|
+
});
|
|
7164
8291
|
} else {
|
|
7165
|
-
await this.setModel(next.model, next.role);
|
|
8292
|
+
await this.setModel(next.model, next.role, { cause: "user-selection" });
|
|
7166
8293
|
if (next.explicitThinkingLevel && next.thinkingLevel !== undefined) {
|
|
7167
8294
|
this.setThinkingLevel(next.thinkingLevel);
|
|
7168
8295
|
}
|
|
@@ -7194,7 +8321,6 @@ export class AgentSession {
|
|
|
7194
8321
|
}
|
|
7195
8322
|
|
|
7196
8323
|
async #cycleScopedModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
|
|
7197
|
-
const previousEditMode = this.#resolveActiveEditMode();
|
|
7198
8324
|
const scopedModels = await this.#getScopedModelsWithApiKey();
|
|
7199
8325
|
if (scopedModels.length <= 1) return undefined;
|
|
7200
8326
|
|
|
@@ -7206,22 +8332,15 @@ export class AgentSession {
|
|
|
7206
8332
|
const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
|
|
7207
8333
|
const next = scopedModels[nextIndex];
|
|
7208
8334
|
|
|
7209
|
-
|
|
7210
|
-
this.#clearActiveRetryFallback();
|
|
7211
|
-
this.#setModelWithProviderSessionReset(next.model);
|
|
7212
|
-
this.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
|
|
7213
|
-
this.settings.setModelRole("default", this.#formatRoleModelValue("default", next.model));
|
|
7214
|
-
this.settings.getStorage()?.recordModelUsage(`${next.model.provider}/${next.model.id}`);
|
|
8335
|
+
await this.setModel(next.model, "default", { cause: "user-selection" });
|
|
7215
8336
|
|
|
7216
8337
|
// Apply the scoped model's configured thinking level
|
|
7217
8338
|
this.setThinkingLevel(next.thinkingLevel);
|
|
7218
|
-
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
7219
8339
|
|
|
7220
8340
|
return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
|
|
7221
8341
|
}
|
|
7222
8342
|
|
|
7223
8343
|
async #cycleAvailableModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
|
|
7224
|
-
const previousEditMode = this.#resolveActiveEditMode();
|
|
7225
8344
|
const availableModels = this.#modelRegistry.getAvailable();
|
|
7226
8345
|
if (availableModels.length <= 1) return undefined;
|
|
7227
8346
|
|
|
@@ -7238,14 +8357,9 @@ export class AgentSession {
|
|
|
7238
8357
|
throw new Error(`No API key for ${nextModel.provider}/${nextModel.id}`);
|
|
7239
8358
|
}
|
|
7240
8359
|
|
|
7241
|
-
this
|
|
7242
|
-
this.#setModelWithProviderSessionReset(nextModel);
|
|
7243
|
-
this.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
|
|
7244
|
-
this.settings.setModelRole("default", this.#formatRoleModelValue("default", nextModel));
|
|
7245
|
-
this.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
|
|
8360
|
+
await this.setModel(nextModel, "default", { cause: "user-selection" });
|
|
7246
8361
|
// Re-apply the current thinking level for the newly selected model
|
|
7247
8362
|
this.setThinkingLevel(this.thinkingLevel);
|
|
7248
|
-
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
7249
8363
|
|
|
7250
8364
|
return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
|
|
7251
8365
|
}
|
|
@@ -7261,6 +8375,11 @@ export class AgentSession {
|
|
|
7261
8375
|
// Thinking Level Management
|
|
7262
8376
|
// =========================================================================
|
|
7263
8377
|
|
|
8378
|
+
#getInheritedThinkingLevel(): ThinkingLevel | undefined {
|
|
8379
|
+
if (this.settings.has("defaultThinkingLevel")) return this.settings.get("defaultThinkingLevel");
|
|
8380
|
+
return this.model?.thinking?.defaultLevel ?? this.settings.get("defaultThinkingLevel");
|
|
8381
|
+
}
|
|
8382
|
+
|
|
7264
8383
|
/**
|
|
7265
8384
|
* Set thinking level.
|
|
7266
8385
|
* Saves the effective metadata-clamped level to the session, and to settings when requested.
|
|
@@ -7271,9 +8390,11 @@ export class AgentSession {
|
|
|
7271
8390
|
|
|
7272
8391
|
this.#thinkingLevel = effectiveLevel;
|
|
7273
8392
|
this.agent.setThinkingLevel(toReasoningEffort(effectiveLevel));
|
|
8393
|
+
if (isChanging) this.#defaultModelSelectionMutationRevision++;
|
|
7274
8394
|
|
|
7275
|
-
if (persist
|
|
7276
|
-
|
|
8395
|
+
if (persist) {
|
|
8396
|
+
const persistedLevel = level === ThinkingLevel.Inherit ? getDefault("defaultThinkingLevel") : effectiveLevel;
|
|
8397
|
+
if (persistedLevel !== undefined) this.settings.set("defaultThinkingLevel", persistedLevel);
|
|
7277
8398
|
}
|
|
7278
8399
|
|
|
7279
8400
|
if (isChanging) {
|
|
@@ -7282,6 +8403,83 @@ export class AgentSession {
|
|
|
7282
8403
|
}
|
|
7283
8404
|
}
|
|
7284
8405
|
|
|
8406
|
+
/**
|
|
8407
|
+
* Set thinking level from a control surface. Global changes are durable or rolled back.
|
|
8408
|
+
*/
|
|
8409
|
+
async setThinkingLevelForControl(level: ThinkingLevel, persist: boolean): Promise<void> {
|
|
8410
|
+
const previousThinkingLevel = this.thinkingLevel;
|
|
8411
|
+
const previousScope = this.getThinkingScopeForControl();
|
|
8412
|
+
if (!persist) {
|
|
8413
|
+
this.setThinkingLevel(level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : level);
|
|
8414
|
+
if (level === ThinkingLevel.Inherit || this.thinkingLevel === previousThinkingLevel) {
|
|
8415
|
+
this.sessionManager.appendThinkingLevelChange(level);
|
|
8416
|
+
}
|
|
8417
|
+
return;
|
|
8418
|
+
}
|
|
8419
|
+
|
|
8420
|
+
const previousDefaultThinkingLevel = this.settings.getGlobal("defaultThinkingLevel");
|
|
8421
|
+
if (level === ThinkingLevel.Inherit) {
|
|
8422
|
+
const defaultLevel = getDefault("defaultThinkingLevel");
|
|
8423
|
+
this.settings.set("defaultThinkingLevel", defaultLevel);
|
|
8424
|
+
this.setThinkingLevel(this.#getInheritedThinkingLevel());
|
|
8425
|
+
} else {
|
|
8426
|
+
this.setThinkingLevel(level, true);
|
|
8427
|
+
}
|
|
8428
|
+
try {
|
|
8429
|
+
await this.settings.flushOrThrow();
|
|
8430
|
+
this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
|
|
8431
|
+
} catch {
|
|
8432
|
+
this.settings.set("defaultThinkingLevel", previousDefaultThinkingLevel ?? getDefault("defaultThinkingLevel"));
|
|
8433
|
+
this.setThinkingLevel(previousThinkingLevel);
|
|
8434
|
+
this.sessionManager.appendThinkingLevelChange(
|
|
8435
|
+
previousScope === "global config" ? ThinkingLevel.Inherit : previousThinkingLevel,
|
|
8436
|
+
);
|
|
8437
|
+
await this.settings.flushOrThrow().catch(() => {});
|
|
8438
|
+
throw new Error("Unable to persist reasoning settings.");
|
|
8439
|
+
}
|
|
8440
|
+
}
|
|
8441
|
+
|
|
8442
|
+
getThinkingScopeForControl(): "session" | "global config" {
|
|
8443
|
+
const latest = this.sessionManager
|
|
8444
|
+
.getBranch()
|
|
8445
|
+
.toReversed()
|
|
8446
|
+
.find(entry => entry.type === "thinking_level_change");
|
|
8447
|
+
return latest && latest.thinkingLevel !== ThinkingLevel.Inherit ? "session" : "global config";
|
|
8448
|
+
}
|
|
8449
|
+
|
|
8450
|
+
getThinkingVisibility(): "visible" | "hidden" {
|
|
8451
|
+
return this.agent.hideThinkingSummary ? "hidden" : "visible";
|
|
8452
|
+
}
|
|
8453
|
+
|
|
8454
|
+
setThinkingVisibility(visibility: "visible" | "hidden", persist: boolean = false): void {
|
|
8455
|
+
this.agent.hideThinkingSummary = visibility === "hidden";
|
|
8456
|
+
if (persist) {
|
|
8457
|
+
this.settings.set("hideThinkingBlock", visibility === "hidden");
|
|
8458
|
+
}
|
|
8459
|
+
}
|
|
8460
|
+
|
|
8461
|
+
/**
|
|
8462
|
+
* Set thinking visibility from a control surface. Global changes are durable or rolled back.
|
|
8463
|
+
*/
|
|
8464
|
+
async setThinkingVisibilityForControl(visibility: "visible" | "hidden", persist: boolean): Promise<void> {
|
|
8465
|
+
if (!persist) {
|
|
8466
|
+
this.setThinkingVisibility(visibility);
|
|
8467
|
+
return;
|
|
8468
|
+
}
|
|
8469
|
+
|
|
8470
|
+
const previousVisibility = this.getThinkingVisibility();
|
|
8471
|
+
const previousHideThinkingBlock = this.settings.getGlobal("hideThinkingBlock");
|
|
8472
|
+
this.setThinkingVisibility(visibility, true);
|
|
8473
|
+
try {
|
|
8474
|
+
await this.settings.flushOrThrow();
|
|
8475
|
+
} catch {
|
|
8476
|
+
this.settings.set("hideThinkingBlock", previousHideThinkingBlock ?? getDefault("hideThinkingBlock"));
|
|
8477
|
+
this.setThinkingVisibility(previousVisibility);
|
|
8478
|
+
await this.settings.flushOrThrow().catch(() => {});
|
|
8479
|
+
throw new Error("Unable to persist reasoning settings.");
|
|
8480
|
+
}
|
|
8481
|
+
}
|
|
8482
|
+
|
|
7285
8483
|
/**
|
|
7286
8484
|
* Cycle to next thinking level.
|
|
7287
8485
|
* @returns New level, or undefined if model doesn't support thinking
|
|
@@ -7482,7 +8680,7 @@ export class AgentSession {
|
|
|
7482
8680
|
// Compaction
|
|
7483
8681
|
// =========================================================================
|
|
7484
8682
|
|
|
7485
|
-
async #pruneToolOutputs(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
|
|
8683
|
+
async #pruneToolOutputs(signal?: AbortSignal): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
|
|
7486
8684
|
const branchEntries = this.sessionManager.getBranch();
|
|
7487
8685
|
const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG);
|
|
7488
8686
|
const argumentResult = pruneAssistantToolArguments(branchEntries, DEFAULT_PRUNE_CONFIG);
|
|
@@ -7492,7 +8690,7 @@ export class AgentSession {
|
|
|
7492
8690
|
const tokensSaved =
|
|
7493
8691
|
result.tokensSaved + argumentResult.argumentTokensSaved + Math.round(fileMentionResult.bytesSaved / 4);
|
|
7494
8692
|
const prunedCount = result.prunedCount + argumentResult.argumentPrunedCount + fileMentionResult.changed.length;
|
|
7495
|
-
if (prunedCount === 0) {
|
|
8693
|
+
if (prunedCount === 0 || signal?.aborted) {
|
|
7496
8694
|
return undefined;
|
|
7497
8695
|
}
|
|
7498
8696
|
|
|
@@ -7757,6 +8955,28 @@ export class AgentSession {
|
|
|
7757
8955
|
this.#handoffAbortController?.abort();
|
|
7758
8956
|
}
|
|
7759
8957
|
|
|
8958
|
+
#abortActiveMidRunBarriers(): void {
|
|
8959
|
+
for (const controller of this.#activeMidRunBarrierControllers) {
|
|
8960
|
+
controller.abort();
|
|
8961
|
+
}
|
|
8962
|
+
this.#activeMidRunBarrierControllers.clear();
|
|
8963
|
+
}
|
|
8964
|
+
|
|
8965
|
+
#trackMidRunMaintenance(maintenance: Promise<MidRunMaintenanceOutcome>): Promise<MidRunMaintenanceOutcome> {
|
|
8966
|
+
this.#activeMidRunMaintenancePromises.add(maintenance);
|
|
8967
|
+
maintenance.then(
|
|
8968
|
+
() => this.#activeMidRunMaintenancePromises.delete(maintenance),
|
|
8969
|
+
() => this.#activeMidRunMaintenancePromises.delete(maintenance),
|
|
8970
|
+
);
|
|
8971
|
+
return maintenance;
|
|
8972
|
+
}
|
|
8973
|
+
|
|
8974
|
+
async #waitForActiveMidRunMaintenance(): Promise<void> {
|
|
8975
|
+
while (this.#activeMidRunMaintenancePromises.size > 0) {
|
|
8976
|
+
await Promise.allSettled([...this.#activeMidRunMaintenancePromises]);
|
|
8977
|
+
}
|
|
8978
|
+
}
|
|
8979
|
+
|
|
7760
8980
|
/** Trigger idle compaction through the auto-compaction flow (with UI events). */
|
|
7761
8981
|
async runIdleCompaction(): Promise<void> {
|
|
7762
8982
|
if (this.isStreaming || this.isCompacting) return;
|
|
@@ -7914,6 +9134,13 @@ export class AgentSession {
|
|
|
7914
9134
|
this.#syncTodoPhasesFromBranch();
|
|
7915
9135
|
|
|
7916
9136
|
this.#resetIrcRosterDeliveryState();
|
|
9137
|
+
if (this.#extensionRunner) {
|
|
9138
|
+
await this.#extensionRunner.emit({
|
|
9139
|
+
type: "session_switch",
|
|
9140
|
+
reason: "new",
|
|
9141
|
+
previousSessionFile,
|
|
9142
|
+
});
|
|
9143
|
+
}
|
|
7917
9144
|
|
|
7918
9145
|
return { document: handoffText, savedPath };
|
|
7919
9146
|
} catch (error) {
|
|
@@ -7990,7 +9217,8 @@ export class AgentSession {
|
|
|
7990
9217
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
7991
9218
|
if (promoted) {
|
|
7992
9219
|
// Retry on the promoted (larger) model without compacting
|
|
7993
|
-
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
9220
|
+
this.#scheduleAgentContinue({ delayMs: 100, generation, suppressPredecessorAgentEnd: true });
|
|
9221
|
+
|
|
7994
9222
|
return;
|
|
7995
9223
|
}
|
|
7996
9224
|
|
|
@@ -8031,6 +9259,167 @@ export class AgentSession {
|
|
|
8031
9259
|
}
|
|
8032
9260
|
}
|
|
8033
9261
|
|
|
9262
|
+
/**
|
|
9263
|
+
* Cooperative mid-run context maintenance (issue #2035).
|
|
9264
|
+
*
|
|
9265
|
+
* Invoked by the agent loop (via {@link Agent.setMaintainContext}) at the top
|
|
9266
|
+
* of each tool-loop iteration — after pending tool-result / steering messages
|
|
9267
|
+
* are durable and before the model call. Threshold-based auto-compaction was
|
|
9268
|
+
* previously only evaluated at `agent_end` and before a user prompt, so a
|
|
9269
|
+
* single long agentic run grew straight through the compaction margin and died
|
|
9270
|
+
* with provider `context_length_exceeded`. This bounds that run.
|
|
9271
|
+
*
|
|
9272
|
+
* Decision input is the last assistant `usage.totalTokens` plus the estimated
|
|
9273
|
+
* trailing tool/steering deltas (NOT the pre-prompt estimator's prompt-only
|
|
9274
|
+
* anchor, which drops the last assistant's output). Reuses the existing
|
|
9275
|
+
* prune → promote → compact machinery and returns an explicit outcome.
|
|
9276
|
+
*
|
|
9277
|
+
* A non-"not-needed" outcome ends the current run losslessly
|
|
9278
|
+
* (`agent_end.stopReason === "maintenance"`); the `agent_end` handler is the
|
|
9279
|
+
* single continuation owner that resumes the run with no synthetic prompt.
|
|
9280
|
+
* This method never self-continues, runs goal-runtime hooks, clears skill
|
|
9281
|
+
* state, or emits a user-facing pause.
|
|
9282
|
+
*/
|
|
9283
|
+
async #runMidRunMaintenance(
|
|
9284
|
+
context: AgentContext,
|
|
9285
|
+
lifecycle: MidRunMaintenanceLifecycle,
|
|
9286
|
+
): Promise<MidRunMaintenanceOutcome> {
|
|
9287
|
+
if (this.#isDisposed) return "aborted";
|
|
9288
|
+
const invocationController = new AbortController();
|
|
9289
|
+
this.#activeMidRunBarrierControllers.add(invocationController);
|
|
9290
|
+
const maintenanceSignal = AbortSignal.any([lifecycle.signal, invocationController.signal]);
|
|
9291
|
+
const isAborted = () => maintenanceSignal.aborted;
|
|
9292
|
+
|
|
9293
|
+
try {
|
|
9294
|
+
try {
|
|
9295
|
+
await lifecycle.awaitEventDrain(invocationController.signal);
|
|
9296
|
+
} catch {
|
|
9297
|
+
return isAborted() ? "aborted" : "failed";
|
|
9298
|
+
}
|
|
9299
|
+
if (isAborted()) return "aborted";
|
|
9300
|
+
|
|
9301
|
+
// In-place context-full maintenance only. "off" defers entirely; "handoff"
|
|
9302
|
+
// keeps its existing agent_end / pre-prompt boundaries (a mid-tool-loop
|
|
9303
|
+
// session swap would be far more disruptive than the overflow it avoids).
|
|
9304
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
9305
|
+
if (!compactionSettings.enabled || compactionSettings.strategy !== "context-full") return "not-needed";
|
|
9306
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
9307
|
+
if (contextWindow <= 0) return "not-needed";
|
|
9308
|
+
// A compaction already in flight (overflow recovery, manual, idle) owns the
|
|
9309
|
+
// context; never double-compact underneath it.
|
|
9310
|
+
if (this.isCompacting) return "not-needed";
|
|
9311
|
+
|
|
9312
|
+
// Model maxTokens is a capability ceiling, not a per-turn reservation;
|
|
9313
|
+
// track actual context fullness (mirrors the agent_end / pre-prompt checks).
|
|
9314
|
+
const autoCompactionOutputReserveTokens = 0;
|
|
9315
|
+
const anchor = this.#findMidRunUsageAnchor(context.messages);
|
|
9316
|
+
let contextTokens = this.#estimateMidRunContextTokens(context.messages);
|
|
9317
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
|
|
9318
|
+
return "not-needed";
|
|
9319
|
+
}
|
|
9320
|
+
// Anti-loop (#1662): a given provider response anchors at most one
|
|
9321
|
+
// maintenance attempt. Until a NEW response re-anchors usage, repeat checks
|
|
9322
|
+
// on the same anchor are no-ops so a compaction that cannot shrink further
|
|
9323
|
+
// cannot wedge the loop into interrupt → resume → interrupt.
|
|
9324
|
+
const anchorSignature = anchor
|
|
9325
|
+
? `${anchor.message.provider}/${anchor.message.model}#${anchor.message.timestamp}#${calculateContextTokens(anchor.message.usage as Usage)}`
|
|
9326
|
+
: undefined;
|
|
9327
|
+
if (anchorSignature) {
|
|
9328
|
+
if (anchorSignature === this.#lastMidRunMaintenanceAnchorSignature) return "not-needed";
|
|
9329
|
+
this.#lastMidRunMaintenanceAnchorSignature = anchorSignature;
|
|
9330
|
+
}
|
|
9331
|
+
|
|
9332
|
+
// The FIFO consumer barrier made every prior materialized message canonical.
|
|
9333
|
+
// Flush those synchronous branch appends before any history rewrite.
|
|
9334
|
+
if (isAborted()) return "aborted";
|
|
9335
|
+
await this.sessionManager.flush();
|
|
9336
|
+
if (isAborted()) return "aborted";
|
|
9337
|
+
|
|
9338
|
+
// 1) Prune stale tool outputs first — cheaper than compaction, may avert it,
|
|
9339
|
+
// and (like all history rewrites) resets the codex provider session /
|
|
9340
|
+
// prompt-cache epoch via #closeCodexProviderSessionsForHistoryRewrite.
|
|
9341
|
+
if (isAborted()) return "aborted";
|
|
9342
|
+
const pruneResult = await this.#pruneToolOutputs(maintenanceSignal);
|
|
9343
|
+
if (isAborted()) return "aborted";
|
|
9344
|
+
if (pruneResult) {
|
|
9345
|
+
contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
9346
|
+
}
|
|
9347
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
|
|
9348
|
+
return pruneResult && pruneResult.prunedCount > 0 ? "pruned" : "not-needed";
|
|
9349
|
+
}
|
|
9350
|
+
|
|
9351
|
+
// 2) Try context promotion (switch to a larger-window model) before compacting.
|
|
9352
|
+
const lastAssistant = this.#findLastAssistantMessage();
|
|
9353
|
+
if (lastAssistant && lastAssistant.stopReason !== "aborted" && lastAssistant.stopReason !== "error") {
|
|
9354
|
+
if (isAborted()) return "aborted";
|
|
9355
|
+
const promoted = await this.#tryContextPromotion(lastAssistant, maintenanceSignal);
|
|
9356
|
+
if (isAborted()) return "aborted";
|
|
9357
|
+
if (promoted) return "promoted";
|
|
9358
|
+
}
|
|
9359
|
+
|
|
9360
|
+
// 3) Compact via the existing auto-compaction machinery. continueAfterMaintenance
|
|
9361
|
+
// is false so it does NOT schedule its own (synthetic) continuation — the
|
|
9362
|
+
// agent_end("maintenance") handler owns resumption. The oversized-maintenance
|
|
9363
|
+
// signature guard and the previous_response_id / prompt-cache-epoch reset
|
|
9364
|
+
// (#applyCompactionPostAppend) are inherited from #runAutoCompaction.
|
|
9365
|
+
if (isAborted()) return "aborted";
|
|
9366
|
+
const compactionStatus = await this.#runAutoCompaction("threshold", false, false, {
|
|
9367
|
+
continueAfterMaintenance: false,
|
|
9368
|
+
deferHandoffMaintenance: false,
|
|
9369
|
+
signal: maintenanceSignal,
|
|
9370
|
+
});
|
|
9371
|
+
if (isAborted()) return "aborted";
|
|
9372
|
+
if (compactionStatus.kind === "compacted") return "compacted";
|
|
9373
|
+
if (compactionStatus.kind === "aborted") {
|
|
9374
|
+
return compactionStatus.source === "hook" ? "not-needed" : "aborted";
|
|
9375
|
+
}
|
|
9376
|
+
return "failed";
|
|
9377
|
+
} finally {
|
|
9378
|
+
this.#activeMidRunBarrierControllers.delete(invocationController);
|
|
9379
|
+
}
|
|
9380
|
+
}
|
|
9381
|
+
|
|
9382
|
+
/**
|
|
9383
|
+
* Mid-run context-token estimate for {@link #runMidRunMaintenance}.
|
|
9384
|
+
*
|
|
9385
|
+
* Anchors on the last non-error assistant's `usage.totalTokens` — at the top
|
|
9386
|
+
* of a tool-loop iteration that output is already durable context that will be
|
|
9387
|
+
* re-sent — plus the inflated script-aware delta (see {@link #estimateMessageCompactionDeltaTokens}) of every trailing tool-result /
|
|
9388
|
+
* steering message appended since. Operates on the loop's authoritative
|
|
9389
|
+
* context view (not `this.messages`) so it is independent of listener flush
|
|
9390
|
+
* timing.
|
|
9391
|
+
*/
|
|
9392
|
+
#estimateMidRunContextTokens(messages: readonly AgentMessage[]): number {
|
|
9393
|
+
const anchor = this.#findMidRunUsageAnchor(messages);
|
|
9394
|
+
if (!anchor) {
|
|
9395
|
+
let estimated = 0;
|
|
9396
|
+
for (const message of messages) estimated += this.#estimateMessageCompactionDeltaTokens(message);
|
|
9397
|
+
return estimated;
|
|
9398
|
+
}
|
|
9399
|
+
let tokens = calculateContextTokens(anchor.message.usage as Usage);
|
|
9400
|
+
for (let i = anchor.index + 1; i < messages.length; i++) {
|
|
9401
|
+
tokens += this.#estimateMessageCompactionDeltaTokens(messages[i]);
|
|
9402
|
+
}
|
|
9403
|
+
return tokens;
|
|
9404
|
+
}
|
|
9405
|
+
|
|
9406
|
+
/**
|
|
9407
|
+
* Locate the anchor for {@link #estimateMidRunContextTokens}: the most recent
|
|
9408
|
+
* non-aborted/non-error assistant message that carries provider usage. Also
|
|
9409
|
+
* keys the anti-loop guard so each provider response drives at most one
|
|
9410
|
+
* maintenance attempt.
|
|
9411
|
+
*/
|
|
9412
|
+
#findMidRunUsageAnchor(messages: readonly AgentMessage[]): { index: number; message: AssistantMessage } | undefined {
|
|
9413
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
9414
|
+
const msg = messages[i];
|
|
9415
|
+
if (msg.role !== "assistant") continue;
|
|
9416
|
+
const assistantMsg = msg as AssistantMessage;
|
|
9417
|
+
if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") continue;
|
|
9418
|
+
if (assistantMsg.usage) return { index: i, message: assistantMsg };
|
|
9419
|
+
}
|
|
9420
|
+
return undefined;
|
|
9421
|
+
}
|
|
9422
|
+
|
|
8034
9423
|
async #checkEstimatedContextBeforePrompt(pendingMessages: readonly AgentMessage[] = []): Promise<void> {
|
|
8035
9424
|
if (this.#prePromptContextCheckPromise) {
|
|
8036
9425
|
await this.#prePromptContextCheckPromise;
|
|
@@ -8407,7 +9796,8 @@ export class AgentSession {
|
|
|
8407
9796
|
* Attempt context promotion to a larger model.
|
|
8408
9797
|
* Returns true if promotion succeeded (caller should retry without compacting).
|
|
8409
9798
|
*/
|
|
8410
|
-
async #tryContextPromotion(assistantMessage: AssistantMessage): Promise<boolean> {
|
|
9799
|
+
async #tryContextPromotion(assistantMessage: AssistantMessage, signal?: AbortSignal): Promise<boolean> {
|
|
9800
|
+
if (signal?.aborted) return false;
|
|
8411
9801
|
const promotionSettings = this.settings.getGroup("contextPromotion");
|
|
8412
9802
|
if (!promotionSettings.enabled) return false;
|
|
8413
9803
|
const currentModel = this.model;
|
|
@@ -8416,11 +9806,19 @@ export class AgentSession {
|
|
|
8416
9806
|
return false;
|
|
8417
9807
|
const contextWindow = currentModel.contextWindow ?? 0;
|
|
8418
9808
|
if (contextWindow <= 0) return false;
|
|
8419
|
-
const targetModel = await this.#resolveContextPromotionTarget(currentModel, contextWindow);
|
|
8420
|
-
if (!targetModel) return false;
|
|
9809
|
+
const targetModel = await this.#resolveContextPromotionTarget(currentModel, contextWindow, signal);
|
|
9810
|
+
if (!targetModel || signal?.aborted) return false;
|
|
8421
9811
|
|
|
8422
9812
|
try {
|
|
8423
|
-
await this.setModelTemporary(targetModel
|
|
9813
|
+
const scope = await this.setModelTemporary(targetModel, undefined, {
|
|
9814
|
+
cause: "temporary-operation",
|
|
9815
|
+
reason: "context-promotion",
|
|
9816
|
+
signal,
|
|
9817
|
+
});
|
|
9818
|
+
if (signal?.aborted) {
|
|
9819
|
+
if (scope) this.restoreTemporaryProviderSessionScope(scope);
|
|
9820
|
+
return false;
|
|
9821
|
+
}
|
|
8424
9822
|
logger.debug("Context promotion switched model on overflow", {
|
|
8425
9823
|
from: `${currentModel.provider}/${currentModel.id}`,
|
|
8426
9824
|
to: `${targetModel.provider}/${targetModel.id}`,
|
|
@@ -8436,7 +9834,12 @@ export class AgentSession {
|
|
|
8436
9834
|
}
|
|
8437
9835
|
}
|
|
8438
9836
|
|
|
8439
|
-
async #resolveContextPromotionTarget(
|
|
9837
|
+
async #resolveContextPromotionTarget(
|
|
9838
|
+
currentModel: Model,
|
|
9839
|
+
contextWindow: number,
|
|
9840
|
+
signal?: AbortSignal,
|
|
9841
|
+
): Promise<Model | undefined> {
|
|
9842
|
+
if (signal?.aborted) return undefined;
|
|
8440
9843
|
const availableModels = this.#modelRegistry.getAvailable();
|
|
8441
9844
|
if (availableModels.length === 0) return undefined;
|
|
8442
9845
|
|
|
@@ -8445,18 +9848,37 @@ export class AgentSession {
|
|
|
8445
9848
|
if (modelsAreEqual(candidate, currentModel)) return undefined;
|
|
8446
9849
|
if (candidate.contextWindow <= contextWindow) return undefined;
|
|
8447
9850
|
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
8448
|
-
if (!apiKey) return undefined;
|
|
9851
|
+
if (!apiKey || signal?.aborted) return undefined;
|
|
8449
9852
|
return candidate;
|
|
8450
9853
|
}
|
|
8451
9854
|
|
|
9855
|
+
#canonicalSelector(model: Model, selector?: string, thinkingLevel?: ThinkingLevel): string {
|
|
9856
|
+
const parsed = selector ? parseModelString(selector) : undefined;
|
|
9857
|
+
const base = parsed ? `${parsed.provider}/${parsed.id}` : `${model.provider}/${model.id}`;
|
|
9858
|
+
return thinkingLevel === undefined ? (selector ?? base) : `${base}:${String(thinkingLevel).toLowerCase()}`;
|
|
9859
|
+
}
|
|
9860
|
+
|
|
9861
|
+
#clearActiveRetryFallback(): void {
|
|
9862
|
+
this.#defaultFallbackController = undefined;
|
|
9863
|
+
this.#defaultFallbackExhaustedLastTurn = false;
|
|
9864
|
+
}
|
|
9865
|
+
|
|
8452
9866
|
#setModelWithProviderSessionReset(model: Model | undefined): void {
|
|
9867
|
+
this.#defaultModelSelectionMutationRevision++;
|
|
8453
9868
|
const currentModel = this.model;
|
|
8454
9869
|
if (currentModel) {
|
|
8455
9870
|
this.#closeProviderSessionsForModelSwitch(currentModel, model);
|
|
8456
9871
|
}
|
|
8457
9872
|
this.agent.setModel(model);
|
|
9873
|
+
}
|
|
8458
9874
|
|
|
8459
|
-
|
|
9875
|
+
#setModelAuthoritatively(model: Model, cause: ModelChangeCause): void {
|
|
9876
|
+
// Only a non-temporary cause makes a model switch authoritative: it commits
|
|
9877
|
+
// any suspended provider-session scopes instead of restoring them later.
|
|
9878
|
+
if (cause !== "temporary-operation") this.#commitAllTemporaryProviderSessionScopes();
|
|
9879
|
+
const currentModel = this.model;
|
|
9880
|
+
if (currentModel) this.#closeProviderSessionsForModelSwitch(currentModel, model);
|
|
9881
|
+
this.agent.setModel(model);
|
|
8460
9882
|
this.#syncAppendOnlyContext(model);
|
|
8461
9883
|
}
|
|
8462
9884
|
|
|
@@ -8974,13 +10396,18 @@ export class AgentSession {
|
|
|
8974
10396
|
reason: "overflow" | "threshold" | "idle",
|
|
8975
10397
|
willRetry: boolean,
|
|
8976
10398
|
deferred = false,
|
|
8977
|
-
options?: {
|
|
8978
|
-
|
|
10399
|
+
options?: {
|
|
10400
|
+
continueAfterMaintenance?: boolean;
|
|
10401
|
+
deferHandoffMaintenance?: boolean;
|
|
10402
|
+
force?: boolean;
|
|
10403
|
+
signal?: AbortSignal;
|
|
10404
|
+
},
|
|
10405
|
+
): Promise<AutoCompactionTerminalStatus> {
|
|
8979
10406
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
8980
10407
|
// `force` is the non-disableable emergency floor (F6): it bypasses the user's
|
|
8981
10408
|
// disabled/off settings so a resource-floor breach still compacts before OOM.
|
|
8982
|
-
if (!options?.force && compactionSettings.strategy === "off") return;
|
|
8983
|
-
if (!options?.force && reason !== "idle" && !compactionSettings.enabled) return;
|
|
10409
|
+
if (!options?.force && compactionSettings.strategy === "off") return { kind: "skipped" };
|
|
10410
|
+
if (!options?.force && reason !== "idle" && !compactionSettings.enabled) return { kind: "skipped" };
|
|
8984
10411
|
const generation = this.#promptGeneration;
|
|
8985
10412
|
if (
|
|
8986
10413
|
options?.deferHandoffMaintenance !== false &&
|
|
@@ -8997,27 +10424,44 @@ export class AgentSession {
|
|
|
8997
10424
|
},
|
|
8998
10425
|
{ generation },
|
|
8999
10426
|
);
|
|
9000
|
-
return;
|
|
10427
|
+
return { kind: "skipped" };
|
|
9001
10428
|
}
|
|
9002
10429
|
|
|
9003
10430
|
let action: "context-full" | "handoff" =
|
|
9004
10431
|
compactionSettings.strategy === "handoff" && reason !== "overflow" ? "handoff" : "context-full";
|
|
9005
10432
|
const continueAfterMaintenance = options?.continueAfterMaintenance !== false;
|
|
9006
|
-
|
|
9007
|
-
//
|
|
10433
|
+
// Register the controller before the observable start event so an abort from
|
|
10434
|
+
// a local subscriber, extension, dispose, or caller signal owns this run.
|
|
9008
10435
|
this.#autoCompactionAbortController?.abort();
|
|
9009
10436
|
const autoCompactionAbortController = new AbortController();
|
|
9010
10437
|
this.#autoCompactionAbortController = autoCompactionAbortController;
|
|
9011
|
-
const autoCompactionSignal =
|
|
10438
|
+
const autoCompactionSignal = options?.signal
|
|
10439
|
+
? AbortSignal.any([autoCompactionAbortController.signal, options.signal])
|
|
10440
|
+
: autoCompactionAbortController.signal;
|
|
9012
10441
|
let maintenanceAttemptSignature: string | undefined;
|
|
10442
|
+
const emitAborted = async (): Promise<AutoCompactionTerminalStatus> => {
|
|
10443
|
+
await this.#emitSessionEvent({
|
|
10444
|
+
type: "auto_compaction_end",
|
|
10445
|
+
action,
|
|
10446
|
+
result: undefined,
|
|
10447
|
+
aborted: true,
|
|
10448
|
+
willRetry: false,
|
|
10449
|
+
});
|
|
10450
|
+
return { kind: "aborted", source: "signal" };
|
|
10451
|
+
};
|
|
9013
10452
|
|
|
9014
10453
|
try {
|
|
10454
|
+
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
10455
|
+
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
10456
|
+
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
10457
|
+
|
|
9015
10458
|
if (compactionSettings.strategy === "handoff" && reason !== "overflow") {
|
|
9016
10459
|
const handoffFocus = AUTO_HANDOFF_THRESHOLD_FOCUS;
|
|
9017
10460
|
const handoffResult = await this.handoff(handoffFocus, {
|
|
9018
10461
|
autoTriggered: true,
|
|
9019
|
-
signal:
|
|
10462
|
+
signal: autoCompactionSignal,
|
|
9020
10463
|
});
|
|
10464
|
+
|
|
9021
10465
|
if (!handoffResult) {
|
|
9022
10466
|
const aborted = autoCompactionSignal.aborted;
|
|
9023
10467
|
if (aborted) {
|
|
@@ -9028,13 +10472,15 @@ export class AgentSession {
|
|
|
9028
10472
|
aborted: true,
|
|
9029
10473
|
willRetry: false,
|
|
9030
10474
|
});
|
|
9031
|
-
return;
|
|
10475
|
+
return { kind: "aborted", source: "signal" };
|
|
9032
10476
|
}
|
|
9033
10477
|
logger.warn("Auto-handoff returned no document; falling back to context-full maintenance", {
|
|
9034
10478
|
reason,
|
|
9035
10479
|
});
|
|
9036
10480
|
action = "context-full";
|
|
9037
10481
|
}
|
|
10482
|
+
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
10483
|
+
|
|
9038
10484
|
if (handoffResult) {
|
|
9039
10485
|
await this.#emitSessionEvent({
|
|
9040
10486
|
type: "auto_compaction_end",
|
|
@@ -9043,15 +10489,12 @@ export class AgentSession {
|
|
|
9043
10489
|
aborted: false,
|
|
9044
10490
|
willRetry: false,
|
|
9045
10491
|
});
|
|
9046
|
-
if (
|
|
9047
|
-
|
|
9048
|
-
!autoCompactionSignal.aborted &&
|
|
9049
|
-
reason !== "idle" &&
|
|
9050
|
-
compactionSettings.autoContinue !== false
|
|
9051
|
-
) {
|
|
10492
|
+
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
10493
|
+
if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
9052
10494
|
this.#scheduleAutoContinuePrompt(generation);
|
|
9053
10495
|
}
|
|
9054
|
-
return;
|
|
10496
|
+
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
10497
|
+
return { kind: "compacted" };
|
|
9055
10498
|
}
|
|
9056
10499
|
}
|
|
9057
10500
|
|
|
@@ -9064,7 +10507,7 @@ export class AgentSession {
|
|
|
9064
10507
|
willRetry: false,
|
|
9065
10508
|
skipped: true,
|
|
9066
10509
|
});
|
|
9067
|
-
return;
|
|
10510
|
+
return { kind: "skipped" };
|
|
9068
10511
|
}
|
|
9069
10512
|
|
|
9070
10513
|
const availableModels = this.#modelRegistry.getAvailable();
|
|
@@ -9077,9 +10520,11 @@ export class AgentSession {
|
|
|
9077
10520
|
willRetry: false,
|
|
9078
10521
|
skipped: true,
|
|
9079
10522
|
});
|
|
9080
|
-
return;
|
|
10523
|
+
return { kind: "skipped" };
|
|
9081
10524
|
}
|
|
9082
10525
|
|
|
10526
|
+
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
10527
|
+
|
|
9083
10528
|
const pathEntries = this.sessionManager.getBranch();
|
|
9084
10529
|
|
|
9085
10530
|
// Emergency/overflow-recovery compaction is conservative: apply the token
|
|
@@ -9089,6 +10534,8 @@ export class AgentSession {
|
|
|
9089
10534
|
const preparation = prepareCompaction(pathEntries, compactionSettings, {
|
|
9090
10535
|
tokenCorrectionRatio: overflowRatio !== undefined ? Math.max(1, overflowRatio) : undefined,
|
|
9091
10536
|
});
|
|
10537
|
+
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
10538
|
+
|
|
9092
10539
|
if (!preparation) {
|
|
9093
10540
|
const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
|
|
9094
10541
|
await this.#emitSessionEvent({
|
|
@@ -9106,6 +10553,8 @@ export class AgentSession {
|
|
|
9106
10553
|
this.#scheduleAgentContinue({
|
|
9107
10554
|
delayMs: 100,
|
|
9108
10555
|
generation,
|
|
10556
|
+
suppressPredecessorAgentEnd: true,
|
|
10557
|
+
|
|
9109
10558
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
9110
10559
|
onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
|
|
9111
10560
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
@@ -9113,7 +10562,7 @@ export class AgentSession {
|
|
|
9113
10562
|
} else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
9114
10563
|
this.#scheduleAutoContinuePrompt(generation);
|
|
9115
10564
|
}
|
|
9116
|
-
return;
|
|
10565
|
+
return { kind: "skipped" };
|
|
9117
10566
|
}
|
|
9118
10567
|
|
|
9119
10568
|
let hookCompaction: CompactionResult | undefined;
|
|
@@ -9128,6 +10577,7 @@ export class AgentSession {
|
|
|
9128
10577
|
customInstructions: undefined,
|
|
9129
10578
|
signal: autoCompactionSignal,
|
|
9130
10579
|
})) as SessionBeforeCompactResult | undefined;
|
|
10580
|
+
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
9131
10581
|
|
|
9132
10582
|
if (hookResult?.cancel) {
|
|
9133
10583
|
await this.#emitSessionEvent({
|
|
@@ -9137,7 +10587,7 @@ export class AgentSession {
|
|
|
9137
10587
|
aborted: true,
|
|
9138
10588
|
willRetry: false,
|
|
9139
10589
|
});
|
|
9140
|
-
return;
|
|
10590
|
+
return { kind: "aborted", source: "hook" };
|
|
9141
10591
|
}
|
|
9142
10592
|
|
|
9143
10593
|
if (hookResult?.compaction) {
|
|
@@ -9147,6 +10597,7 @@ export class AgentSession {
|
|
|
9147
10597
|
}
|
|
9148
10598
|
|
|
9149
10599
|
const compactionPrep = await this.#prepareCompactionFromHooks(preparation, hookCompaction);
|
|
10600
|
+
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
9150
10601
|
|
|
9151
10602
|
let summary: string;
|
|
9152
10603
|
let shortSummary: string | undefined;
|
|
@@ -9181,7 +10632,7 @@ export class AgentSession {
|
|
|
9181
10632
|
errorMessage:
|
|
9182
10633
|
"Auto-compaction skipped: previous unchanged maintenance request exceeded the model context window; change or reduce the conversation before retrying maintenance.",
|
|
9183
10634
|
});
|
|
9184
|
-
return;
|
|
10635
|
+
return { kind: "skipped" };
|
|
9185
10636
|
}
|
|
9186
10637
|
const retrySettings = this.settings.getGroup("retry");
|
|
9187
10638
|
const telemetry = resolveTelemetry(this.agent.telemetry, this.sessionId);
|
|
@@ -9195,6 +10646,8 @@ export class AgentSession {
|
|
|
9195
10646
|
let attempt = 0;
|
|
9196
10647
|
while (true) {
|
|
9197
10648
|
try {
|
|
10649
|
+
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
10650
|
+
|
|
9198
10651
|
compactResult = await compact(preparation, candidate, apiKey, undefined, autoCompactionSignal, {
|
|
9199
10652
|
...this.#maintenanceProviderTransport(),
|
|
9200
10653
|
promptOverride: compactionPrep.hookPrompt,
|
|
@@ -9293,7 +10746,7 @@ export class AgentSession {
|
|
|
9293
10746
|
aborted: true,
|
|
9294
10747
|
willRetry: false,
|
|
9295
10748
|
});
|
|
9296
|
-
return;
|
|
10749
|
+
return { kind: "aborted", source: "signal" };
|
|
9297
10750
|
}
|
|
9298
10751
|
|
|
9299
10752
|
const compactionEntryId = this.sessionManager.appendCompaction(
|
|
@@ -9306,6 +10759,7 @@ export class AgentSession {
|
|
|
9306
10759
|
preserveData,
|
|
9307
10760
|
);
|
|
9308
10761
|
await this.#applyCompactionPostAppend(compactionEntryId, firstKeptEntryId, fromExtension);
|
|
10762
|
+
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
9309
10763
|
|
|
9310
10764
|
const result: CompactionResult = {
|
|
9311
10765
|
summary,
|
|
@@ -9326,6 +10780,7 @@ export class AgentSession {
|
|
|
9326
10780
|
willRetry: willRetry && !continuationSkipReason,
|
|
9327
10781
|
continuationSkipReason,
|
|
9328
10782
|
});
|
|
10783
|
+
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
9329
10784
|
|
|
9330
10785
|
if (willRetry) {
|
|
9331
10786
|
this.#scheduleOverflowRetryContinuation(generation);
|
|
@@ -9335,6 +10790,7 @@ export class AgentSession {
|
|
|
9335
10790
|
this.#scheduleAgentContinue({
|
|
9336
10791
|
delayMs: 100,
|
|
9337
10792
|
generation,
|
|
10793
|
+
suppressPredecessorAgentEnd: true,
|
|
9338
10794
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
9339
10795
|
onSkip: reason => this.#logCompactionContinuationSkipped("queued_continue", reason),
|
|
9340
10796
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
@@ -9342,6 +10798,7 @@ export class AgentSession {
|
|
|
9342
10798
|
} else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
9343
10799
|
this.#scheduleAutoContinuePrompt(generation);
|
|
9344
10800
|
}
|
|
10801
|
+
return { kind: "compacted" };
|
|
9345
10802
|
} catch (error) {
|
|
9346
10803
|
if (autoCompactionSignal.aborted) {
|
|
9347
10804
|
await this.#emitSessionEvent({
|
|
@@ -9351,7 +10808,7 @@ export class AgentSession {
|
|
|
9351
10808
|
aborted: true,
|
|
9352
10809
|
willRetry: false,
|
|
9353
10810
|
});
|
|
9354
|
-
return;
|
|
10811
|
+
return { kind: "aborted", source: "signal" };
|
|
9355
10812
|
}
|
|
9356
10813
|
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
|
9357
10814
|
if (maintenanceAttemptSignature && this.#isOversizedMaintenanceError(errorMessage)) {
|
|
@@ -9368,6 +10825,7 @@ export class AgentSession {
|
|
|
9368
10825
|
? `Context overflow recovery failed: ${errorMessage}`
|
|
9369
10826
|
: `Auto-compaction failed: ${errorMessage}`,
|
|
9370
10827
|
});
|
|
10828
|
+
return { kind: "failed" };
|
|
9371
10829
|
} finally {
|
|
9372
10830
|
if (this.#autoCompactionAbortController === autoCompactionAbortController) {
|
|
9373
10831
|
this.#autoCompactionAbortController = undefined;
|
|
@@ -9400,20 +10858,30 @@ export class AgentSession {
|
|
|
9400
10858
|
* (auth/400/not-found) surface immediately; usage-limit, transient, and
|
|
9401
10859
|
* unknown/no-code errors are retryable.
|
|
9402
10860
|
*/
|
|
10861
|
+
|
|
9403
10862
|
#isRetryableError(message: AssistantMessage): boolean {
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
10863
|
+
if (message.errorMessage?.startsWith("Model fallback chain exhausted;")) return false;
|
|
10864
|
+
const managedFallback = this.#defaultFallbackChain().chain.entries.length > 1;
|
|
10865
|
+
if (!managedFallback) {
|
|
10866
|
+
const classification = this.#classifyErrorForRetry(message);
|
|
10867
|
+
return (
|
|
10868
|
+
classification === "usage_limit" ||
|
|
10869
|
+
classification === "transient" ||
|
|
10870
|
+
classification === "unknown" ||
|
|
10871
|
+
classification === "first_event_timeout"
|
|
10872
|
+
);
|
|
9410
10873
|
}
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
10874
|
+
const trigger = classifyFallbackTrigger({ message: message.errorMessage, status: message.errorStatus });
|
|
10875
|
+
if (
|
|
10876
|
+
trigger.class === "rate_limit" ||
|
|
10877
|
+
trigger.class === "quota" ||
|
|
10878
|
+
trigger.class === "auth" ||
|
|
10879
|
+
trigger.class === "server"
|
|
10880
|
+
) {
|
|
10881
|
+
return true;
|
|
10882
|
+
}
|
|
10883
|
+
const classification = this.#classifyErrorForRetry(message);
|
|
10884
|
+
return classification === "transient" || classification === "unknown" || classification === "first_event_timeout";
|
|
9417
10885
|
}
|
|
9418
10886
|
|
|
9419
10887
|
#isTransientErrorMessage(errorMessage: string): boolean {
|
|
@@ -9548,232 +11016,6 @@ export class AgentSession {
|
|
|
9548
11016
|
return "unknown";
|
|
9549
11017
|
}
|
|
9550
11018
|
|
|
9551
|
-
#getRetryFallbackChains(): RetryFallbackChains {
|
|
9552
|
-
const configuredChains = this.settings.get("retry.fallbackChains");
|
|
9553
|
-
if (!configuredChains || typeof configuredChains !== "object") return {};
|
|
9554
|
-
return configuredChains as RetryFallbackChains;
|
|
9555
|
-
}
|
|
9556
|
-
|
|
9557
|
-
#validateRetryFallbackChains(): void {
|
|
9558
|
-
const configuredChains = this.settings.get("retry.fallbackChains");
|
|
9559
|
-
if (configuredChains === undefined) return;
|
|
9560
|
-
if (!configuredChains || typeof configuredChains !== "object" || Array.isArray(configuredChains)) {
|
|
9561
|
-
const msg = "retry.fallbackChains must be a mapping of role names to selector arrays.";
|
|
9562
|
-
logger.warn(msg);
|
|
9563
|
-
this.configWarnings.push(msg);
|
|
9564
|
-
return;
|
|
9565
|
-
}
|
|
9566
|
-
|
|
9567
|
-
for (const [role, chain] of Object.entries(configuredChains)) {
|
|
9568
|
-
if (!Array.isArray(chain)) {
|
|
9569
|
-
const msg = `Fallback chain for role '${role}' must be an array of selector strings.`;
|
|
9570
|
-
logger.warn(msg);
|
|
9571
|
-
this.configWarnings.push(msg);
|
|
9572
|
-
continue;
|
|
9573
|
-
}
|
|
9574
|
-
for (const selectorStr of chain) {
|
|
9575
|
-
if (typeof selectorStr !== "string") {
|
|
9576
|
-
const msg = `Fallback chain for role '${role}' contains a non-string selector.`;
|
|
9577
|
-
logger.warn(msg);
|
|
9578
|
-
this.configWarnings.push(msg);
|
|
9579
|
-
continue;
|
|
9580
|
-
}
|
|
9581
|
-
const parsed = parseRetryFallbackSelector(selectorStr);
|
|
9582
|
-
if (!parsed) {
|
|
9583
|
-
const msg = `Invalid fallback selector format in role '${role}': ${selectorStr}`;
|
|
9584
|
-
logger.warn(msg);
|
|
9585
|
-
this.configWarnings.push(msg);
|
|
9586
|
-
continue;
|
|
9587
|
-
}
|
|
9588
|
-
const exists = this.#modelRegistry.find(parsed.provider, parsed.id);
|
|
9589
|
-
if (!exists) {
|
|
9590
|
-
const msg = `Fallback chain for role '${role}' references unknown model: ${selectorStr}`;
|
|
9591
|
-
logger.warn(msg);
|
|
9592
|
-
this.configWarnings.push(msg);
|
|
9593
|
-
}
|
|
9594
|
-
}
|
|
9595
|
-
}
|
|
9596
|
-
}
|
|
9597
|
-
|
|
9598
|
-
#getRetryFallbackRevertPolicy(): RetryFallbackRevertPolicy {
|
|
9599
|
-
return this.settings.get("retry.fallbackRevertPolicy") === "never" ? "never" : "cooldown-expiry";
|
|
9600
|
-
}
|
|
9601
|
-
|
|
9602
|
-
#getRetryFallbackPrimarySelector(role: string): RetryFallbackSelector | undefined {
|
|
9603
|
-
const configuredSelector = this.settings.getModelRole(role);
|
|
9604
|
-
return configuredSelector ? parseRetryFallbackSelector(configuredSelector) : undefined;
|
|
9605
|
-
}
|
|
9606
|
-
|
|
9607
|
-
#clearActiveRetryFallback(): void {
|
|
9608
|
-
this.#activeRetryFallback = undefined;
|
|
9609
|
-
}
|
|
9610
|
-
|
|
9611
|
-
#isRetryFallbackSelectorSuppressed(selector: RetryFallbackSelector): boolean {
|
|
9612
|
-
return this.#modelRegistry.isSelectorSuppressed(selector.raw);
|
|
9613
|
-
}
|
|
9614
|
-
|
|
9615
|
-
#noteRetryFallbackCooldown(currentSelector: string, retryAfterMs: number | undefined, errorMessage: string): void {
|
|
9616
|
-
let cooldownMs = retryAfterMs;
|
|
9617
|
-
if (!cooldownMs || cooldownMs <= 0) {
|
|
9618
|
-
const reason = parseRateLimitReason(errorMessage);
|
|
9619
|
-
cooldownMs = reason === "UNKNOWN" ? 5 * 60 * 1000 : calculateRateLimitBackoffMs(reason);
|
|
9620
|
-
}
|
|
9621
|
-
this.#modelRegistry.suppressSelector(currentSelector, Date.now() + cooldownMs);
|
|
9622
|
-
}
|
|
9623
|
-
|
|
9624
|
-
#resolveRetryFallbackRole(currentSelector: string): string | undefined {
|
|
9625
|
-
const parsedCurrent = parseRetryFallbackSelector(currentSelector);
|
|
9626
|
-
if (!parsedCurrent) return undefined;
|
|
9627
|
-
const currentBaseSelector = formatRetryFallbackBaseSelector(parsedCurrent);
|
|
9628
|
-
for (const role of Object.keys(this.#getRetryFallbackChains())) {
|
|
9629
|
-
const primarySelector = this.#getRetryFallbackPrimarySelector(role);
|
|
9630
|
-
if (!primarySelector) continue;
|
|
9631
|
-
if (primarySelector.raw === currentSelector) return role;
|
|
9632
|
-
if (formatRetryFallbackBaseSelector(primarySelector) === currentBaseSelector) return role;
|
|
9633
|
-
}
|
|
9634
|
-
return undefined;
|
|
9635
|
-
}
|
|
9636
|
-
|
|
9637
|
-
#getRetryFallbackEffectiveChain(role: string): RetryFallbackSelector[] {
|
|
9638
|
-
const primarySelector = this.#getRetryFallbackPrimarySelector(role);
|
|
9639
|
-
if (!primarySelector) return [];
|
|
9640
|
-
const chain = [primarySelector];
|
|
9641
|
-
const seen = new Set<string>([primarySelector.raw]);
|
|
9642
|
-
for (const selector of this.#getRetryFallbackChains()[role] ?? []) {
|
|
9643
|
-
const parsed = parseRetryFallbackSelector(selector);
|
|
9644
|
-
if (!parsed || seen.has(parsed.raw)) continue;
|
|
9645
|
-
seen.add(parsed.raw);
|
|
9646
|
-
chain.push(parsed);
|
|
9647
|
-
}
|
|
9648
|
-
return chain;
|
|
9649
|
-
}
|
|
9650
|
-
|
|
9651
|
-
#hasRetryFallbackCandidate(options: { currentSelector: string | undefined; requireNonLocal?: boolean }): boolean {
|
|
9652
|
-
if (!options.currentSelector) return false;
|
|
9653
|
-
const role = this.#activeRetryFallback?.role ?? this.#resolveRetryFallbackRole(options.currentSelector);
|
|
9654
|
-
if (!role) return false;
|
|
9655
|
-
for (const selector of this.#findRetryFallbackCandidates(role, options.currentSelector)) {
|
|
9656
|
-
if (this.#isRetryFallbackSelectorSuppressed(selector)) continue;
|
|
9657
|
-
const candidate = this.#modelRegistry.find(selector.provider, selector.id);
|
|
9658
|
-
if (!candidate) continue;
|
|
9659
|
-
if (options.requireNonLocal && isLocalModelEndpoint(candidate)) continue;
|
|
9660
|
-
return true;
|
|
9661
|
-
}
|
|
9662
|
-
return false;
|
|
9663
|
-
}
|
|
9664
|
-
|
|
9665
|
-
#findRetryFallbackCandidates(role: string, currentSelector: string): RetryFallbackSelector[] {
|
|
9666
|
-
const chain = this.#getRetryFallbackEffectiveChain(role);
|
|
9667
|
-
if (chain.length <= 1) return [];
|
|
9668
|
-
const parsedCurrent = parseRetryFallbackSelector(currentSelector);
|
|
9669
|
-
const currentBaseSelector = parsedCurrent ? formatRetryFallbackBaseSelector(parsedCurrent) : undefined;
|
|
9670
|
-
const exactIndex = chain.findIndex(selector => selector.raw === currentSelector);
|
|
9671
|
-
if (exactIndex >= 0) return chain.slice(exactIndex + 1);
|
|
9672
|
-
const baseIndex = currentBaseSelector
|
|
9673
|
-
? chain.findIndex(selector => formatRetryFallbackBaseSelector(selector) === currentBaseSelector)
|
|
9674
|
-
: -1;
|
|
9675
|
-
if (baseIndex >= 0) return chain.slice(baseIndex + 1);
|
|
9676
|
-
return chain.slice(1);
|
|
9677
|
-
}
|
|
9678
|
-
|
|
9679
|
-
async #applyRetryFallbackCandidate(
|
|
9680
|
-
role: string,
|
|
9681
|
-
selector: RetryFallbackSelector,
|
|
9682
|
-
currentSelector: string,
|
|
9683
|
-
): Promise<void> {
|
|
9684
|
-
const candidate = this.#modelRegistry.find(selector.provider, selector.id);
|
|
9685
|
-
if (!candidate) {
|
|
9686
|
-
throw new Error(`Retry fallback model not found: ${selector.raw}`);
|
|
9687
|
-
}
|
|
9688
|
-
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
9689
|
-
if (!apiKey) {
|
|
9690
|
-
throw new Error(`No API key for retry fallback ${selector.raw}`);
|
|
9691
|
-
}
|
|
9692
|
-
|
|
9693
|
-
const currentThinkingLevel = this.thinkingLevel;
|
|
9694
|
-
const nextThinkingLevel = selector.thinkingLevel ?? currentThinkingLevel;
|
|
9695
|
-
|
|
9696
|
-
this.#setModelWithProviderSessionReset(candidate);
|
|
9697
|
-
this.sessionManager.appendModelChange(`${candidate.provider}/${candidate.id}`, "temporary");
|
|
9698
|
-
this.settings.getStorage()?.recordModelUsage(`${candidate.provider}/${candidate.id}`);
|
|
9699
|
-
this.setThinkingLevel(nextThinkingLevel);
|
|
9700
|
-
if (!this.#activeRetryFallback) {
|
|
9701
|
-
this.#activeRetryFallback = {
|
|
9702
|
-
role,
|
|
9703
|
-
originalSelector: currentSelector,
|
|
9704
|
-
originalThinkingLevel: currentThinkingLevel,
|
|
9705
|
-
lastAppliedFallbackThinkingLevel: nextThinkingLevel,
|
|
9706
|
-
};
|
|
9707
|
-
} else {
|
|
9708
|
-
this.#activeRetryFallback.lastAppliedFallbackThinkingLevel = nextThinkingLevel;
|
|
9709
|
-
}
|
|
9710
|
-
await this.#emitSessionEvent({
|
|
9711
|
-
type: "retry_fallback_applied",
|
|
9712
|
-
from: currentSelector,
|
|
9713
|
-
to: selector.raw,
|
|
9714
|
-
role,
|
|
9715
|
-
});
|
|
9716
|
-
}
|
|
9717
|
-
|
|
9718
|
-
async #tryRetryModelFallback(currentSelector: string, options?: { requireNonLocal?: boolean }): Promise<boolean> {
|
|
9719
|
-
const role = this.#activeRetryFallback?.role ?? this.#resolveRetryFallbackRole(currentSelector);
|
|
9720
|
-
if (!role) return false;
|
|
9721
|
-
|
|
9722
|
-
for (const selector of this.#findRetryFallbackCandidates(role, currentSelector)) {
|
|
9723
|
-
if (this.#isRetryFallbackSelectorSuppressed(selector)) continue;
|
|
9724
|
-
const candidate = this.#modelRegistry.find(selector.provider, selector.id);
|
|
9725
|
-
if (!candidate) continue;
|
|
9726
|
-
if (options?.requireNonLocal && isLocalModelEndpoint(candidate)) continue;
|
|
9727
|
-
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
9728
|
-
if (!apiKey) continue;
|
|
9729
|
-
await this.#applyRetryFallbackCandidate(role, selector, currentSelector);
|
|
9730
|
-
return true;
|
|
9731
|
-
}
|
|
9732
|
-
|
|
9733
|
-
return false;
|
|
9734
|
-
}
|
|
9735
|
-
|
|
9736
|
-
async #maybeRestoreRetryFallbackPrimary(): Promise<void> {
|
|
9737
|
-
if (!this.#activeRetryFallback) return;
|
|
9738
|
-
if (this.#getRetryFallbackRevertPolicy() !== "cooldown-expiry") return;
|
|
9739
|
-
|
|
9740
|
-
const {
|
|
9741
|
-
originalSelector: originalSelectorRaw,
|
|
9742
|
-
originalThinkingLevel,
|
|
9743
|
-
lastAppliedFallbackThinkingLevel,
|
|
9744
|
-
} = this.#activeRetryFallback;
|
|
9745
|
-
const originalSelector = parseRetryFallbackSelector(originalSelectorRaw);
|
|
9746
|
-
if (!originalSelector) {
|
|
9747
|
-
this.#clearActiveRetryFallback();
|
|
9748
|
-
return;
|
|
9749
|
-
}
|
|
9750
|
-
|
|
9751
|
-
const currentModel = this.model;
|
|
9752
|
-
if (!currentModel) return;
|
|
9753
|
-
const currentSelector = formatRetryFallbackSelector(currentModel, this.thinkingLevel);
|
|
9754
|
-
if (currentSelector === originalSelector.raw) {
|
|
9755
|
-
if (!this.#isRetryFallbackSelectorSuppressed(originalSelector)) {
|
|
9756
|
-
this.#clearActiveRetryFallback();
|
|
9757
|
-
}
|
|
9758
|
-
return;
|
|
9759
|
-
}
|
|
9760
|
-
if (this.#isRetryFallbackSelectorSuppressed(originalSelector)) return;
|
|
9761
|
-
|
|
9762
|
-
const primaryModel = this.#modelRegistry.find(originalSelector.provider, originalSelector.id);
|
|
9763
|
-
if (!primaryModel) return;
|
|
9764
|
-
const apiKey = await this.#modelRegistry.getApiKey(primaryModel, this.sessionId);
|
|
9765
|
-
if (!apiKey) return;
|
|
9766
|
-
|
|
9767
|
-
const currentThinkingLevel = this.thinkingLevel;
|
|
9768
|
-
const thinkingToApply =
|
|
9769
|
-
currentThinkingLevel === lastAppliedFallbackThinkingLevel ? originalThinkingLevel : currentThinkingLevel;
|
|
9770
|
-
this.#setModelWithProviderSessionReset(primaryModel);
|
|
9771
|
-
this.sessionManager.appendModelChange(`${primaryModel.provider}/${primaryModel.id}`, "temporary");
|
|
9772
|
-
this.settings.getStorage()?.recordModelUsage(`${primaryModel.provider}/${primaryModel.id}`);
|
|
9773
|
-
this.setThinkingLevel(thinkingToApply);
|
|
9774
|
-
this.#clearActiveRetryFallback();
|
|
9775
|
-
}
|
|
9776
|
-
|
|
9777
11019
|
#parseRetryAfterMsFromError(errorMessage: string): number | undefined {
|
|
9778
11020
|
const now = Date.now();
|
|
9779
11021
|
const retryAfterMsMatch = /retry-after-ms\s*[:=]\s*(\d+)/i.exec(errorMessage);
|
|
@@ -9821,149 +11063,394 @@ export class AgentSession {
|
|
|
9821
11063
|
}
|
|
9822
11064
|
}
|
|
9823
11065
|
|
|
9824
|
-
//
|
|
11066
|
+
// No provider retry hint was available.
|
|
9825
11067
|
return undefined;
|
|
9826
11068
|
}
|
|
9827
11069
|
|
|
11070
|
+
#managedFallbackPromptOptions(): {
|
|
11071
|
+
fallbackManaged?: boolean;
|
|
11072
|
+
nextFallbackAttempt?: (model: Model) => ReturnType<typeof beginAttempt>;
|
|
11073
|
+
onManagedAttemptAccepted?: () => void;
|
|
11074
|
+
onManagedAttemptOutcome?: (
|
|
11075
|
+
outcome: ManagedAttemptOutcome,
|
|
11076
|
+
) => ManagedAttemptDecision | Promise<ManagedAttemptDecision>;
|
|
11077
|
+
} {
|
|
11078
|
+
const controller = this.#defaultFallbackChain();
|
|
11079
|
+
if (controller.chain.entries.length < 2) return {};
|
|
11080
|
+
return {
|
|
11081
|
+
fallbackManaged: true,
|
|
11082
|
+
nextFallbackAttempt: model => {
|
|
11083
|
+
controller.onAttemptStarted();
|
|
11084
|
+
return beginAttempt(formatModelString(model), String(++this.#fallbackInvocationId));
|
|
11085
|
+
},
|
|
11086
|
+
onManagedAttemptAccepted: () => controller.resetAttemptBudget(),
|
|
11087
|
+
onManagedAttemptOutcome: outcome => this.#handleManagedAttemptOutcome(outcome),
|
|
11088
|
+
};
|
|
11089
|
+
}
|
|
11090
|
+
|
|
11091
|
+
async #resetDefaultFallbackForNewTurn(): Promise<void> {
|
|
11092
|
+
if (!this.#defaultFallbackExhaustedLastTurn) return;
|
|
11093
|
+
const controller = this.#defaultFallbackChain();
|
|
11094
|
+
this.#defaultFallbackExhaustedLastTurn = false;
|
|
11095
|
+
controller.resetForNewTurn();
|
|
11096
|
+
if (controller.chain.entries.length > 1) {
|
|
11097
|
+
await this.#advanceDefaultFallback(controller, "new_turn", 0);
|
|
11098
|
+
}
|
|
11099
|
+
}
|
|
11100
|
+
|
|
11101
|
+
async #ensureDefaultFallbackResolution(): Promise<void> {
|
|
11102
|
+
const controller = this.#defaultFallbackChain();
|
|
11103
|
+
if (controller.chain.entries.length < 2) return;
|
|
11104
|
+
const resolutionStart = controller.activeIndex;
|
|
11105
|
+
const resolution = await resolveModelChainWithAuth(
|
|
11106
|
+
controller.chain.entries.slice(resolutionStart),
|
|
11107
|
+
this.#modelRegistry,
|
|
11108
|
+
this.settings,
|
|
11109
|
+
this.sessionId,
|
|
11110
|
+
{ managedFallback: true },
|
|
11111
|
+
);
|
|
11112
|
+
const activeIndex = resolutionStart + resolution.activeIndex;
|
|
11113
|
+
if (activeIndex > resolutionStart) {
|
|
11114
|
+
this.seedDefaultFallbackResolution(activeIndex, [...controller.skips, ...resolution.skips]);
|
|
11115
|
+
} else {
|
|
11116
|
+
controller.seedResolution(activeIndex, [...controller.skips, ...resolution.skips]);
|
|
11117
|
+
}
|
|
11118
|
+
if (!resolution.model) throw new Error(this.#fallbackExhaustionError(controller));
|
|
11119
|
+
this.#setModelAuthoritatively(resolution.model, "restore");
|
|
11120
|
+
this.setThinkingLevel(resolution.explicitThinkingLevel ? resolution.thinkingLevel : this.thinkingLevel);
|
|
11121
|
+
}
|
|
11122
|
+
|
|
9828
11123
|
/**
|
|
9829
|
-
*
|
|
9830
|
-
*
|
|
11124
|
+
* Materialize the default controller from the persisted configured-chain
|
|
11125
|
+
* metadata. Consumers seed only resolution state; role/origin/identity stay
|
|
11126
|
+
* intrinsic to controller construction and are never inferred at runtime.
|
|
9831
11127
|
*/
|
|
9832
|
-
|
|
9833
|
-
const
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
const
|
|
11128
|
+
#defaultFallbackChain(): FallbackChainController {
|
|
11129
|
+
const configuredChain = this.sessionManager.buildSessionContext().configuredModelChains.default;
|
|
11130
|
+
const settingsEntries = normalizeModelSelectorValue(
|
|
11131
|
+
this.settings.getModelRole("default") ?? (this.model ? formatModelString(this.model) : undefined),
|
|
11132
|
+
);
|
|
11133
|
+
const materializeSettingsChain =
|
|
11134
|
+
configuredChain?.origin === "legacy_session" &&
|
|
11135
|
+
configuredChain.entries.length === 1 &&
|
|
11136
|
+
settingsEntries.length > 1;
|
|
11137
|
+
if (materializeSettingsChain) {
|
|
11138
|
+
this.setConfiguredModelChain("default", settingsEntries, "modelRoles");
|
|
11139
|
+
}
|
|
11140
|
+
const chain: ConfiguredFallbackChain = materializeSettingsChain
|
|
11141
|
+
? { role: "default", entries: settingsEntries, origin: "modelRoles", explicitHead: true }
|
|
11142
|
+
: configuredChain
|
|
11143
|
+
? { ...configuredChain, entries: [...configuredChain.entries] }
|
|
11144
|
+
: { role: "default", entries: settingsEntries, origin: "session", explicitHead: true };
|
|
11145
|
+
const existing = this.#defaultFallbackController;
|
|
11146
|
+
if (
|
|
11147
|
+
existing &&
|
|
11148
|
+
(existing.chain.origin === "runtime" || existing.chain.entries.join("\u0000") === chain.entries.join("\u0000"))
|
|
11149
|
+
) {
|
|
11150
|
+
return existing;
|
|
11151
|
+
}
|
|
11152
|
+
this.#defaultFallbackController = new FallbackChainController(chain, this.settings.get("fallback.maxAttempts"));
|
|
11153
|
+
return this.#defaultFallbackController;
|
|
11154
|
+
}
|
|
9838
11155
|
|
|
9839
|
-
|
|
9840
|
-
|
|
11156
|
+
async #handleManagedAttemptOutcome(outcome: ManagedAttemptOutcome): Promise<ManagedAttemptDecision> {
|
|
11157
|
+
if (outcome.type === "run_terminal") {
|
|
11158
|
+
this.#defaultFallbackChain().resetAttemptBudget();
|
|
11159
|
+
return { type: "terminal", terminal: { stopReason: outcome.reason } };
|
|
11160
|
+
}
|
|
11161
|
+
return this.#handleRetryableError(
|
|
11162
|
+
outcome.failure.message,
|
|
11163
|
+
true,
|
|
11164
|
+
outcome.failure.transportFailure,
|
|
11165
|
+
) as Promise<ManagedAttemptDecision>;
|
|
11166
|
+
}
|
|
9841
11167
|
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
9845
|
-
|
|
9846
|
-
|
|
9847
|
-
|
|
11168
|
+
#managedFallbackExhaustionMessage(discarded: AssistantMessage, errorMessage: string): AssistantMessage {
|
|
11169
|
+
return {
|
|
11170
|
+
...discarded,
|
|
11171
|
+
content: [{ type: "text", text: "" }],
|
|
11172
|
+
stopReason: "error",
|
|
11173
|
+
errorMessage,
|
|
11174
|
+
timestamp: Date.now(),
|
|
11175
|
+
};
|
|
11176
|
+
}
|
|
11177
|
+
|
|
11178
|
+
#managedFallbackExhaustionDecision(discarded: AssistantMessage, errorMessage: string): ManagedAttemptDecision {
|
|
11179
|
+
return {
|
|
11180
|
+
type: "terminal",
|
|
11181
|
+
terminal: {
|
|
11182
|
+
stopReason: "exhausted",
|
|
11183
|
+
messages: [this.#managedFallbackExhaustionMessage(discarded, errorMessage)],
|
|
11184
|
+
},
|
|
11185
|
+
};
|
|
11186
|
+
}
|
|
11187
|
+
|
|
11188
|
+
#fallbackTriggerFor(
|
|
11189
|
+
message: AssistantMessage,
|
|
11190
|
+
allowLegacyUsageLimit: boolean,
|
|
11191
|
+
transportFailure?: TransportFailureFacts,
|
|
11192
|
+
): { class: FallbackTriggerClass; retryAfterMs?: number } | undefined {
|
|
11193
|
+
const transport = classifyFallbackTrigger(transportFailure ?? { status: message.errorStatus });
|
|
11194
|
+
if (transport.class !== "other") return transport;
|
|
11195
|
+
const classification = this.#classifyErrorForRetry(message);
|
|
11196
|
+
if (allowLegacyUsageLimit && classification === "usage_limit") {
|
|
11197
|
+
return { class: "quota" };
|
|
11198
|
+
}
|
|
11199
|
+
if (classification === "transient" || classification === "first_event_timeout" || classification === "unknown") {
|
|
11200
|
+
return { class: "server", retryAfterMs: transport.retryAfterMs };
|
|
9848
11201
|
}
|
|
11202
|
+
return undefined;
|
|
11203
|
+
}
|
|
9849
11204
|
|
|
9850
|
-
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
11205
|
+
async #advanceDefaultFallback(
|
|
11206
|
+
controller: FallbackChainController,
|
|
11207
|
+
reason: string,
|
|
11208
|
+
attemptsUsed: number,
|
|
11209
|
+
): Promise<boolean> {
|
|
11210
|
+
while (!controller.isExhausted()) {
|
|
11211
|
+
const selector = controller.currentSelector();
|
|
11212
|
+
if (!selector) return false;
|
|
11213
|
+
const resolved = resolveModelRoleValue(selector, this.#modelRegistry.getAvailable(), {
|
|
11214
|
+
settings: this.settings,
|
|
11215
|
+
matchPreferences: { usageOrder: this.settings.getStorage()?.getModelUsageOrder() },
|
|
11216
|
+
modelRegistry: this.#modelRegistry,
|
|
9857
11217
|
});
|
|
9858
|
-
|
|
9859
|
-
|
|
11218
|
+
if (!resolved.model) {
|
|
11219
|
+
controller.onResolutionSkip("unknown_model");
|
|
11220
|
+
continue;
|
|
11221
|
+
}
|
|
11222
|
+
const managedCursorUnavailable = managedCursorFallbackUnavailableReason(resolved.model, selector);
|
|
11223
|
+
if (managedCursorUnavailable) {
|
|
11224
|
+
controller.onResolutionSkip(managedCursorUnavailable);
|
|
11225
|
+
continue;
|
|
11226
|
+
}
|
|
11227
|
+
const key = await this.#modelRegistry.getApiKey(resolved.model, this.sessionId);
|
|
11228
|
+
if (!key) {
|
|
11229
|
+
controller.onResolutionSkip("unauthenticated");
|
|
11230
|
+
continue;
|
|
11231
|
+
}
|
|
11232
|
+
const from =
|
|
11233
|
+
controller.tried.at(-1)?.selector ?? controller.chain.entries[controller.activeIndex - 1] ?? selector;
|
|
11234
|
+
const to = selector;
|
|
11235
|
+
this.#setModelAuthoritatively(resolved.model, "fallback-switch");
|
|
11236
|
+
this.setThinkingLevel(resolved.explicitThinkingLevel ? resolved.thinkingLevel : this.thinkingLevel);
|
|
11237
|
+
if (from !== to) {
|
|
11238
|
+
this.#emit({
|
|
11239
|
+
type: "model_fallback_switched",
|
|
11240
|
+
eventId: crypto.randomUUID(),
|
|
11241
|
+
from,
|
|
11242
|
+
to,
|
|
11243
|
+
reason,
|
|
11244
|
+
role:
|
|
11245
|
+
controller.chain.origin === "subagent"
|
|
11246
|
+
? (controller.chain.identity ?? controller.chain.role)
|
|
11247
|
+
: controller.chain.role,
|
|
11248
|
+
scope: controller.chain.origin === "subagent" ? "subagent-call" : "session",
|
|
11249
|
+
activeIndex: controller.activeIndex,
|
|
11250
|
+
chainLength: controller.chain.entries.length,
|
|
11251
|
+
attemptsUsed,
|
|
11252
|
+
});
|
|
11253
|
+
}
|
|
11254
|
+
return true;
|
|
11255
|
+
}
|
|
11256
|
+
return false;
|
|
11257
|
+
}
|
|
11258
|
+
|
|
11259
|
+
#emitResolutionFallbackSwitch(controller: FallbackChainController): void {
|
|
11260
|
+
if (controller.activeIndex <= 0) return;
|
|
11261
|
+
const to = controller.currentSelector();
|
|
11262
|
+
const from = controller.chain.entries[controller.activeIndex - 1];
|
|
11263
|
+
if (!from || !to || from === to) return;
|
|
11264
|
+
const event: Extract<AgentSessionEvent, { type: "model_fallback_switched" }> = {
|
|
11265
|
+
type: "model_fallback_switched",
|
|
11266
|
+
eventId: crypto.randomUUID(),
|
|
11267
|
+
from,
|
|
11268
|
+
to,
|
|
11269
|
+
reason: "resolution",
|
|
11270
|
+
role:
|
|
11271
|
+
controller.chain.origin === "subagent"
|
|
11272
|
+
? (controller.chain.identity ?? controller.chain.role)
|
|
11273
|
+
: controller.chain.role,
|
|
11274
|
+
scope: controller.chain.origin === "subagent" ? "subagent-call" : "session",
|
|
11275
|
+
activeIndex: controller.activeIndex,
|
|
11276
|
+
chainLength: controller.chain.entries.length,
|
|
11277
|
+
attemptsUsed: 0,
|
|
11278
|
+
};
|
|
11279
|
+
if (this.#eventListeners.length === 0) {
|
|
11280
|
+
this.#pendingFallbackSwitches.push(event);
|
|
11281
|
+
} else {
|
|
11282
|
+
this.#emit(event);
|
|
11283
|
+
}
|
|
11284
|
+
}
|
|
11285
|
+
|
|
11286
|
+
#fallbackExhaustionError(controller: FallbackChainController): string {
|
|
11287
|
+
const tried = controller.tried.map(failure => `${failure.selector} (${failure.reason})`).join(", ") || "none";
|
|
11288
|
+
const skipped = controller.skips.map(skip => `${skip.selector} (${skip.reason})`).join(", ") || "none";
|
|
11289
|
+
return `Model fallback chain exhausted; models tried: ${tried}; models skipped: ${skipped}`;
|
|
11290
|
+
}
|
|
11291
|
+
|
|
11292
|
+
async #markFailedManagedCredential(trigger: { class: FallbackTriggerClass; retryAfterMs?: number }): Promise<void> {
|
|
11293
|
+
if (!this.model || (trigger.class !== "auth" && trigger.class !== "quota" && trigger.class !== "rate_limit"))
|
|
11294
|
+
return;
|
|
11295
|
+
|
|
11296
|
+
const authStorage = this.#modelRegistry.authStorage;
|
|
11297
|
+
if (trigger.class === "auth") {
|
|
11298
|
+
const apiKey = await this.#modelRegistry.getApiKey(this.model, this.sessionId);
|
|
11299
|
+
if (isAuthenticated(apiKey)) {
|
|
11300
|
+
await authStorage.invalidateCredentialMatching(this.model.provider, apiKey, { sessionId: this.sessionId });
|
|
11301
|
+
}
|
|
11302
|
+
return;
|
|
11303
|
+
}
|
|
11304
|
+
|
|
11305
|
+
await authStorage.markUsageLimitReached(this.model.provider, this.sessionId, {
|
|
11306
|
+
retryAfterMs: trigger.retryAfterMs,
|
|
11307
|
+
});
|
|
11308
|
+
}
|
|
11309
|
+
|
|
11310
|
+
/** Handle retryable errors with exponential backoff. */
|
|
11311
|
+
async #handleRetryableError(
|
|
11312
|
+
message: AssistantMessage,
|
|
11313
|
+
managedOutcome = false,
|
|
11314
|
+
transportFailure?: TransportFailureFacts,
|
|
11315
|
+
): Promise<boolean | ManagedAttemptDecision> {
|
|
11316
|
+
const controller = this.#defaultFallbackChain();
|
|
11317
|
+
const managedFallback = controller.chain.entries.length > 1;
|
|
11318
|
+
const trigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
|
|
11319
|
+
if (!trigger) {
|
|
11320
|
+
return managedOutcome
|
|
11321
|
+
? this.#managedFallbackExhaustionDecision(message, message.errorMessage || "Model fallback attempt failed")
|
|
11322
|
+
: false;
|
|
11323
|
+
}
|
|
11324
|
+
const retrySettings = this.settings.getGroup("retry");
|
|
11325
|
+
const legacyRetryConfigured =
|
|
11326
|
+
this.settings.has("retry.enabled") ||
|
|
11327
|
+
this.settings.has("retry.maxRetries") ||
|
|
11328
|
+
this.settings.has("retry.baseDelayMs") ||
|
|
11329
|
+
this.settings.has("retry.maxDelayMs");
|
|
11330
|
+
// A bare default retry configuration must preserve the historical
|
|
11331
|
+
// fail-closed behavior for generic provider errors. Explicit retry
|
|
11332
|
+
// settings opt into the resilient legacy retry path.
|
|
11333
|
+
if (!managedFallback && (!retrySettings.enabled || !legacyRetryConfigured)) return false;
|
|
11334
|
+
const legacyClassification = managedFallback ? undefined : this.#classifyErrorForRetry(message);
|
|
11335
|
+
const legacyUnbounded = legacyClassification === "transient" || legacyClassification === "unknown";
|
|
11336
|
+
const attemptsUsed = managedFallback ? controller.attemptsUsed || 1 : this.#retryAttempt + 1;
|
|
11337
|
+
const outcome = managedFallback
|
|
11338
|
+
? controller.onAttemptFailure(trigger.class, message.errorMessage || "Unknown error")
|
|
11339
|
+
: !legacyUnbounded && attemptsUsed > retrySettings.maxRetries
|
|
11340
|
+
? "exhausted"
|
|
11341
|
+
: "retry";
|
|
11342
|
+
if (outcome === "exhausted") {
|
|
11343
|
+
if (managedFallback) {
|
|
11344
|
+
const errorMessage = this.#fallbackExhaustionError(controller);
|
|
11345
|
+
this.emitNotice("error", errorMessage, "fallback");
|
|
11346
|
+
this.#defaultFallbackExhaustedLastTurn = true;
|
|
11347
|
+
controller.resetSticky();
|
|
11348
|
+
return managedOutcome ? this.#managedFallbackExhaustionDecision(message, errorMessage) : false;
|
|
11349
|
+
}
|
|
9860
11350
|
return false;
|
|
9861
11351
|
}
|
|
9862
11352
|
|
|
11353
|
+
const generation = this.#promptGeneration;
|
|
9863
11354
|
const errorMessage = message.errorMessage || "Unknown error";
|
|
9864
|
-
const
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
|
|
9868
|
-
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
retryAfterMs,
|
|
9876
|
-
baseUrl: this.model.baseUrl,
|
|
9877
|
-
},
|
|
9878
|
-
);
|
|
9879
|
-
if (switched) {
|
|
9880
|
-
switchedCredential = true;
|
|
9881
|
-
delayMs = 0;
|
|
9882
|
-
} else if (retryAfterMs > delayMs) {
|
|
9883
|
-
// No more accounts to switch to — wait out the backoff
|
|
9884
|
-
delayMs = retryAfterMs;
|
|
9885
|
-
}
|
|
9886
|
-
}
|
|
9887
|
-
|
|
9888
|
-
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
|
9889
|
-
if (!switchedCredential && currentSelector) {
|
|
9890
|
-
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
9891
|
-
switchedModel = await this.#tryRetryModelFallback(currentSelector, { requireNonLocal: localUnavailable });
|
|
9892
|
-
if (switchedModel) {
|
|
9893
|
-
delayMs = 0;
|
|
9894
|
-
} else if (parsedRetryAfterMs && parsedRetryAfterMs > delayMs) {
|
|
9895
|
-
delayMs = parsedRetryAfterMs;
|
|
9896
|
-
}
|
|
9897
|
-
}
|
|
9898
|
-
|
|
9899
|
-
// Fail-fast cap: if the provider asks us to wait longer than
|
|
9900
|
-
// retry.maxDelayMs and we have no fallback credential or model to
|
|
9901
|
-
// switch to, surface the error instead of sleeping. Defends against
|
|
9902
|
-
// 3-hour Anthropic rate-limit windows that would otherwise leave a
|
|
9903
|
-
// subagent (or interactive session) silently hung. The original
|
|
9904
|
-
// assistant error message is preserved in agent state so the caller
|
|
9905
|
-
// can act on it.
|
|
9906
|
-
const maxDelayMs = retrySettings.maxDelayMs;
|
|
9907
|
-
if (unboundedClass && !switchedCredential && !switchedModel) {
|
|
9908
|
-
// Retry forever: honor a provider-supplied wait, otherwise cap the
|
|
9909
|
-
// exponential backoff at the ceiling instead of giving up.
|
|
9910
|
-
if (parsedRetryAfterMs !== undefined) {
|
|
9911
|
-
delayMs = Math.max(delayMs, parsedRetryAfterMs);
|
|
9912
|
-
} else if (maxDelayMs > 0) {
|
|
9913
|
-
delayMs = Math.min(delayMs, maxDelayMs);
|
|
9914
|
-
}
|
|
9915
|
-
}
|
|
9916
|
-
if (!unboundedClass && maxDelayMs > 0 && delayMs > maxDelayMs && !switchedCredential && !switchedModel) {
|
|
9917
|
-
const attempt = this.#retryAttempt;
|
|
11355
|
+
const retryAfterMs =
|
|
11356
|
+
trigger.retryAfterMs ?? (managedFallback ? undefined : this.#parseRetryAfterMsFromError(errorMessage));
|
|
11357
|
+
const delayMs =
|
|
11358
|
+
outcome === "advance"
|
|
11359
|
+
? 0
|
|
11360
|
+
: managedFallback
|
|
11361
|
+
? effectiveFallbackDelay(retrySettings.baseDelayMs, retrySettings.maxDelayMs, attemptsUsed, retryAfterMs)
|
|
11362
|
+
: retryAfterMs !== undefined
|
|
11363
|
+
? Math.max(retrySettings.baseDelayMs * 2 ** Math.max(0, attemptsUsed - 1), retryAfterMs)
|
|
11364
|
+
: retrySettings.baseDelayMs * 2 ** Math.max(0, attemptsUsed - 1);
|
|
11365
|
+
if (!managedFallback && !legacyUnbounded && retrySettings.maxDelayMs > 0 && delayMs > retrySettings.maxDelayMs) {
|
|
9918
11366
|
this.#retryAttempt = 0;
|
|
9919
11367
|
await this.#emitSessionEvent({
|
|
9920
11368
|
type: "auto_retry_end",
|
|
9921
11369
|
success: false,
|
|
9922
|
-
attempt,
|
|
9923
|
-
finalError: `Provider requested ${delayMs}ms wait, exceeds retry.maxDelayMs (${maxDelayMs}ms). Original error: ${errorMessage}`,
|
|
11370
|
+
attempt: attemptsUsed,
|
|
11371
|
+
finalError: `Provider requested ${delayMs}ms wait, exceeds retry.maxDelayMs (${retrySettings.maxDelayMs}ms). Original error: ${errorMessage}`,
|
|
9924
11372
|
});
|
|
9925
11373
|
this.#resolveRetry();
|
|
9926
11374
|
return false;
|
|
9927
11375
|
}
|
|
9928
11376
|
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
11377
|
+
const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
|
|
11378
|
+
if (managedFallback) await this.#markFailedManagedCredential(trigger);
|
|
11379
|
+
let advanced = outcome !== "advance";
|
|
11380
|
+
let resolutionError: unknown;
|
|
11381
|
+
if (outcome === "advance") {
|
|
11382
|
+
try {
|
|
11383
|
+
advanced = await this.#advanceDefaultFallback(controller, trigger.class, attemptsUsed);
|
|
11384
|
+
} catch (error) {
|
|
11385
|
+
resolutionError = error;
|
|
11386
|
+
}
|
|
11387
|
+
}
|
|
11388
|
+
if (!advanced) {
|
|
11389
|
+
const errorMessage = resolutionError
|
|
11390
|
+
? `${this.#fallbackExhaustionError(controller)}; resolution failed: ${resolutionError instanceof Error ? resolutionError.message : String(resolutionError)}`
|
|
11391
|
+
: this.#fallbackExhaustionError(controller);
|
|
11392
|
+
this.emitNotice("error", errorMessage, "fallback");
|
|
11393
|
+
if (managedOutcome && ownership) {
|
|
11394
|
+
this.agent.requestRunTerminal(ownership.logicalRunId, {
|
|
11395
|
+
stopReason: "exhausted",
|
|
11396
|
+
messages: [this.#managedFallbackExhaustionMessage(message, errorMessage)],
|
|
11397
|
+
});
|
|
11398
|
+
}
|
|
11399
|
+
this.#defaultFallbackExhaustedLastTurn = true;
|
|
11400
|
+
controller.resetSticky();
|
|
11401
|
+
this.#retryAttempt = 0;
|
|
11402
|
+
this.#resolveRetry();
|
|
11403
|
+
return;
|
|
11404
|
+
}
|
|
9937
11405
|
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
unbounded: unboundedClass,
|
|
9945
|
-
});
|
|
11406
|
+
this.#retryAttempt = attemptsUsed;
|
|
11407
|
+
if (!this.#retryPromise) {
|
|
11408
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
11409
|
+
this.#retryPromise = promise;
|
|
11410
|
+
this.#retryResolve = resolve;
|
|
11411
|
+
}
|
|
9946
11412
|
|
|
9947
|
-
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
this
|
|
9951
|
-
|
|
11413
|
+
const retryAbortController = new AbortController();
|
|
11414
|
+
this.#retryAbortController?.abort();
|
|
11415
|
+
this.#retryAbortController = retryAbortController;
|
|
11416
|
+
this.#retryNowRequested = false;
|
|
11417
|
+
await this.#emitSessionEvent({
|
|
11418
|
+
type: "auto_retry_start",
|
|
11419
|
+
attempt: this.#retryAttempt,
|
|
11420
|
+
maxAttempts: managedFallback ? controller.maxAttempts : retrySettings.maxRetries,
|
|
11421
|
+
delayMs,
|
|
11422
|
+
errorMessage,
|
|
11423
|
+
unbounded: !managedFallback && legacyUnbounded,
|
|
11424
|
+
});
|
|
9952
11425
|
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
} catch {
|
|
9957
|
-
if (this.#retryAbortController !== retryAbortController) {
|
|
9958
|
-
return false;
|
|
11426
|
+
const messages = this.agent.state.messages;
|
|
11427
|
+
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
11428
|
+
this.agent.replaceMessages(messages.slice(0, -1));
|
|
9959
11429
|
}
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
this.#
|
|
9965
|
-
|
|
9966
|
-
|
|
11430
|
+
|
|
11431
|
+
try {
|
|
11432
|
+
await scheduler.wait(delayMs, { signal: retryAbortController.signal });
|
|
11433
|
+
} catch {
|
|
11434
|
+
if (this.#retryAbortController !== retryAbortController) return;
|
|
11435
|
+
this.#retryAbortController = undefined;
|
|
11436
|
+
if (this.#retryNowRequested) {
|
|
11437
|
+
// Fall through below so the retry continues immediately.
|
|
11438
|
+
} else {
|
|
11439
|
+
const attempt = this.#retryAttempt;
|
|
11440
|
+
this.#retryAttempt = 0;
|
|
11441
|
+
await this.#emitSessionEvent({
|
|
11442
|
+
type: "auto_retry_end",
|
|
11443
|
+
success: false,
|
|
11444
|
+
attempt,
|
|
11445
|
+
finalError: "Retry cancelled",
|
|
11446
|
+
});
|
|
11447
|
+
this.#resolveRetry();
|
|
11448
|
+
return;
|
|
11449
|
+
}
|
|
11450
|
+
}
|
|
11451
|
+
if (retryAbortController.signal.aborted && !this.#retryNowRequested) {
|
|
11452
|
+
if (this.#retryAbortController !== retryAbortController) return;
|
|
11453
|
+
this.#retryAbortController = undefined;
|
|
9967
11454
|
const attempt = this.#retryAttempt;
|
|
9968
11455
|
this.#retryAttempt = 0;
|
|
9969
11456
|
await this.#emitSessionEvent({
|
|
@@ -9973,28 +11460,55 @@ export class AgentSession {
|
|
|
9973
11460
|
finalError: "Retry cancelled",
|
|
9974
11461
|
});
|
|
9975
11462
|
this.#resolveRetry();
|
|
9976
|
-
return
|
|
11463
|
+
return;
|
|
9977
11464
|
}
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
this.#retryAbortController = undefined;
|
|
9981
|
-
}
|
|
11465
|
+
if (this.#retryAbortController === retryAbortController) this.#retryAbortController = undefined;
|
|
11466
|
+
this.#retryNowRequested = false;
|
|
9982
11467
|
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
11468
|
+
if (managedOutcome) {
|
|
11469
|
+
try {
|
|
11470
|
+
await this.#checkEstimatedContextBeforePrompt();
|
|
11471
|
+
if (!ownership?.isCurrent()) {
|
|
11472
|
+
const attempt = this.#retryAttempt;
|
|
11473
|
+
this.#retryAttempt = 0;
|
|
11474
|
+
await this.#emitSessionEvent({
|
|
11475
|
+
type: "auto_retry_end",
|
|
11476
|
+
success: false,
|
|
11477
|
+
attempt,
|
|
11478
|
+
finalError: "Retry continuation was superseded",
|
|
11479
|
+
});
|
|
11480
|
+
this.#resolveRetry();
|
|
11481
|
+
return;
|
|
11482
|
+
}
|
|
11483
|
+
await this.agent.continue(this.#managedFallbackPromptOptions());
|
|
11484
|
+
return;
|
|
11485
|
+
} catch (error) {
|
|
11486
|
+
const attempt = this.#retryAttempt;
|
|
11487
|
+
this.#retryAttempt = 0;
|
|
11488
|
+
try {
|
|
11489
|
+
await this.#emitSessionEvent({
|
|
11490
|
+
type: "auto_retry_end",
|
|
11491
|
+
success: false,
|
|
11492
|
+
attempt,
|
|
11493
|
+
finalError: error instanceof Error ? error.message : String(error),
|
|
11494
|
+
});
|
|
11495
|
+
} finally {
|
|
11496
|
+
this.#resolveRetry();
|
|
11497
|
+
}
|
|
11498
|
+
throw error;
|
|
11499
|
+
}
|
|
11500
|
+
}
|
|
11501
|
+
|
|
11502
|
+
this.#scheduleAgentContinue({
|
|
11503
|
+
delayMs: 1,
|
|
11504
|
+
generation,
|
|
11505
|
+
onError: () => this.#failRetryRecovery("Retry continuation failed to start"),
|
|
11506
|
+
onSkip: () => this.#failRetryRecovery("Retry continuation was superseded"),
|
|
11507
|
+
});
|
|
11508
|
+
};
|
|
9997
11509
|
|
|
11510
|
+
if (managedOutcome) return { type: "retry", continuation: retry };
|
|
11511
|
+
await retry();
|
|
9998
11512
|
return true;
|
|
9999
11513
|
}
|
|
10000
11514
|
|
|
@@ -10040,12 +11554,26 @@ export class AgentSession {
|
|
|
10040
11554
|
this.#resolveRetry();
|
|
10041
11555
|
}
|
|
10042
11556
|
|
|
10043
|
-
async #promptAgentWithIdleRetry(
|
|
11557
|
+
async #promptAgentWithIdleRetry(
|
|
11558
|
+
messages: AgentMessage[],
|
|
11559
|
+
options?: { toolChoice?: ToolChoice; fallbackManaged?: boolean; onRunAccepted?: () => void },
|
|
11560
|
+
predecessorAgentEndHold?: symbol,
|
|
11561
|
+
): Promise<void> {
|
|
10044
11562
|
const deadline = Date.now() + 30_000;
|
|
11563
|
+
let continuationHold = predecessorAgentEndHold;
|
|
10045
11564
|
for (;;) {
|
|
10046
11565
|
try {
|
|
10047
|
-
|
|
10048
|
-
|
|
11566
|
+
const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(
|
|
11567
|
+
continuationHold ?? this.#reserveDeferredAgentEndForContinuation(),
|
|
11568
|
+
);
|
|
11569
|
+
continuationHold = undefined;
|
|
11570
|
+
try {
|
|
11571
|
+
await this.agent.prompt(messages, options);
|
|
11572
|
+
return;
|
|
11573
|
+
} catch (error) {
|
|
11574
|
+
this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
|
|
11575
|
+
throw error;
|
|
11576
|
+
}
|
|
10049
11577
|
} catch (err) {
|
|
10050
11578
|
if (!(err instanceof AgentBusyError)) {
|
|
10051
11579
|
throw err;
|
|
@@ -11003,6 +12531,10 @@ export class AgentSession {
|
|
|
11003
12531
|
this.#followUpMessages = [];
|
|
11004
12532
|
this.#pendingNextTurnMessages = [];
|
|
11005
12533
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
12534
|
+
const suspendedWorkflowGateEmitter = switchingToDifferentSession
|
|
12535
|
+
? this.#suspendWorkflowGateEmitter(previousSessionState.sessionId)
|
|
12536
|
+
: undefined;
|
|
12537
|
+
let unavailableDefaultChainMessage: string | undefined;
|
|
11006
12538
|
|
|
11007
12539
|
try {
|
|
11008
12540
|
await this.sessionManager.setSessionFile(sessionPath);
|
|
@@ -11016,53 +12548,41 @@ export class AgentSession {
|
|
|
11016
12548
|
const fallbackSelectedMCPToolNames = this.#getSessionDefaultSelectedMCPToolNames(sessionPath);
|
|
11017
12549
|
await this.#restoreMCPSelectionsForSessionContext(sessionContext, { fallbackSelectedMCPToolNames });
|
|
11018
12550
|
|
|
11019
|
-
// The target session is loaded and MCP selections are restored:
|
|
11020
|
-
// switch
|
|
11021
|
-
// Clear before session_switch hooks, so messages enqueued by hooks belong
|
|
11022
|
-
// to the new session and remain deliverable.
|
|
12551
|
+
// The target session is loaded and MCP selections are restored: discard
|
|
12552
|
+
// pre-switch delivery queues before completing the restored agent state.
|
|
11023
12553
|
this.agent.clearAllQueues();
|
|
11024
12554
|
|
|
11025
|
-
// Emit session_switch event to hooks
|
|
11026
|
-
if (this.#extensionRunner) {
|
|
11027
|
-
await this.#extensionRunner.emit({
|
|
11028
|
-
type: "session_switch",
|
|
11029
|
-
reason: "resume",
|
|
11030
|
-
previousSessionFile,
|
|
11031
|
-
});
|
|
11032
|
-
}
|
|
11033
|
-
|
|
11034
12555
|
this.agent.replaceMessages(sessionContext.messages);
|
|
11035
12556
|
this.#resetInjectedContextSignatures();
|
|
11036
12557
|
this.#syncTodoPhasesFromBranch();
|
|
11037
|
-
if (switchingToDifferentSession) {
|
|
11038
|
-
this.#closeAllProviderSessions("session switch");
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
|
|
11046
|
-
|
|
11047
|
-
|
|
11048
|
-
|
|
11049
|
-
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
|
|
11055
|
-
|
|
11056
|
-
|
|
11057
|
-
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
}
|
|
12558
|
+
if (switchingToDifferentSession || didReloadConversationChange) {
|
|
12559
|
+
this.#closeAllProviderSessions(switchingToDifferentSession ? "session switch" : "session reload");
|
|
12560
|
+
this.#rebindProviderSessionState(new Map());
|
|
12561
|
+
}
|
|
12562
|
+
|
|
12563
|
+
const configuredDefaultChain = sessionContext.configuredModelChains.default?.entries;
|
|
12564
|
+
const defaultEntries =
|
|
12565
|
+
configuredDefaultChain ?? (sessionContext.models.default ? [sessionContext.models.default] : []);
|
|
12566
|
+
this.#defaultFallbackController = undefined;
|
|
12567
|
+
if (defaultEntries.length > 0) {
|
|
12568
|
+
const resolution = await resolveModelChainWithAuth(
|
|
12569
|
+
defaultEntries,
|
|
12570
|
+
this.#modelRegistry,
|
|
12571
|
+
this.settings,
|
|
12572
|
+
this.sessionId,
|
|
12573
|
+
{ managedFallback: true },
|
|
12574
|
+
);
|
|
12575
|
+
const controller = this.#defaultFallbackChain();
|
|
12576
|
+
this.seedDefaultFallbackResolution(resolution.activeIndex, resolution.skips);
|
|
12577
|
+
if (!resolution.model) {
|
|
12578
|
+
unavailableDefaultChainMessage = this.#fallbackExhaustionError(controller);
|
|
12579
|
+
throw new Error(unavailableDefaultChainMessage);
|
|
12580
|
+
}
|
|
12581
|
+
if (!this.model || !modelsAreEqual(this.model, resolution.model)) {
|
|
12582
|
+
this.#setModelAuthoritatively(resolution.model, "restore");
|
|
12583
|
+
}
|
|
12584
|
+
if (resolution.explicitThinkingLevel && resolution.thinkingLevel !== undefined) {
|
|
12585
|
+
this.setThinkingLevel(resolution.thinkingLevel);
|
|
11066
12586
|
}
|
|
11067
12587
|
}
|
|
11068
12588
|
|
|
@@ -11072,9 +12592,14 @@ export class AgentSession {
|
|
|
11072
12592
|
.some(entry => entry.type === "service_tier_change");
|
|
11073
12593
|
const defaultThinkingLevel = this.settings.get("defaultThinkingLevel");
|
|
11074
12594
|
const configuredServiceTier = this.settings.get("serviceTier");
|
|
12595
|
+
const persistedThinkingLevel = hasThinkingEntry
|
|
12596
|
+
? (sessionContext.thinkingLevel as ThinkingLevel | undefined)
|
|
12597
|
+
: defaultThinkingLevel;
|
|
11075
12598
|
const nextThinkingLevel = resolveThinkingLevelForModel(
|
|
11076
12599
|
this.model,
|
|
11077
|
-
|
|
12600
|
+
persistedThinkingLevel === ThinkingLevel.Inherit
|
|
12601
|
+
? this.#getInheritedThinkingLevel()
|
|
12602
|
+
: persistedThinkingLevel,
|
|
11078
12603
|
);
|
|
11079
12604
|
this.#thinkingLevel = nextThinkingLevel;
|
|
11080
12605
|
this.agent.setThinkingLevel(toReasoningEffort(nextThinkingLevel));
|
|
@@ -11083,6 +12608,9 @@ export class AgentSession {
|
|
|
11083
12608
|
: configuredServiceTier === "none"
|
|
11084
12609
|
? undefined
|
|
11085
12610
|
: configuredServiceTier;
|
|
12611
|
+
// Establish the successor's durable session identity only after every
|
|
12612
|
+
// restored state facet is live. Identity-bound extension hooks run below.
|
|
12613
|
+
await this.sessionManager.ensureOnDisk();
|
|
11086
12614
|
|
|
11087
12615
|
if (switchingToDifferentSession) {
|
|
11088
12616
|
this.#resetHindsightConversationTrackingIfHindsight();
|
|
@@ -11090,10 +12618,24 @@ export class AgentSession {
|
|
|
11090
12618
|
}
|
|
11091
12619
|
|
|
11092
12620
|
this.#reconnectToAgent();
|
|
12621
|
+
// session_switch is the post-commit identity signal. SDK authority and
|
|
12622
|
+
// other identity-bound integrations must not observe the successor until
|
|
12623
|
+
// messages, model state, MCP selections, and the agent subscription are live.
|
|
12624
|
+
if (this.#extensionRunner) {
|
|
12625
|
+
await this.#extensionRunner.emit({
|
|
12626
|
+
type: "session_switch",
|
|
12627
|
+
reason: "resume",
|
|
12628
|
+
previousSessionFile,
|
|
12629
|
+
});
|
|
12630
|
+
}
|
|
12631
|
+
if (suspendedWorkflowGateEmitter)
|
|
12632
|
+
this.#bindWorkflowGateEmitter(previousSessionState.sessionId, suspendedWorkflowGateEmitter);
|
|
11093
12633
|
return true;
|
|
11094
12634
|
} catch (error) {
|
|
11095
12635
|
this.sessionManager.restoreState(previousSessionState);
|
|
12636
|
+
this.#defaultFallbackController = undefined;
|
|
11096
12637
|
this.#syncAgentSessionId(previousSessionState.sessionId);
|
|
12638
|
+
this.#restoreWorkflowGateEmitter(suspendedWorkflowGateEmitter);
|
|
11097
12639
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
11098
12640
|
let restoreMcpError: unknown;
|
|
11099
12641
|
try {
|
|
@@ -11133,6 +12675,10 @@ export class AgentSession {
|
|
|
11133
12675
|
if (restoreMcpError) {
|
|
11134
12676
|
throw restoreMcpError;
|
|
11135
12677
|
}
|
|
12678
|
+
if (unavailableDefaultChainMessage) {
|
|
12679
|
+
this.emitNotice("error", `Could not restore session model: ${unavailableDefaultChainMessage}`, "fallback");
|
|
12680
|
+
return false;
|
|
12681
|
+
}
|
|
11136
12682
|
throw error;
|
|
11137
12683
|
}
|
|
11138
12684
|
}
|
|
@@ -11151,6 +12697,7 @@ export class AgentSession {
|
|
|
11151
12697
|
cancelled: boolean;
|
|
11152
12698
|
}> {
|
|
11153
12699
|
const previousSessionFile = this.sessionFile;
|
|
12700
|
+
const previousWorkflowGateSessionId = this.sessionId;
|
|
11154
12701
|
const selectedEntry = this.sessionManager.getEntryForFidelity(entryId);
|
|
11155
12702
|
|
|
11156
12703
|
if (selectedEntry?.type !== "message" || selectedEntry.message.role !== "user") {
|
|
@@ -11189,22 +12736,17 @@ export class AgentSession {
|
|
|
11189
12736
|
}
|
|
11190
12737
|
this.#syncTodoPhasesFromBranch();
|
|
11191
12738
|
this.#syncAgentSessionId();
|
|
12739
|
+
this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
|
|
11192
12740
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
11193
12741
|
this.#resetHindsightConversationTrackingIfHindsight();
|
|
12742
|
+
this.#closeAllProviderSessions("session branch");
|
|
12743
|
+
this.#rebindProviderSessionState(new Map());
|
|
11194
12744
|
|
|
11195
12745
|
// Reload messages from entries (works for both file and in-memory mode)
|
|
11196
12746
|
const sessionContext = this.buildDisplaySessionContext();
|
|
11197
12747
|
|
|
11198
12748
|
await this.#restoreMCPSelectionsForSessionContext(sessionContext);
|
|
11199
12749
|
|
|
11200
|
-
// Emit session_branch event to hooks (after branch completes)
|
|
11201
|
-
if (this.#extensionRunner) {
|
|
11202
|
-
await this.#extensionRunner.emit({
|
|
11203
|
-
type: "session_branch",
|
|
11204
|
-
previousSessionFile,
|
|
11205
|
-
});
|
|
11206
|
-
}
|
|
11207
|
-
|
|
11208
12750
|
if (!skipConversationRestore) {
|
|
11209
12751
|
this.agent.replaceMessages(sessionContext.messages);
|
|
11210
12752
|
this.#resetInjectedContextSignatures();
|
|
@@ -11212,6 +12754,14 @@ export class AgentSession {
|
|
|
11212
12754
|
}
|
|
11213
12755
|
|
|
11214
12756
|
this.#resetIrcRosterDeliveryState();
|
|
12757
|
+
// session_branch is the post-commit identity signal. Publish it only after
|
|
12758
|
+
// the successor's messages and MCP selections are restored.
|
|
12759
|
+
if (this.#extensionRunner) {
|
|
12760
|
+
await this.#extensionRunner.emit({
|
|
12761
|
+
type: "session_branch",
|
|
12762
|
+
previousSessionFile,
|
|
12763
|
+
});
|
|
12764
|
+
}
|
|
11215
12765
|
|
|
11216
12766
|
return { selectedText, cancelled: false };
|
|
11217
12767
|
}
|
|
@@ -11669,6 +13219,15 @@ export class AgentSession {
|
|
|
11669
13219
|
});
|
|
11670
13220
|
}
|
|
11671
13221
|
|
|
13222
|
+
async fetchUsageReportsForControl(): Promise<UsageReport[] | null> {
|
|
13223
|
+
const authStorage = this.#modelRegistry.authStorage;
|
|
13224
|
+
if (!authStorage.fetchUsageReports) return null;
|
|
13225
|
+
return authStorage.fetchUsageReports({
|
|
13226
|
+
baseUrlResolver: provider => this.#modelRegistry.getProviderBaseUrl?.(provider),
|
|
13227
|
+
logDetails: false,
|
|
13228
|
+
});
|
|
13229
|
+
}
|
|
13230
|
+
|
|
11672
13231
|
/**
|
|
11673
13232
|
* Estimate context tokens from messages, using the last assistant usage when available.
|
|
11674
13233
|
*/
|
|
@@ -11735,9 +13294,9 @@ export class AgentSession {
|
|
|
11735
13294
|
|
|
11736
13295
|
/**
|
|
11737
13296
|
* Observed heuristic→actual token correction for the compaction keep window
|
|
11738
|
-
* (Finding 7). Compares the provider's real prompt tokens against the
|
|
11739
|
-
* heuristic estimate of the same content (stable
|
|
11740
|
-
* before the last usage-bearing assistant turn — that turn's own output is
|
|
13297
|
+
* (Finding 7). Compares the provider's real prompt tokens against the
|
|
13298
|
+
* script-aware display-token heuristic estimate of the same content (stable
|
|
13299
|
+
* system prefix + history before the last usage-bearing assistant turn — that turn's own output is
|
|
11741
13300
|
* the response, not part of the request's prompt, so it belongs on neither
|
|
11742
13301
|
* side of the ratio). Image-bearing content is bucketed out
|
|
11743
13302
|
* of BOTH sides using the identical fixed IMAGE_TOKEN_ESTIMATE so the 1200-token
|