@gajae-code/coding-agent 0.13.2 → 0.14.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 +215 -8
- package/README.md +2 -0
- package/bin/gajaessi.js +4 -0
- package/dist/types/async/job-manager.d.ts +55 -2
- package/dist/types/async/support.d.ts +14 -0
- package/dist/types/capability/mcp.d.ts +6 -0
- package/dist/types/capability/types.d.ts +6 -0
- package/dist/types/cli/accounts-cli.d.ts +19 -0
- package/dist/types/cli/args.d.ts +3 -0
- package/dist/types/cli/config-cli.d.ts +6 -0
- package/dist/types/cli/crash-cli.d.ts +10 -0
- package/dist/types/cli/customize-doctor.d.ts +145 -0
- package/dist/types/cli/daemon-cli.d.ts +3 -1
- package/dist/types/cli/mcp-cli.d.ts +11 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/root-flags.d.ts +12 -0
- package/dist/types/cli/setup-cli.d.ts +3 -1
- package/dist/types/cli/skills-cli.d.ts +2 -1
- package/dist/types/cli/update-cli.d.ts +19 -1
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/commands/accounts.d.ts +40 -0
- package/dist/types/commands/crash.d.ts +24 -0
- package/dist/types/commands/customize.d.ts +22 -0
- package/dist/types/commands/daemon.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/commands/skills.d.ts +6 -1
- package/dist/types/commands/update.d.ts +7 -0
- package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
- package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
- package/dist/types/config/config-schema-version.d.ts +10 -0
- package/dist/types/config/file-lock.d.ts +18 -0
- package/dist/types/config/model-profile-activation.d.ts +3 -1
- package/dist/types/config/model-registry.d.ts +10 -1
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +73 -9
- package/dist/types/config/settings.d.ts +63 -0
- package/dist/types/config/skill-settings-defaults.d.ts +26 -0
- package/dist/types/config/task-delegation.d.ts +39 -0
- package/dist/types/config/update-channel.d.ts +10 -0
- package/dist/types/coordinator-mcp/server.d.ts +5 -1
- package/dist/types/crash/index-store.d.ts +87 -0
- package/dist/types/crash/nudge.d.ts +50 -0
- package/dist/types/crash/record-loader.d.ts +17 -0
- package/dist/types/crash/report.d.ts +134 -0
- package/dist/types/crash/sanitize.d.ts +25 -0
- package/dist/types/cursor.d.ts +13 -2
- package/dist/types/customization/import.d.ts +37 -0
- package/dist/types/customization/index.d.ts +4 -0
- package/dist/types/customization/inventory.d.ts +25 -0
- package/dist/types/customization/mutations.d.ts +32 -0
- package/dist/types/customization/types.d.ts +115 -0
- package/dist/types/daemon/control-types.d.ts +8 -1
- package/dist/types/daemon/operator-contract.d.ts +4 -4
- package/dist/types/discovery/claude.d.ts +39 -1
- package/dist/types/discovery/codex.d.ts +39 -1
- package/dist/types/discovery/index.d.ts +2 -0
- package/dist/types/discovery/mcp-compat.d.ts +30 -0
- package/dist/types/edit/renderer.d.ts +23 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +21 -4
- package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
- package/dist/types/extensibility/hooks/loader.d.ts +19 -5
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
- package/dist/types/extensibility/skill-management.d.ts +107 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
- package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
- package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
- package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
- package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
- package/dist/types/hooks/events.d.ts +60 -0
- package/dist/types/hooks/normalize.d.ts +83 -0
- package/dist/types/hooks/skill-state.d.ts +9 -2
- package/dist/types/main.d.ts +2 -1
- package/dist/types/modes/acp/abort-scope.d.ts +13 -0
- package/dist/types/modes/acp/acp-agent.d.ts +7 -4
- package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
- package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
- package/dist/types/modes/components/customization/index.d.ts +1 -0
- package/dist/types/modes/components/dynamic-theme-text.d.ts +9 -0
- package/dist/types/modes/components/gajae-pet-widget.d.ts +6 -28
- package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
- package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
- package/dist/types/modes/components/oauth-selector.d.ts +10 -4
- package/dist/types/modes/components/pet-capability.d.ts +7 -21
- package/dist/types/modes/components/settings-selector.d.ts +5 -0
- package/dist/types/modes/components/status-line/gh.d.ts +6 -2
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
- package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/theme/defaults/index.d.ts +118 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
- package/dist/types/runtime-mcp/config.d.ts +9 -0
- package/dist/types/runtime-mcp/manager.d.ts +26 -1
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
- package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
- package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
- package/dist/types/runtime-mcp/protocol.d.ts +278 -0
- package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
- package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
- package/dist/types/runtime-mcp/types.d.ts +102 -10
- package/dist/types/sdk/acp/adapter.d.ts +26 -5
- package/dist/types/sdk/broker/broker.d.ts +3 -55
- package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
- package/dist/types/sdk/broker/session-index.d.ts +14 -5
- package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
- package/dist/types/sdk/bus/config.d.ts +11 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
- package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
- package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
- package/dist/types/sdk/bus/index.d.ts +85 -99
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
- package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
- package/dist/types/sdk/bus/notification-service.d.ts +52 -0
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
- package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
- package/dist/types/sdk/bus/session-control.d.ts +25 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
- package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
- package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
- package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
- package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
- package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
- package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
- package/dist/types/sdk/cli/rows.d.ts +2 -11
- package/dist/types/sdk/cli/session-cli.d.ts +15 -25
- package/dist/types/sdk/client/client.d.ts +97 -1
- package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +18 -5
- package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
- package/dist/types/sdk/host/host.d.ts +15 -1
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/host/session-runtime.d.ts +127 -7
- package/dist/types/sdk/host/types.d.ts +1 -1
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +2 -1
- package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
- package/dist/types/sdk/lifecycle/client.d.ts +59 -0
- package/dist/types/sdk/lifecycle/index.d.ts +3 -0
- package/dist/types/sdk/lifecycle/service.d.ts +232 -0
- package/dist/types/sdk/mcp/server.d.ts +9 -6
- package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
- package/dist/types/sdk/prompt-status.d.ts +10 -3
- package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
- package/dist/types/sdk/receipt-state.d.ts +17 -0
- package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
- package/dist/types/sdk/router/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +152 -0
- package/dist/types/sdk/session-list.d.ts +22 -0
- package/dist/types/sdk/session-reconnect.d.ts +43 -1
- package/dist/types/sdk/session.d.ts +34 -1
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
- package/dist/types/session/account-inventory.d.ts +86 -0
- package/dist/types/session/agent-session.d.ts +145 -6
- package/dist/types/session/agent-storage.d.ts +10 -0
- package/dist/types/session/coordinator-tool-label.d.ts +28 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
- package/dist/types/session/messages.d.ts +1 -1
- package/dist/types/session/secure-token-file.d.ts +3 -0
- package/dist/types/session/session-manager.d.ts +47 -1
- package/dist/types/session/startup-auth-config.d.ts +44 -0
- package/dist/types/session/terminal-abort.d.ts +378 -0
- package/dist/types/session/yield-queue.d.ts +7 -0
- package/dist/types/session-import/claude.d.ts +26 -0
- package/dist/types/session-import/codex.d.ts +88 -0
- package/dist/types/session-import/command.d.ts +35 -0
- package/dist/types/session-import/detect.d.ts +22 -0
- package/dist/types/session-import/index.d.ts +7 -0
- package/dist/types/session-import/provider-codex.d.ts +22 -0
- package/dist/types/session-import/provider-service.d.ts +38 -0
- package/dist/types/session-import/redact.d.ts +23 -0
- package/dist/types/session-import/service.d.ts +47 -0
- package/dist/types/session-import/types.d.ts +120 -0
- package/dist/types/setup/paseo/check.d.ts +16 -0
- package/dist/types/setup/paseo/install-saga.d.ts +115 -0
- package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
- package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
- package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
- package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
- package/dist/types/setup/paseo/provider-config.d.ts +35 -0
- package/dist/types/setup/paseo/remove.d.ts +14 -0
- package/dist/types/setup/paseo/result-types.d.ts +59 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
- package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
- package/dist/types/skill-state/active-state.d.ts +5 -1
- package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
- package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
- package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
- package/dist/types/slash-commands/types.d.ts +7 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/tools/ask-answer-registry.d.ts +6 -3
- package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
- package/dist/types/tools/bash.d.ts +2 -6
- package/dist/types/tools/browser/attach.d.ts +11 -0
- package/dist/types/tools/browser/launch.d.ts +20 -0
- package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
- package/dist/types/tools/browser/render.d.ts +1 -0
- package/dist/types/tools/browser.d.ts +3 -1
- package/dist/types/tools/image-gen.d.ts +10 -2
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/tools/monitor.d.ts +1 -1
- package/dist/types/tools/skill-discovery.d.ts +8 -1
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
- package/dist/types/utils/herdr-pane.d.ts +76 -0
- package/package.json +19 -9
- package/scripts/build-binary.ts +2 -1
- package/scripts/build-sdk-package-smoke.ts +2 -13
- package/scripts/capture-sticky-viewport-showcase.ts +0 -1
- package/scripts/compile-args.ts +2 -0
- package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
- package/scripts/generate-sdk-operation-inventory.ts +28 -0
- package/scripts/generate-telegram-baseline-manifest.ts +1 -0
- package/scripts/generate-tool-catalog.ts +6 -4
- package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
- package/src/async/job-manager.ts +352 -17
- package/src/async/support.ts +25 -0
- package/src/autoresearch/dashboard.ts +28 -13
- package/src/capability/index.ts +3 -1
- package/src/capability/mcp.ts +6 -0
- package/src/capability/types.ts +6 -0
- package/src/cli/accounts-cli.ts +395 -0
- package/src/cli/args.ts +23 -0
- package/src/cli/auth-broker-cli.ts +136 -66
- package/src/cli/auth-gateway-cli.ts +94 -70
- package/src/cli/config-cli.ts +27 -2
- package/src/cli/crash-cli.ts +78 -0
- package/src/cli/customize-doctor.ts +1605 -0
- package/src/cli/daemon-cli.ts +12 -3
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/mcp-cli.ts +128 -60
- package/src/cli/notify-cli.ts +8 -14
- package/src/cli/root-flags.ts +8 -0
- package/src/cli/setup-cli.ts +145 -0
- package/src/cli/skills-cli.ts +38 -2
- package/src/cli/update-cli.ts +167 -12
- package/src/cli.ts +4 -0
- package/src/commands/accounts.ts +71 -0
- package/src/commands/crash.ts +35 -0
- package/src/commands/customize.ts +84 -0
- package/src/commands/daemon.ts +2 -2
- package/src/commands/deep-interview.ts +2 -0
- package/src/commands/launch.ts +1 -0
- package/src/commands/mcp.ts +12 -7
- package/src/commands/ralplan.ts +2 -0
- package/src/commands/sdk.ts +65 -32
- package/src/commands/setup.ts +5 -0
- package/src/commands/skills.ts +14 -4
- package/src/commands/ultragoal.ts +10 -1
- package/src/commands/update.ts +26 -15
- package/src/config/atomic-yaml-patch-worker.ts +57 -0
- package/src/config/atomic-yaml-patch.ts +769 -29
- package/src/config/config-schema-version.ts +10 -0
- package/src/config/file-lock.ts +70 -1
- package/src/config/mcp-schema.json +52 -14
- package/src/config/model-profile-activation.ts +23 -2
- package/src/config/model-profiles.ts +88 -21
- package/src/config/model-registry.ts +147 -21
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +77 -7
- package/src/config/settings.ts +4038 -199
- package/src/config/skill-settings-defaults.ts +41 -3
- package/src/config/task-delegation.ts +87 -0
- package/src/config/update-channel.ts +23 -0
- package/src/coordinator-mcp/server.ts +377 -308
- package/src/crash/index-store.ts +723 -0
- package/src/crash/nudge.ts +89 -0
- package/src/crash/record-loader.ts +149 -0
- package/src/crash/report.ts +551 -0
- package/src/crash/sanitize.ts +129 -0
- package/src/cursor.ts +36 -10
- package/src/customization/import.ts +946 -0
- package/src/customization/index.ts +4 -0
- package/src/customization/inventory.ts +335 -0
- package/src/customization/mutations.ts +141 -0
- package/src/customization/types.ts +223 -0
- package/src/daemon/control-types.ts +8 -1
- package/src/daemon/operator-contract.ts +6 -6
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +31 -6
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
- package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
- package/src/discovery/claude-hooks.ts +38 -0
- package/src/discovery/claude.ts +142 -209
- package/src/discovery/codex-hooks.ts +38 -0
- package/src/discovery/codex.ts +132 -198
- package/src/discovery/helpers.ts +8 -1
- package/src/discovery/index.ts +2 -0
- package/src/discovery/mcp-compat.ts +234 -0
- package/src/discovery/mcp-json.ts +6 -0
- package/src/edit/renderer.ts +40 -0
- package/src/edit/streaming.ts +4 -2
- package/src/exec/bash-executor.ts +24 -20
- package/src/export/html/index.ts +51 -2
- package/src/extensibility/extensions/loader.ts +9 -1
- package/src/extensibility/extensions/runner.ts +17 -4
- package/src/extensibility/extensions/types.ts +31 -6
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
- package/src/extensibility/gjc-plugins/installer.ts +9 -5
- package/src/extensibility/gjc-plugins/observability.ts +8 -2
- package/src/extensibility/gjc-plugins/registry.ts +13 -2
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
- package/src/extensibility/gjc-plugins/schema.ts +222 -39
- package/src/extensibility/hooks/loader.ts +154 -10
- package/src/extensibility/runtime-skill-discovery.ts +257 -60
- package/src/extensibility/skill-management.ts +368 -0
- package/src/extensibility/skills.ts +52 -16
- package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
- package/src/gjc-runtime/launch-worktree.ts +56 -2
- package/src/gjc-runtime/linux-proc.ts +7 -1
- package/src/gjc-runtime/ralplan-runtime.ts +100 -164
- package/src/gjc-runtime/session-state-lock.ts +992 -0
- package/src/gjc-runtime/session-state-sidecar.ts +644 -142
- package/src/gjc-runtime/state-writer.ts +39 -14
- package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
- package/src/gjc-runtime/ultragoal-guard.ts +11 -4
- package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
- package/src/gjc-runtime/workflow-settings.ts +596 -0
- package/src/goals/tools/goal-tool.ts +6 -1
- package/src/hooks/events.ts +291 -0
- package/src/hooks/native-skill-hook.ts +4 -0
- package/src/hooks/normalize.ts +369 -0
- package/src/hooks/skill-state.ts +165 -46
- package/src/internal-urls/docs-index.generated.ts +41 -32
- package/src/main.ts +36 -5
- package/src/migrate/mcp-mapper.ts +25 -2
- package/src/modes/acp/abort-scope.ts +29 -0
- package/src/modes/acp/acp-agent.ts +423 -98
- package/src/modes/components/assistant-message.ts +6 -1
- package/src/modes/components/chrome.ts +4 -3
- package/src/modes/components/customization/customization-dashboard.ts +443 -0
- package/src/modes/components/customization/import-wizard.ts +372 -0
- package/src/modes/components/customization/index.ts +1 -0
- package/src/modes/components/dynamic-theme-text.ts +22 -0
- package/src/modes/components/gajae-pet-widget.ts +607 -193
- package/src/modes/components/irc-sidebar.ts +11 -0
- package/src/modes/components/iterm-pet-transport.ts +585 -0
- package/src/modes/components/model-selector.ts +1 -6
- package/src/modes/components/oauth-selector.ts +277 -86
- package/src/modes/components/pet-capability.ts +35 -31
- package/src/modes/components/settings-selector.ts +29 -5
- package/src/modes/components/status-line/gh.ts +45 -1
- package/src/modes/components/status-line/segments.ts +19 -4
- package/src/modes/components/tool-status-header.ts +13 -3
- package/src/modes/controllers/command-controller.ts +20 -8
- package/src/modes/controllers/extension-ui-controller.ts +56 -257
- package/src/modes/controllers/input-controller.ts +10 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
- package/src/modes/controllers/selector-controller.ts +238 -77
- package/src/modes/controllers/stt-controller.ts +3 -3
- package/src/modes/interactive-mode.ts +88 -16
- package/src/modes/print-mode.ts +35 -6
- package/src/modes/runtime-init.ts +21 -68
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/theme/defaults/ouroboros.json +118 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/injected-user-submission.ts +25 -0
- package/src/modes/utils/terminal-bell.ts +15 -1
- package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/critic.md +1 -1
- package/src/prompts/system/rlm-research.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -2
- package/src/prompts/tools/bash.md +7 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/cron.md +6 -6
- package/src/prompts/tools/read.md +0 -2
- package/src/prompts/tools/skill-discovery.md +4 -2
- package/src/runtime-mcp/client.ts +427 -23
- package/src/runtime-mcp/config.ts +37 -5
- package/src/runtime-mcp/manager-pool.test.ts +56 -0
- package/src/runtime-mcp/manager.ts +52 -2
- package/src/runtime-mcp/oauth-discovery.ts +13 -0
- package/src/runtime-mcp/oauth-flow.ts +46 -1
- package/src/runtime-mcp/pool-key.ts +5 -1
- package/src/runtime-mcp/pool.test.ts +9 -0
- package/src/runtime-mcp/protocol.test.ts +353 -0
- package/src/runtime-mcp/protocol.ts +624 -0
- package/src/runtime-mcp/scope-config.ts +78 -0
- package/src/runtime-mcp/tool-bridge.ts +44 -10
- package/src/runtime-mcp/transports/http.ts +84 -13
- package/src/runtime-mcp/types.ts +123 -12
- package/src/sdk/acp/adapter.ts +328 -97
- package/src/sdk/broker/broker.ts +160 -741
- package/src/sdk/broker/ensure.ts +9 -1
- package/src/sdk/broker/lifecycle.ts +510 -116
- package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
- package/src/sdk/broker/process-incarnation.ts +19 -0
- package/src/sdk/broker/session-index.ts +516 -117
- package/src/sdk/broker/startup-failure.ts +28 -5
- package/src/sdk/broker/transport.ts +1 -13
- package/src/sdk/bus/chat-daemon-cli.ts +4 -2
- package/src/sdk/bus/chat-daemon-control.ts +67 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
- package/src/sdk/bus/config.ts +13 -0
- package/src/sdk/bus/conversation-store.ts +59 -171
- package/src/sdk/bus/daemon-paths.ts +20 -0
- package/src/sdk/bus/discord-conversation.ts +2 -0
- package/src/sdk/bus/discord-daemon.ts +524 -126
- package/src/sdk/bus/discord-live-provider.ts +206 -79
- package/src/sdk/bus/existing-thread-readiness.ts +4 -97
- package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
- package/src/sdk/bus/index.ts +2480 -456
- package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
- package/src/sdk/bus/lifecycle-commands.ts +2 -2
- package/src/sdk/bus/managed-daemon.ts +2 -145
- package/src/sdk/bus/notification-service.ts +196 -0
- package/src/sdk/bus/operator-runtime.ts +1 -1
- package/src/sdk/bus/prompt-reconciliation.ts +24 -15
- package/src/sdk/bus/reconciliation-store.ts +553 -24
- package/src/sdk/bus/session-control.ts +49 -3
- package/src/sdk/bus/slack-conversation.ts +4 -0
- package/src/sdk/bus/slack-daemon.ts +733 -134
- package/src/sdk/bus/slack-live-provider.ts +65 -15
- package/src/sdk/bus/slack-provider.ts +4 -0
- package/src/sdk/bus/slack-thread-binding.ts +9 -104
- package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
- package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
- package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
- package/src/sdk/bus/telegram-daemon-control.ts +3 -13
- package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
- package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
- package/src/sdk/bus/telegram-daemon.ts +2552 -2280
- package/src/sdk/bus/telegram-reference.ts +1 -226
- package/src/sdk/bus/telegram-setup.ts +2 -2
- package/src/sdk/bus/threaded-render.ts +1 -0
- package/src/sdk/bus/topic-registry.ts +279 -146
- package/src/sdk/cli/rows.ts +2 -35
- package/src/sdk/cli/session-cli.ts +776 -700
- package/src/sdk/client/client.ts +892 -1
- package/src/sdk/host/control/dispatch.ts +93 -58
- package/src/sdk/host/control/index.ts +1 -1
- package/src/sdk/host/control/operations.ts +18 -10
- package/src/sdk/host/control/runtime-gate.ts +40 -0
- package/src/sdk/host/host.ts +63 -12
- package/src/sdk/host/query/handlers.ts +66 -7
- package/src/sdk/host/reverse-leases.ts +2 -2
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +2787 -34
- package/src/sdk/host/session-runtime.ts +1489 -101
- package/src/sdk/host/surface-policy.ts +12 -8
- package/src/sdk/host/types.ts +1 -1
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
- package/src/sdk/host/websocket-transport.ts +85 -50
- package/src/sdk/index.ts +2 -1
- package/src/sdk/lifecycle/broker-client.ts +53 -0
- package/src/sdk/lifecycle/client.ts +234 -0
- package/src/sdk/lifecycle/index.ts +3 -0
- package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
- package/src/sdk/lifecycle/service.ts +720 -0
- package/src/sdk/mcp/server.ts +226 -188
- package/src/sdk/prompt-deadline-lease.ts +55 -0
- package/src/sdk/prompt-deadline-manager.ts +148 -0
- package/src/sdk/prompt-status.ts +11 -3
- package/src/sdk/protocol/operation-inventory.generated.json +272 -129
- package/src/sdk/protocol/operation-registry.ts +71 -8
- package/src/sdk/receipt-state.ts +43 -0
- package/src/sdk/reconciliation-extensions.ts +14 -0
- package/src/sdk/router/index.ts +1 -0
- package/src/sdk/router/session-router.ts +1658 -0
- package/src/sdk/session-list.ts +93 -0
- package/src/sdk/session-reconnect.ts +45 -1
- package/src/sdk/session.ts +716 -120
- package/src/sdk/transport/serve-cli.ts +18 -34
- package/src/session/account-inventory.ts +603 -0
- package/src/session/agent-session.ts +2873 -559
- package/src/session/agent-storage.ts +35 -0
- package/src/session/blob-store.ts +32 -1
- package/src/session/coordinator-tool-label.ts +40 -0
- package/src/session/internal/managed-session-scope.ts +4 -0
- package/src/session/internal/managed-session-storage.ts +199 -31
- package/src/session/messages.ts +1 -1
- package/src/session/secure-token-file.ts +80 -0
- package/src/session/session-manager.ts +757 -57
- package/src/session/session-storage.ts +25 -16
- package/src/session/startup-auth-config.ts +297 -0
- package/src/session/terminal-abort.ts +1253 -0
- package/src/session/yield-queue.ts +57 -15
- package/src/session-import/claude.ts +334 -0
- package/src/session-import/codex.ts +1033 -0
- package/src/session-import/command.ts +77 -0
- package/src/session-import/detect.ts +133 -0
- package/src/session-import/index.ts +7 -0
- package/src/session-import/provider-codex.ts +327 -0
- package/src/session-import/provider-service.ts +457 -0
- package/src/session-import/redact.ts +128 -0
- package/src/session-import/service.ts +1264 -0
- package/src/session-import/types.ts +148 -0
- package/src/setup/paseo/check.ts +213 -0
- package/src/setup/paseo/install-saga.ts +293 -0
- package/src/setup/paseo/json-publisher.ts +263 -0
- package/src/setup/paseo/orchestration-preferences.ts +86 -0
- package/src/setup/paseo/paseo-ownership.ts +224 -0
- package/src/setup/paseo/paseo-setup.ts +253 -0
- package/src/setup/paseo/provider-config.ts +139 -0
- package/src/setup/paseo/remove.ts +183 -0
- package/src/setup/paseo/result-types.ts +79 -0
- package/src/setup/paseo/setup-deps.ts +175 -0
- package/src/setup/paseo/skills-bridge.ts +260 -0
- package/src/skill-state/active-state.ts +21 -11
- package/src/slash-commands/acp-builtins.ts +9 -1
- package/src/slash-commands/builtin-registry.ts +117 -19
- package/src/slash-commands/helpers/credential-switch.ts +143 -0
- package/src/slash-commands/helpers/mcp.ts +9 -3
- package/src/slash-commands/helpers/usage-report.ts +61 -56
- package/src/slash-commands/types.ts +7 -2
- package/src/task/executor.ts +30 -11
- package/src/task/index.ts +66 -7
- package/src/task/render.ts +23 -9
- package/src/tools/ask-answer-registry.ts +23 -7
- package/src/tools/ask.ts +8 -4
- package/src/tools/bash-sleep-advisory.ts +115 -0
- package/src/tools/bash.ts +98 -18
- package/src/tools/browser/attach.ts +71 -3
- package/src/tools/browser/launch.ts +86 -13
- package/src/tools/browser/profile-discovery.ts +57 -7
- package/src/tools/browser/render.ts +8 -1
- package/src/tools/browser.ts +130 -21
- package/src/tools/computer.enforcement.test.ts +1 -1
- package/src/tools/computer.ts +3 -2
- package/src/tools/cron.ts +12 -1
- package/src/tools/image-gen.ts +349 -142
- package/src/tools/index.ts +8 -0
- package/src/tools/job.ts +33 -6
- package/src/tools/monitor.ts +41 -2
- package/src/tools/skill-discovery.ts +21 -29
- package/src/tools/subagent.ts +142 -24
- package/src/tools/todo-contract.ts +55 -3
- package/src/tools/todo-write.ts +4 -3
- package/src/tools/tool-catalog.generated.ts +6 -6
- package/src/tools/ultragoal-ask-guard.ts +10 -3
- package/src/utils/herdr-pane.ts +345 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/scrapers/index.ts +6 -4
- package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
- package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
- package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
- package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
- package/dist/types/runtime/lazy-service.test.d.ts +0 -1
- package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
- package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
- package/dist/types/runtime-mcp/config.test.d.ts +0 -1
- package/dist/types/runtime-mcp/http.test.d.ts +0 -1
- package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
- package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
- package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
- package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
- package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
- package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
- package/dist/types/sdk/elevation/audit.d.ts +0 -93
- package/dist/types/sdk/elevation/capability.d.ts +0 -3
- package/dist/types/sdk/elevation/digest.d.ts +0 -72
- package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
- package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
- package/dist/types/sdk/elevation/owner.d.ts +0 -48
- package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
- package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
- package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
- package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
- package/dist/types/session/auth-broker-config.d.ts +0 -13
- package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
- package/dist/types/skills/skills.test.d.ts +0 -1
- package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
- package/dist/types/task/token-log.test.d.ts +0 -1
- package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
- package/dist/types/tools/descriptors.test.d.ts +0 -1
- package/scripts/g005-daemon-smoke.ts +0 -180
- package/scripts/g011-daemon-path-smoke.ts +0 -153
- package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
- package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
- package/src/sdk/bus/telegram-cli.ts +0 -212
- package/src/sdk/elevation/allowlist.ts +0 -54
- package/src/sdk/elevation/audit.ts +0 -208
- package/src/sdk/elevation/capability.ts +0 -41
- package/src/sdk/elevation/digest.ts +0 -129
- package/src/sdk/elevation/dispatch-receipt.ts +0 -184
- package/src/sdk/elevation/grant-ledger.ts +0 -1142
- package/src/sdk/elevation/owner.ts +0 -109
- package/src/session/auth-broker-config.ts +0 -109
- /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
- /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
- /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
|
@@ -33,7 +33,9 @@ import {
|
|
|
33
33
|
assertImagePlaceholdersHavePayload,
|
|
34
34
|
type ContextMaintenanceResult,
|
|
35
35
|
canContinuePersistedHistory,
|
|
36
|
+
dispatchedToolIdentity,
|
|
36
37
|
getAgentTerminalOwnerContext,
|
|
38
|
+
isNonDispatchedToolEvent,
|
|
37
39
|
type ManagedAttemptContinuationOwnership,
|
|
38
40
|
type ManagedAttemptDecision,
|
|
39
41
|
type ManagedAttemptOutcome,
|
|
@@ -175,6 +177,7 @@ export interface ForkContextSeedOptions {
|
|
|
175
177
|
signal?: AbortSignal;
|
|
176
178
|
}
|
|
177
179
|
|
|
180
|
+
import type { AuthCredentialSelector } from "@gajae-code/ai/core";
|
|
178
181
|
import type { MacOSPowerAssertion } from "@gajae-code/natives";
|
|
179
182
|
import {
|
|
180
183
|
extractRetryHint,
|
|
@@ -186,9 +189,14 @@ import {
|
|
|
186
189
|
prompt,
|
|
187
190
|
Snowflake,
|
|
188
191
|
} from "@gajae-code/utils";
|
|
189
|
-
|
|
190
192
|
import { createAppendOnlyContextManager, resolveAppendOnlyMode } from "../append-only-mode";
|
|
191
|
-
import {
|
|
193
|
+
import {
|
|
194
|
+
type AsyncJob,
|
|
195
|
+
type AsyncJobDeliveryState,
|
|
196
|
+
AsyncJobManager,
|
|
197
|
+
asyncJobEndpointId as deriveAsyncJobEndpointId,
|
|
198
|
+
type OwnerSubagentShutdownLease,
|
|
199
|
+
} from "../async";
|
|
192
200
|
import { reset as resetCapabilities } from "../capability";
|
|
193
201
|
import type { Rule } from "../capability/rule";
|
|
194
202
|
import type { CasReceipt } from "../config/atomic-yaml-patch";
|
|
@@ -224,6 +232,7 @@ import type { Settings, SkillsSettings } from "../config/settings";
|
|
|
224
232
|
import { onAppendOnlyModeChanged } from "../config/settings";
|
|
225
233
|
import type { SettingPath } from "../config/settings-schema";
|
|
226
234
|
import { getDefault } from "../config/settings-schema";
|
|
235
|
+
import { resolveEagerTaskDelegation } from "../config/task-delegation";
|
|
227
236
|
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
228
237
|
import { loadCapability } from "../discovery";
|
|
229
238
|
import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
@@ -294,16 +303,23 @@ import {
|
|
|
294
303
|
sessionStateDir,
|
|
295
304
|
} from "../gjc-runtime/session-layout";
|
|
296
305
|
import {
|
|
306
|
+
type CoordinatorToolObservation,
|
|
297
307
|
ownerTerminalContextFromEnvironment,
|
|
298
308
|
persistCoordinatorRuntimeStateFromEvent,
|
|
299
309
|
registerCoordinatorRuntimeStateFinalizer,
|
|
310
|
+
UNPROVEN_TOOL_LABEL,
|
|
300
311
|
} from "../gjc-runtime/session-state-sidecar";
|
|
301
312
|
import { requestGjcWorkerIntegrationAttempt } from "../gjc-runtime/team-runtime";
|
|
302
313
|
import { GjcTeamWorkerHeartbeatReporter } from "../gjc-runtime/team-worker-heartbeat";
|
|
303
314
|
import { GoalRuntime } from "../goals/runtime";
|
|
304
315
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
305
316
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
306
|
-
import {
|
|
317
|
+
import {
|
|
318
|
+
buildSkillStopOutput,
|
|
319
|
+
ensureWorkflowSkillActivationSeed,
|
|
320
|
+
ensureWorkflowSkillActivationState,
|
|
321
|
+
type WorkflowSkillActivationSeed,
|
|
322
|
+
} from "../hooks/skill-state";
|
|
307
323
|
import { initializeLocalRoot, type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
308
324
|
import { shutdownAll as shutdownAllLspClients } from "../lsp/client";
|
|
309
325
|
import { resolveMemoryBackendId } from "../memory-backend/resolve";
|
|
@@ -410,6 +426,7 @@ import {
|
|
|
410
426
|
type ContributionPrepResult,
|
|
411
427
|
prepareContributionPrep,
|
|
412
428
|
} from "./contribution-prep";
|
|
429
|
+
import { canonicalCoordinatorToolLabel } from "./coordinator-tool-label";
|
|
413
430
|
import { pruneStaleFileMentions } from "./file-mention-pruning";
|
|
414
431
|
import type { MemoryGuardRestoreResult } from "./memory-guard-checkpoint-participant";
|
|
415
432
|
import {
|
|
@@ -448,9 +465,22 @@ import {
|
|
|
448
465
|
SessionAppendPersistenceError,
|
|
449
466
|
SessionContextTooLargeError,
|
|
450
467
|
SessionManager,
|
|
468
|
+
SessionNearLimitAppendError,
|
|
451
469
|
transferSessionMessageIdentity,
|
|
452
470
|
} from "./session-manager";
|
|
453
471
|
import { getEntriesForInternalRead, getSessionContextForInternalRead } from "./session-manager-internal";
|
|
472
|
+
import {
|
|
473
|
+
bindToolLineage,
|
|
474
|
+
classifyOwnedEnvelope,
|
|
475
|
+
isOwnedCompletionEnvelope,
|
|
476
|
+
lookupTerminalScope,
|
|
477
|
+
mintTurnLineageIdHash,
|
|
478
|
+
type OwnedCompletionEnvelope,
|
|
479
|
+
registerTerminalTurnScope,
|
|
480
|
+
retireOwnedRegistrationsForEndpoint,
|
|
481
|
+
settleToolLineageRegistrationWindow,
|
|
482
|
+
unregisterOwnedRegistration,
|
|
483
|
+
} from "./terminal-abort";
|
|
454
484
|
|
|
455
485
|
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
456
486
|
import { pruneSupersededMaintenanceReminders, pruneSupersededVolatileProjectContext } from "./volatile-context-pruning";
|
|
@@ -479,6 +509,37 @@ function appendCompactionStateContext(summary: string, stateContext: string[]):
|
|
|
479
509
|
if (stateContext.length === 0) return summary;
|
|
480
510
|
return `${summary}\n\n<compaction-state>\n${stateContext.join("\n")}\n</compaction-state>`;
|
|
481
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* Classify an async-result delivery against terminal-abort ownership:
|
|
514
|
+
* - "ordinary": no owned-completion envelope — deliver as before.
|
|
515
|
+
* - "fresh": an exact registered owned-completion the owning scope's gate
|
|
516
|
+
* authorizes as a fresh-turn resume (scope:"turn", policy enabled).
|
|
517
|
+
* - "drop": a recognized owned-completion the gate denies — scope:"owned"
|
|
518
|
+
* (policy disabled, stopped work must never call followUp/prompt), a
|
|
519
|
+
* forged/unregistered tuple, or an envelope whose terminal scope no longer
|
|
520
|
+
* exists. Dropped entries never reach the agent (AC 36 zero final calls
|
|
521
|
+
* from stopped work), even if a delivery races the settlement purge.
|
|
522
|
+
*/
|
|
523
|
+
export function ownedCompletionResumeAction(message: AgentMessage): "ordinary" | "fresh" | "drop" {
|
|
524
|
+
const details = (message as { details?: { ownedCompletions?: unknown } }).details;
|
|
525
|
+
const envelopes = details?.ownedCompletions;
|
|
526
|
+
// Extension-provided details are untrusted: the field must be an array of
|
|
527
|
+
// structurally valid internal envelopes, or it is ignored entirely —
|
|
528
|
+
// malformed metadata must never crash idle triggerTurn delivery or the
|
|
529
|
+
// agent loop (review thread P2).
|
|
530
|
+
if (!Array.isArray(envelopes) || envelopes.length === 0) return "ordinary";
|
|
531
|
+
// Three states per envelope: no terminal scope (no abort) is ORDINARY,
|
|
532
|
+
// turn-scope enabled is FRESH, owned-scope disabled is DROP. ANY drop in a
|
|
533
|
+
// mixed batch drops the whole delivery (defense in depth).
|
|
534
|
+
let anyFresh = false;
|
|
535
|
+
for (const envelope of envelopes) {
|
|
536
|
+
if (!isOwnedCompletionEnvelope(envelope)) continue;
|
|
537
|
+
const action = classifyOwnedEnvelope(envelope);
|
|
538
|
+
if (action === "drop") return "drop";
|
|
539
|
+
if (action === "fresh") anyFresh = true;
|
|
540
|
+
}
|
|
541
|
+
return anyFresh ? "fresh" : "ordinary";
|
|
542
|
+
}
|
|
482
543
|
|
|
483
544
|
const PRUNED_ARTIFACT_REF_MAX_CHARS = 64;
|
|
484
545
|
|
|
@@ -571,6 +632,9 @@ export interface AgentSessionConfig {
|
|
|
571
632
|
agent: Agent;
|
|
572
633
|
sessionManager: SessionManager;
|
|
573
634
|
settings: Settings;
|
|
635
|
+
/** The session's REQUESTED effective agent directory, independent of the
|
|
636
|
+
* global Settings singleton (which may be reused across sessions). */
|
|
637
|
+
agentDir?: string;
|
|
574
638
|
/** Lazy memory backend service; omitted callers receive a session-local default. */
|
|
575
639
|
memoryBackend?: LazyService<MemoryBackend>;
|
|
576
640
|
/** Lazy workspace-tree service; omitted callers retain the legacy direct-scan path. */
|
|
@@ -614,6 +678,12 @@ export interface AgentSessionConfig {
|
|
|
614
678
|
workflowGatePublication?: "endpoint" | "local";
|
|
615
679
|
/** Tool registry for LSP and settings */
|
|
616
680
|
toolRegistry?: Map<string, AgentTool>;
|
|
681
|
+
/**
|
|
682
|
+
* Tool objects this session's builder constructed from built-in descriptors, captured
|
|
683
|
+
* before any extension, MCP, or dynamically registered tool could claim their registry
|
|
684
|
+
* names. Absent means no provenance was proven and every tool reports as `custom`.
|
|
685
|
+
*/
|
|
686
|
+
builtinToolIdentities?: ReadonlySet<object>;
|
|
617
687
|
/** Tool-session factory context used to lazily attach workflow-gate-only tools. */
|
|
618
688
|
workflowGateToolSession?: ToolSession;
|
|
619
689
|
/** Current session pre-LLM message transform pipeline */
|
|
@@ -695,6 +765,8 @@ export interface AgentSessionConfig {
|
|
|
695
765
|
* **MUST NOT** dispose it on their own teardown.
|
|
696
766
|
*/
|
|
697
767
|
ownedAsyncJobManager?: AsyncJobManager;
|
|
768
|
+
/** False when a child borrows its parent's manager and must not dispose it. */
|
|
769
|
+
disposeAsyncJobManager?: boolean;
|
|
698
770
|
/** Cheap TUI retained-memory counters; absent for headless sessions. */
|
|
699
771
|
retainedMemorySampler?: () => RetainedMemorySample;
|
|
700
772
|
/**
|
|
@@ -723,8 +795,12 @@ export interface AgentSessionConfig {
|
|
|
723
795
|
providerSessionId?: string;
|
|
724
796
|
/** Optional auth-selection identity, distinct from logical/canonical and provider-cache identity. */
|
|
725
797
|
credentialSessionId?: string;
|
|
798
|
+
/** Opaque credential-store authority fingerprint for durable numeric session pins. */
|
|
799
|
+
credentialStoreIdentity?: string;
|
|
726
800
|
/** Optional provider-facing cache identity, distinct from logical session identity. */
|
|
727
801
|
providerCacheSessionId?: string;
|
|
802
|
+
/** Explicit provider affinity whose persisted transcript path scopes async ownership. */
|
|
803
|
+
asyncJobProviderSessionId?: string;
|
|
728
804
|
}
|
|
729
805
|
|
|
730
806
|
export interface AgentSessionMemoryGuardRestoreInput
|
|
@@ -802,6 +878,8 @@ export interface PromptOptions {
|
|
|
802
878
|
onPreflightAcceptCommit?: () => void | Promise<void>;
|
|
803
879
|
/** Skill-only: prepared metadata before the durable fence (path/lineCount/cleanedArgs). */
|
|
804
880
|
onSkillPrepared?: (meta: { name: string; path: string; lineCount?: number; cleanedArgs?: string }) => void;
|
|
881
|
+
/** Optional invocation-scoped cancellation fence used before an accepted skill starts execution. */
|
|
882
|
+
preflightSignal?: AbortSignal;
|
|
805
883
|
}
|
|
806
884
|
|
|
807
885
|
function promptPreflightCancelledError(): Error {
|
|
@@ -809,6 +887,18 @@ function promptPreflightCancelledError(): Error {
|
|
|
809
887
|
error.name = "PromptPreflightCancelledError";
|
|
810
888
|
return error;
|
|
811
889
|
}
|
|
890
|
+
async function awaitPromptInvocationPreflight<T>(pending: Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
891
|
+
if (!signal) return await pending;
|
|
892
|
+
if (signal.aborted) throw promptPreflightCancelledError();
|
|
893
|
+
const cancellation = Promise.withResolvers<never>();
|
|
894
|
+
const cancel = () => cancellation.reject(promptPreflightCancelledError());
|
|
895
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
896
|
+
try {
|
|
897
|
+
return await Promise.race([pending, cancellation.promise]);
|
|
898
|
+
} finally {
|
|
899
|
+
signal.removeEventListener("abort", cancel);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
812
902
|
|
|
813
903
|
function isPromptPreflightCancelledError(error: unknown): boolean {
|
|
814
904
|
return error instanceof Error && error.name === "PromptPreflightCancelledError";
|
|
@@ -950,6 +1040,8 @@ type RetryErrorClassification =
|
|
|
950
1040
|
| "empty_response"
|
|
951
1041
|
| "transient"
|
|
952
1042
|
| "local_unavailable"
|
|
1043
|
+
| "local_snapshot"
|
|
1044
|
+
| "local_buffer_overflow"
|
|
953
1045
|
| "unknown";
|
|
954
1046
|
|
|
955
1047
|
const BARE_DEFAULT_WATCHDOG_ERROR =
|
|
@@ -1040,7 +1132,12 @@ function assistantMessageHasVisibleOrToolContent(message: AssistantMessage): boo
|
|
|
1040
1132
|
|
|
1041
1133
|
function isLocalModelEndpoint(model: Model | undefined): boolean {
|
|
1042
1134
|
if (!model) return false;
|
|
1043
|
-
if (
|
|
1135
|
+
if (
|
|
1136
|
+
model.provider === "ollama" ||
|
|
1137
|
+
model.provider === "lm-studio" ||
|
|
1138
|
+
model.provider === "llama.cpp" ||
|
|
1139
|
+
model.provider === "omlx"
|
|
1140
|
+
) {
|
|
1044
1141
|
return true;
|
|
1045
1142
|
}
|
|
1046
1143
|
try {
|
|
@@ -1504,6 +1601,7 @@ function extractPermissionLocations(
|
|
|
1504
1601
|
* stable edit id while the display arrays preserve delivery order. */
|
|
1505
1602
|
type QueuedDisplayEntry = { text: string; tag?: string; sequence: number };
|
|
1506
1603
|
type IrcRosterClaim = { token: symbol; signature: string; epoch: number; message: CustomMessage };
|
|
1604
|
+
type QueuedFollowUpOwner = { cancel(): boolean };
|
|
1507
1605
|
export type QueuedMessageEditMode = "steer" | "followUp";
|
|
1508
1606
|
|
|
1509
1607
|
export interface QueuedMessageEditEntry {
|
|
@@ -1794,12 +1892,19 @@ type SessionAdmissionEntry = {
|
|
|
1794
1892
|
ready: PromiseWithResolvers<void>;
|
|
1795
1893
|
settled: PromiseWithResolvers<void>;
|
|
1796
1894
|
released: boolean;
|
|
1895
|
+
selectionFenceGeneration: number;
|
|
1896
|
+
continuationCapability?: symbol;
|
|
1797
1897
|
};
|
|
1798
1898
|
|
|
1799
1899
|
type SessionAdmissionLease = {
|
|
1800
1900
|
release(): void;
|
|
1801
1901
|
};
|
|
1802
1902
|
|
|
1903
|
+
type ScheduledContinuationAdmission = {
|
|
1904
|
+
entry: SessionAdmissionEntry;
|
|
1905
|
+
capability: symbol;
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1803
1908
|
export interface DefaultFallbackRuntimeState {
|
|
1804
1909
|
chain: ConfiguredFallbackChain;
|
|
1805
1910
|
controller: FallbackChainRuntimeState;
|
|
@@ -1831,10 +1936,37 @@ function deobfuscateSessionContext(context: SessionContext, obfuscator: SecretOb
|
|
|
1831
1936
|
return { ...context, messages };
|
|
1832
1937
|
}
|
|
1833
1938
|
|
|
1939
|
+
/**
|
|
1940
|
+
* Canonical message_end admission slot. A reservation whose slot is already
|
|
1941
|
+
* `released` needs no await at the canonical append site, so an uncontended
|
|
1942
|
+
* admission never costs a microtask and external emitters keep synchronous
|
|
1943
|
+
* visibility of the persisted append.
|
|
1944
|
+
*/
|
|
1945
|
+
interface CanonicalMessageAdmissionSlot {
|
|
1946
|
+
promise: Promise<void>;
|
|
1947
|
+
released: boolean;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
interface CanonicalMessageAdmission {
|
|
1951
|
+
predecessor: CanonicalMessageAdmissionSlot;
|
|
1952
|
+
release: () => void;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1834
1955
|
export class AgentSession {
|
|
1956
|
+
#provisionalStreamingToolCallIds = new Set<string>();
|
|
1835
1957
|
readonly agent: Agent;
|
|
1836
1958
|
sessionManager: SessionManager;
|
|
1837
1959
|
readonly settings: Settings;
|
|
1960
|
+
readonly #requestedAgentDir: string | undefined;
|
|
1961
|
+
|
|
1962
|
+
/**
|
|
1963
|
+
* The session's effective agent directory: its REQUESTED directory when
|
|
1964
|
+
* provided, else the global Settings singleton's (which may be reused
|
|
1965
|
+
* across sessions and therefore not the requesting session's own).
|
|
1966
|
+
*/
|
|
1967
|
+
getSessionAgentDir(): string {
|
|
1968
|
+
return this.#requestedAgentDir ?? this.settings.getAgentDir();
|
|
1969
|
+
}
|
|
1838
1970
|
readonly memoryBackend: LazyService<MemoryBackend>;
|
|
1839
1971
|
readonly notificationSessionController: NotificationSessionController | undefined;
|
|
1840
1972
|
readonly taskDepth: number;
|
|
@@ -1859,8 +1991,20 @@ export class AgentSession {
|
|
|
1859
1991
|
#preProfileModel: Model | undefined;
|
|
1860
1992
|
#sessionAdmissionQueue: SessionAdmissionEntry[] = [];
|
|
1861
1993
|
#activeSessionAdmission: SessionAdmissionEntry | undefined;
|
|
1994
|
+
#sessionAdmissionClosing = false;
|
|
1862
1995
|
#sessionAdmissionClosed = false;
|
|
1863
1996
|
#sessionAdmissionContext = new AsyncLocalStorage<SessionAdmissionEntry>();
|
|
1997
|
+
#selectionFenceGenerationContext = new AsyncLocalStorage<number>();
|
|
1998
|
+
#selectionFenceTail: Promise<void> = Promise.resolve();
|
|
1999
|
+
#pendingSelectionFences = 0;
|
|
2000
|
+
#selectionFenceDeferredContinuations = new Map<number, number>();
|
|
2001
|
+
#scopedSettlementWaiters = new Set<() => void>();
|
|
2002
|
+
#oldestPendingSelectionFenceGeneration = 0;
|
|
2003
|
+
#followUpReservationEpoch = 0;
|
|
2004
|
+
/** Epochs of follow-up reservations still between reservation and durable enqueue. */
|
|
2005
|
+
#activeFollowUpReservationEpochs = new Set<number>();
|
|
2006
|
+
#followUpReservationDrainWaiters = new Set<() => void>();
|
|
2007
|
+
#selectionFenceGeneration = 0;
|
|
1864
2008
|
#defaultModelSelectionMutationRevision = 0;
|
|
1865
2009
|
#thinkingLevelMutationRevision = 0;
|
|
1866
2010
|
#thinkingVisibilityMutationRevision = 0;
|
|
@@ -1930,9 +2074,62 @@ export class AgentSession {
|
|
|
1930
2074
|
/** Tracks pending follow-up messages for UI display. Removed when delivered.
|
|
1931
2075
|
* See `#steeringMessages` for entry shape. */
|
|
1932
2076
|
#followUpMessages: QueuedDisplayEntry[] = [];
|
|
2077
|
+
/** User/SDK-requested next turns, distinct from aborted-turn continuations. */
|
|
2078
|
+
readonly #externalFollowUps = new WeakSet<AgentMessage>();
|
|
2079
|
+
// SDK requester-ownership correlation per queued follow-up message: the hook
|
|
2080
|
+
// fires when the message is actually DEQUEUED into a run (onFollowUpConsumed),
|
|
2081
|
+
// not when an independently scheduled continuation is accepted — a skipped
|
|
2082
|
+
// continuation must never discard the correlation of work that is still
|
|
2083
|
+
// consumed (review thread P2).
|
|
2084
|
+
readonly #followUpPromotionHooks = new Map<AgentMessage, () => void>();
|
|
2085
|
+
/** SDK-owned follow-ups held outside Agent's live queue until the active run ends. */
|
|
2086
|
+
#deferredSdkFollowUps: AgentMessage[] = [];
|
|
2087
|
+
// Client/SDK steering (turn.prompt diverted to steer while streaming, or an
|
|
2088
|
+
// explicit turn.steer) is an independent root-turn request ONLY when it is
|
|
2089
|
+
// admitted AFTER the terminal abort snapshot: a terminal abort admitted
|
|
2090
|
+
// earlier must never purge it, or the acknowledged submission would never
|
|
2091
|
+
// run and its reconciliation record would stay accepted indefinitely —
|
|
2092
|
+
// while a steer admitted BEFORE the abort is an accepted-pre-close
|
|
2093
|
+
// continuation of the aborted attempt that the contract requires to be
|
|
2094
|
+
// blocked (review thread P1).
|
|
2095
|
+
readonly #externalSteerMessages = new WeakSet<AgentMessage>();
|
|
2096
|
+
/** Per-message SDK requester-ownership correlation for queued client steers:
|
|
2097
|
+
* fired exactly once when the steer's run accepts it — via the idle
|
|
2098
|
+
* auto-continue or the terminal-abort rearm (review thread P1). */
|
|
2099
|
+
readonly #steerPromotionHooks = new WeakMap<AgentMessage, () => void>();
|
|
2100
|
+
/** Monotonic steering admission sequence; the terminal abort snapshots it. */
|
|
2101
|
+
#steeringAdmissionSeq = 0;
|
|
2102
|
+
readonly #externalSteerAdmissionSeq = new WeakMap<AgentMessage, number>();
|
|
2103
|
+
/**
|
|
2104
|
+
* Per-admission terminal-abort steering snapshots, FIFO per aborted turn
|
|
2105
|
+
* key (lineage:epoch). Each admission pushes { sequence, token } here; the
|
|
2106
|
+
* settlement of THAT admission consumes (shifts) it, so overlapping aborts
|
|
2107
|
+
* of the same turn never overwrite an earlier admission's snapshot (review
|
|
2108
|
+
* thread P1). A replay-only abort discards its own token — the durable
|
|
2109
|
+
* replay path never settles, so without the discard a later real abort
|
|
2110
|
+
* would consume the stale entry and treat steering admitted since the
|
|
2111
|
+
* replay as post-abort (review thread P1).
|
|
2112
|
+
*/
|
|
2113
|
+
readonly #terminalAbortSteeringSnapshots = new Map<string, Array<{ seq: number; token: number }>>();
|
|
2114
|
+
readonly #terminalAbortSteeringSnapshotKeys = new Map<number, string>();
|
|
2115
|
+
#terminalAbortAdmissionSeq = 0;
|
|
1933
2116
|
#queuedDisplaySequence = 0;
|
|
2117
|
+
#fireQueuedPromotionHooks(messages: readonly AgentMessage[]): void {
|
|
2118
|
+
for (const message of messages) {
|
|
2119
|
+
const steerHook = this.#steerPromotionHooks.get(message);
|
|
2120
|
+
if (steerHook) {
|
|
2121
|
+
this.#steerPromotionHooks.delete(message);
|
|
2122
|
+
steerHook();
|
|
2123
|
+
}
|
|
2124
|
+
const followUpHook = this.#followUpPromotionHooks.get(message);
|
|
2125
|
+
if (followUpHook) {
|
|
2126
|
+
this.#followUpPromotionHooks.delete(message);
|
|
2127
|
+
followUpHook();
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
1934
2131
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
1935
|
-
#pendingNextTurnMessages: CustomMessage
|
|
2132
|
+
#pendingNextTurnMessages: Array<{ message: CustomMessage; origin: "turn" | "external" }> = [];
|
|
1936
2133
|
#scheduledHiddenNextTurnGeneration: number | undefined = undefined;
|
|
1937
2134
|
#planModeState: PlanModeState | undefined;
|
|
1938
2135
|
#goalModeState: GoalModeState | undefined;
|
|
@@ -1989,6 +2186,7 @@ export class AgentSession {
|
|
|
1989
2186
|
this.#retryReplayEpoch++;
|
|
1990
2187
|
this.#retryReplayUnsafeEpoch = undefined;
|
|
1991
2188
|
this.#firstEventTimeoutRetryStartedAt = Date.now();
|
|
2189
|
+
this.#providerRetryMaxAttempts = undefined;
|
|
1992
2190
|
}
|
|
1993
2191
|
|
|
1994
2192
|
#markRetryReplayUnsafe(): void {
|
|
@@ -2026,6 +2224,7 @@ export class AgentSession {
|
|
|
2026
2224
|
#retryAbortController: AbortController | undefined = undefined;
|
|
2027
2225
|
#retryNowRequested = false;
|
|
2028
2226
|
#firstEventTimeoutRetryStartedAt: number | undefined;
|
|
2227
|
+
#providerRetryMaxAttempts: number | undefined;
|
|
2029
2228
|
#retryAttempt = 0;
|
|
2030
2229
|
#retryPromise: Promise<void> | undefined = undefined;
|
|
2031
2230
|
#retryResolve: (() => void) | undefined = undefined;
|
|
@@ -2069,6 +2268,7 @@ export class AgentSession {
|
|
|
2069
2268
|
* this undefined and **MUST NOT** dispose the global instance on teardown.
|
|
2070
2269
|
*/
|
|
2071
2270
|
readonly #ownedAsyncJobManager: AsyncJobManager | undefined;
|
|
2271
|
+
readonly #disposeAsyncJobManager: boolean;
|
|
2072
2272
|
readonly #ownedMcpManager: MCPManager | undefined;
|
|
2073
2273
|
#startupTurnBarrier: Promise<void> | undefined;
|
|
2074
2274
|
#pendingPythonMessages: Array<{
|
|
@@ -2091,7 +2291,9 @@ export class AgentSession {
|
|
|
2091
2291
|
#ircRosterClaim: IrcRosterClaim | null = null;
|
|
2092
2292
|
#providerSessionId: string | undefined;
|
|
2093
2293
|
#credentialSessionId: string | undefined;
|
|
2294
|
+
#credentialStoreIdentity: string | undefined;
|
|
2094
2295
|
#providerCacheSessionId: string | undefined;
|
|
2296
|
+
readonly #asyncJobProviderSessionId: string | undefined;
|
|
2095
2297
|
#isDisposed = false;
|
|
2096
2298
|
#disposePromise: Promise<void> | undefined;
|
|
2097
2299
|
readonly #toolSessionCleanups = new Set<() => Promise<void> | void>();
|
|
@@ -2100,6 +2302,9 @@ export class AgentSession {
|
|
|
2100
2302
|
#newSessionTransition: Promise<boolean> | undefined;
|
|
2101
2303
|
// Extension system
|
|
2102
2304
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
2305
|
+
/** SDK follow-up ownership by queued message and the attempt that dequeues it. */
|
|
2306
|
+
#sdkRunTokensByQueuedMessage = new WeakMap<AgentMessage, string>();
|
|
2307
|
+
#sdkRunTokensByAttemptScope = new WeakMap<AttemptScope, string>();
|
|
2103
2308
|
#attemptAuthority!: AttemptScopeAuthority;
|
|
2104
2309
|
#attemptRecordStore!: AttemptRecordStore;
|
|
2105
2310
|
#activeLogicalRunId: AttemptRunHandle["logicalRunId"] | undefined;
|
|
@@ -2137,6 +2342,17 @@ export class AgentSession {
|
|
|
2137
2342
|
|
|
2138
2343
|
// Tool registry and prompt builder for extensions
|
|
2139
2344
|
#toolRegistry: Map<string, AgentTool>;
|
|
2345
|
+
/**
|
|
2346
|
+
* Session-owned provenance for tool OBJECTS built from a built-in descriptor.
|
|
2347
|
+
*
|
|
2348
|
+
* Copy-owned so this session can extend it with built-ins it creates or refreshes
|
|
2349
|
+
* later (the execution guard wrapper, an SSH reload, the lazily attached workflow-gate
|
|
2350
|
+
* `ask`) without mutating the builder's set. Weak because it holds no opinion about a
|
|
2351
|
+
* tool's lifetime: an object that is no longer reachable can never be dispatched
|
|
2352
|
+
* again. Custom, MCP, and dynamically registered tools never enter it, so a name
|
|
2353
|
+
* collision cannot inherit a built-in's label.
|
|
2354
|
+
*/
|
|
2355
|
+
#builtinToolIdentities: WeakSet<object>;
|
|
2140
2356
|
#workflowGateToolSession: ToolSession | undefined;
|
|
2141
2357
|
#transformContext: (
|
|
2142
2358
|
messages: AgentMessage[],
|
|
@@ -2234,6 +2450,7 @@ export class AgentSession {
|
|
|
2234
2450
|
/** Test-only abort outcome override for cancel-and-submit rollback coverage. */
|
|
2235
2451
|
#cancelAndSubmitAbortOutcomeProviderForTests: (() => Promise<AbortOutcome>) | undefined = undefined;
|
|
2236
2452
|
#postPromptTasks = new Set<Promise<void>>();
|
|
2453
|
+
#postPromptTaskSelectionFenceGenerations = new Map<Promise<void>, number>();
|
|
2237
2454
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
2238
2455
|
#postPromptTasksResolve: (() => void) | undefined = undefined;
|
|
2239
2456
|
#postPromptTasksAbortController = new AbortController();
|
|
@@ -2297,8 +2514,18 @@ export class AgentSession {
|
|
|
2297
2514
|
|
|
2298
2515
|
#pendingRewindReport: string | undefined = undefined;
|
|
2299
2516
|
#lastSuccessfulYieldToolCallId: string | undefined = undefined;
|
|
2517
|
+
// Private terminal-abort machinery (C04 mode:"terminal"). The lineage id is
|
|
2518
|
+
// minted per prompt turn before the model runs; tool-call bindings attach
|
|
2519
|
+
// the attempt epoch so background registrations can later be classified as
|
|
2520
|
+
// exact owned work (turn-continuation vs owned-completion) by source, never
|
|
2521
|
+
// by timing. Endpoint generation is 0 for local/non-SDK sessions and is
|
|
2522
|
+
// bound by the SDK host layer when a terminal endpoint is known.
|
|
2523
|
+
#terminalEndpointGeneration = 0;
|
|
2524
|
+
#turnLineageIdHash: string | undefined;
|
|
2525
|
+
#terminalLineageSecret = crypto.randomUUID();
|
|
2300
2526
|
#promptGeneration = 0;
|
|
2301
2527
|
#promptPreflightAbortController = new AbortController();
|
|
2528
|
+
#promptPreflightCancellationGeneration = 0;
|
|
2302
2529
|
|
|
2303
2530
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
2304
2531
|
#temporaryProviderSessionScopes: TemporaryProviderSessionScopeRecord[] = [];
|
|
@@ -2364,6 +2591,12 @@ export class AgentSession {
|
|
|
2364
2591
|
});
|
|
2365
2592
|
}
|
|
2366
2593
|
|
|
2594
|
+
#assertSessionAdmissionOpen(): void {
|
|
2595
|
+
if (this.#sessionAdmissionClosing || this.#sessionAdmissionClosed || this.#isDisposed) {
|
|
2596
|
+
throw this.#sessionAdmissionBusyError();
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2367
2600
|
/**
|
|
2368
2601
|
* Reject a turn start while a handoff transition owns the session. Handoff never
|
|
2369
2602
|
* routes its own generation/injection through these turn-start chokepoints, and
|
|
@@ -2412,6 +2645,18 @@ export class AgentSession {
|
|
|
2412
2645
|
next.ready.resolve();
|
|
2413
2646
|
}
|
|
2414
2647
|
|
|
2648
|
+
#captureScheduledContinuationAdmission(): ScheduledContinuationAdmission | undefined {
|
|
2649
|
+
const entry = this.#sessionAdmissionContext.getStore();
|
|
2650
|
+
if (
|
|
2651
|
+
entry?.kind !== "prompt" ||
|
|
2652
|
+
entry.released ||
|
|
2653
|
+
this.#activeSessionAdmission !== entry ||
|
|
2654
|
+
entry.continuationCapability === undefined
|
|
2655
|
+
)
|
|
2656
|
+
return undefined;
|
|
2657
|
+
return { entry, capability: entry.continuationCapability };
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2415
2660
|
async #awaitStartupTurnBarrier(): Promise<void> {
|
|
2416
2661
|
const barrier = this.#startupTurnBarrier;
|
|
2417
2662
|
if (!barrier) return;
|
|
@@ -2427,11 +2672,43 @@ export class AgentSession {
|
|
|
2427
2672
|
async #withSessionAdmission<T>(
|
|
2428
2673
|
kind: SessionAdmissionKind,
|
|
2429
2674
|
body: (lease: SessionAdmissionLease) => Promise<T>,
|
|
2675
|
+
signal?: AbortSignal,
|
|
2676
|
+
continuationAdmission?: ScheduledContinuationAdmission,
|
|
2677
|
+
options?: {
|
|
2678
|
+
allowDuringClosing?: boolean;
|
|
2679
|
+
bypassSelectionFenceGeneration?: number;
|
|
2680
|
+
allowPromptContinuationReentry?: boolean;
|
|
2681
|
+
},
|
|
2430
2682
|
): Promise<T> {
|
|
2431
|
-
if (kind === "prompt") await this.#awaitStartupTurnBarrier();
|
|
2432
2683
|
const owner = this.#sessionAdmissionContext.getStore();
|
|
2433
|
-
if (owner && !owner.released)
|
|
2434
|
-
|
|
2684
|
+
if (owner && !owner.released) {
|
|
2685
|
+
if (
|
|
2686
|
+
continuationAdmission?.entry === owner &&
|
|
2687
|
+
continuationAdmission.capability === owner.continuationCapability
|
|
2688
|
+
)
|
|
2689
|
+
return await body({ release: () => {} });
|
|
2690
|
+
if (options?.allowPromptContinuationReentry === true && owner.kind === "prompt") {
|
|
2691
|
+
return await body({ release: () => {} });
|
|
2692
|
+
}
|
|
2693
|
+
throw this.#sessionAdmissionBusyError();
|
|
2694
|
+
}
|
|
2695
|
+
if (kind === "prompt") await awaitPromptInvocationPreflight(this.#awaitStartupTurnBarrier(), signal);
|
|
2696
|
+
const bypassesSelectionFence =
|
|
2697
|
+
options?.bypassSelectionFenceGeneration !== undefined &&
|
|
2698
|
+
options.bypassSelectionFenceGeneration < this.#selectionFenceGeneration;
|
|
2699
|
+
if (
|
|
2700
|
+
kind === "prompt" &&
|
|
2701
|
+
continuationAdmission === undefined &&
|
|
2702
|
+
!bypassesSelectionFence &&
|
|
2703
|
+
this.#pendingSelectionFences > 0
|
|
2704
|
+
) {
|
|
2705
|
+
await awaitPromptInvocationPreflight(this.#selectionFenceTail, signal);
|
|
2706
|
+
}
|
|
2707
|
+
if (
|
|
2708
|
+
this.#sessionAdmissionClosed ||
|
|
2709
|
+
((this.#sessionAdmissionClosing || this.#isDisposed) && options?.allowDuringClosing !== true)
|
|
2710
|
+
)
|
|
2711
|
+
throw this.#sessionAdmissionBusyError();
|
|
2435
2712
|
// Reject new external turns for the whole handoff transition. The handoff
|
|
2436
2713
|
// itself never acquires prompt admission (it generates via generateHandoff and
|
|
2437
2714
|
// injects via appendCustomMessageEntry), so this fences external entrants —
|
|
@@ -2448,11 +2725,30 @@ export class AgentSession {
|
|
|
2448
2725
|
ready: Promise.withResolvers<void>(),
|
|
2449
2726
|
settled: Promise.withResolvers<void>(),
|
|
2450
2727
|
released: false,
|
|
2728
|
+
selectionFenceGeneration: this.#selectionFenceGeneration,
|
|
2729
|
+
...(kind === "prompt" ? { continuationCapability: Symbol("scheduled-continuation") } : {}),
|
|
2730
|
+
};
|
|
2731
|
+
const releaseEntry = () => {
|
|
2732
|
+
if (entry.released) return;
|
|
2733
|
+
entry.released = true;
|
|
2734
|
+
entry.settled.resolve();
|
|
2735
|
+
const queuedIndex = this.#sessionAdmissionQueue.indexOf(entry);
|
|
2736
|
+
if (queuedIndex >= 0) this.#sessionAdmissionQueue.splice(queuedIndex, 1);
|
|
2737
|
+
if (this.#activeSessionAdmission === entry) this.#activeSessionAdmission = undefined;
|
|
2738
|
+
this.#activateNextSessionAdmission();
|
|
2451
2739
|
};
|
|
2452
2740
|
this.#sessionAdmissionQueue.push(entry);
|
|
2453
2741
|
this.#activateNextSessionAdmission();
|
|
2454
|
-
|
|
2455
|
-
|
|
2742
|
+
try {
|
|
2743
|
+
await awaitPromptInvocationPreflight(entry.ready.promise, signal);
|
|
2744
|
+
} catch (error) {
|
|
2745
|
+
releaseEntry();
|
|
2746
|
+
throw error;
|
|
2747
|
+
}
|
|
2748
|
+
if (
|
|
2749
|
+
this.#sessionAdmissionClosed ||
|
|
2750
|
+
((this.#sessionAdmissionClosing || this.#isDisposed) && options?.allowDuringClosing !== true)
|
|
2751
|
+
) {
|
|
2456
2752
|
entry.released = true;
|
|
2457
2753
|
entry.settled.resolve();
|
|
2458
2754
|
if (this.#activeSessionAdmission === entry) this.#activeSessionAdmission = undefined;
|
|
@@ -2472,11 +2768,7 @@ export class AgentSession {
|
|
|
2472
2768
|
}
|
|
2473
2769
|
|
|
2474
2770
|
const release = () => {
|
|
2475
|
-
|
|
2476
|
-
entry.released = true;
|
|
2477
|
-
entry.settled.resolve();
|
|
2478
|
-
if (this.#activeSessionAdmission === entry) this.#activeSessionAdmission = undefined;
|
|
2479
|
-
this.#activateNextSessionAdmission();
|
|
2771
|
+
releaseEntry();
|
|
2480
2772
|
};
|
|
2481
2773
|
try {
|
|
2482
2774
|
return await this.#sessionAdmissionContext.run(entry, () => body({ release }));
|
|
@@ -2486,19 +2778,22 @@ export class AgentSession {
|
|
|
2486
2778
|
}
|
|
2487
2779
|
|
|
2488
2780
|
async #closeSessionAdmission(): Promise<void> {
|
|
2489
|
-
this.#
|
|
2490
|
-
const queued = this.#sessionAdmissionQueue.splice(0);
|
|
2491
|
-
for (const entry of queued) {
|
|
2492
|
-
entry.released = true;
|
|
2493
|
-
entry.ready.reject(this.#sessionAdmissionBusyError());
|
|
2494
|
-
entry.settled.resolve();
|
|
2495
|
-
}
|
|
2781
|
+
this.#sessionAdmissionClosing = true;
|
|
2496
2782
|
const active = this.#activeSessionAdmission;
|
|
2497
2783
|
if (active?.kind === "prompt") {
|
|
2498
2784
|
this.#promptGeneration++;
|
|
2785
|
+
this.#promptPreflightCancellationGeneration++;
|
|
2499
2786
|
this.#promptPreflightAbortController.abort();
|
|
2500
2787
|
}
|
|
2501
2788
|
if (active) await active.settled.promise;
|
|
2789
|
+
await this.#selectionFenceTail;
|
|
2790
|
+
this.#sessionAdmissionClosed = true;
|
|
2791
|
+
const queued = this.#sessionAdmissionQueue.splice(0);
|
|
2792
|
+
for (const entry of queued) {
|
|
2793
|
+
entry.released = true;
|
|
2794
|
+
entry.ready.resolve();
|
|
2795
|
+
entry.settled.resolve();
|
|
2796
|
+
}
|
|
2502
2797
|
}
|
|
2503
2798
|
|
|
2504
2799
|
#beginInFlight(): void {
|
|
@@ -2509,6 +2804,50 @@ export class AgentSession {
|
|
|
2509
2804
|
// in #refreshTeamWorkerHeartbeat(), including internally dispatched turns.
|
|
2510
2805
|
}
|
|
2511
2806
|
}
|
|
2807
|
+
/**
|
|
2808
|
+
* Allocate a FRESH prompt attempt/lineage for an allowed owned-completion
|
|
2809
|
+
* delivery (corrected turn semantics). The new turn gets a new attempt epoch
|
|
2810
|
+
* and an opaque lineage id; it never reuses the aborted attempt's epoch and
|
|
2811
|
+
* is not a retry/TTSR/steering/successor of the aborted turn. The caller
|
|
2812
|
+
* then invokes the existing followUp/prompt path.
|
|
2813
|
+
*/
|
|
2814
|
+
#resumeFromOwnedCompletion(): void {
|
|
2815
|
+
// Allocate the fresh epoch from the SESSION prompt epoch, not the
|
|
2816
|
+
// module-global counter (root admissions never advance it, so after a
|
|
2817
|
+
// normal root prompt at epoch N the counter could still be <= N and mint
|
|
2818
|
+
// the SAME lineage as the aborted turn — review thread P1). A
|
|
2819
|
+
// session-relative +1 is always distinct from the current turn's epoch.
|
|
2820
|
+
const freshEpoch = this.#promptGeneration + 1;
|
|
2821
|
+
this.#promptGeneration = freshEpoch;
|
|
2822
|
+
this.#turnLineageIdHash = mintTurnLineageIdHash(
|
|
2823
|
+
this.sessionManager.getSessionId?.() ?? "local",
|
|
2824
|
+
freshEpoch,
|
|
2825
|
+
this.#terminalLineageSecret,
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
/**
|
|
2829
|
+
* Whether a same-turn continuation of the current turn is blocked by a
|
|
2830
|
+
* terminal-abort fence. Fails open (false) when no terminal scope exists for
|
|
2831
|
+
* the current lineage+epoch, so ordinary sessions never consult a gate.
|
|
2832
|
+
* Post-close continuations (retry/TTSR/steering/hidden-next-turn/
|
|
2833
|
+
* maintenance/worker successor) are denied at the final synchronous boundary
|
|
2834
|
+
* before method entry; a continuation already linearized as a predecessor
|
|
2835
|
+
* before close stays allowed to finish.
|
|
2836
|
+
*/
|
|
2837
|
+
#isTurnContinuationBlocked(): boolean {
|
|
2838
|
+
const lineageIdHash = this.#turnLineageIdHash;
|
|
2839
|
+
if (!lineageIdHash) return false;
|
|
2840
|
+
const scope = lookupTerminalScope(lineageIdHash, this.#promptGeneration);
|
|
2841
|
+
if (!scope) return false;
|
|
2842
|
+
return (
|
|
2843
|
+
scope.gate.authorizeContinuation({
|
|
2844
|
+
kind: "turn-continuation",
|
|
2845
|
+
lineageIdHash,
|
|
2846
|
+
attemptEpoch: this.#promptGeneration,
|
|
2847
|
+
continuationId: crypto.randomUUID(),
|
|
2848
|
+
}) === "deny"
|
|
2849
|
+
);
|
|
2850
|
+
}
|
|
2512
2851
|
|
|
2513
2852
|
#isPromptPreflightCancelled(generation: number, signal: AbortSignal): boolean {
|
|
2514
2853
|
return signal.aborted || this.#promptGeneration !== generation;
|
|
@@ -2542,6 +2881,17 @@ export class AgentSession {
|
|
|
2542
2881
|
return hold;
|
|
2543
2882
|
}
|
|
2544
2883
|
|
|
2884
|
+
#restoreAndReserveDeferredAgentEndForContinuation(pending: AgentSessionEvent | undefined): symbol | undefined {
|
|
2885
|
+
if (!pending) return undefined;
|
|
2886
|
+
if (this.#pendingAgentEndEmit && this.#pendingAgentEndEmit !== pending) {
|
|
2887
|
+
throw new Error("Cannot restore a deferred agent_end over a different pending terminal event");
|
|
2888
|
+
}
|
|
2889
|
+
this.#pendingAgentEndEmit = pending;
|
|
2890
|
+
const hold = Symbol("deferred-agent-end-continuation");
|
|
2891
|
+
this.#pendingAgentEndContinuationHolds.set(hold, pending);
|
|
2892
|
+
return hold;
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2545
2895
|
#claimDeferredAgentEndForContinuation(hold: symbol | undefined): AgentSessionEvent | undefined {
|
|
2546
2896
|
if (!hold) return undefined;
|
|
2547
2897
|
const pending = this.#pendingAgentEndContinuationHolds.get(hold);
|
|
@@ -2599,33 +2949,159 @@ export class AgentSession {
|
|
|
2599
2949
|
}
|
|
2600
2950
|
lease.closeDiscovery();
|
|
2601
2951
|
}
|
|
2952
|
+
/**
|
|
2953
|
+
* Keep a continuation parked behind a pending selection fence observable to
|
|
2954
|
+
* external idle waits. The deferral window is bounded: it ends when the
|
|
2955
|
+
* deferred invocation synchronously re-reserves its settlement markers
|
|
2956
|
+
* inside the scheduler (`#endSelectionFenceDeferralTracking`); the
|
|
2957
|
+
* promise-settled release is only the safety net for a deferral whose
|
|
2958
|
+
* re-entry never ran (disposed/aborted session).
|
|
2959
|
+
*
|
|
2960
|
+
* The counter is keyed by the fence generation it parks behind. The
|
|
2961
|
+
* selection that owns that fence must not wait on its own parked
|
|
2962
|
+
* continuations: its mid-selection `waitForIdle` is exactly what lets an
|
|
2963
|
+
* inherited continuation settle before the durable mutation, and waiting on
|
|
2964
|
+
* a continuation parked behind the fence the selection itself holds would
|
|
2965
|
+
* reintroduce the selection self-deadlock (#4519).
|
|
2966
|
+
*/
|
|
2967
|
+
#trackSelectionFenceDeferredContinuation(fenceGeneration: number, deferred: Promise<void>): void {
|
|
2968
|
+
this.#selectionFenceDeferredContinuations.set(
|
|
2969
|
+
fenceGeneration,
|
|
2970
|
+
(this.#selectionFenceDeferredContinuations.get(fenceGeneration) ?? 0) + 1,
|
|
2971
|
+
);
|
|
2972
|
+
void deferred
|
|
2973
|
+
.catch(() => {})
|
|
2974
|
+
.finally(() => {
|
|
2975
|
+
this.#endSelectionFenceDeferralTracking(fenceGeneration);
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
/**
|
|
2980
|
+
* End the deferral-limbo tracking window: called synchronously by the
|
|
2981
|
+
* deferred scheduling path once it has re-reserved its own settlement
|
|
2982
|
+
* markers (the predecessor hold and post-prompt task), so the counter no
|
|
2983
|
+
* longer covers the continuation's own execution.
|
|
2984
|
+
*/
|
|
2985
|
+
#endSelectionFenceDeferralTracking(fenceGeneration: number): void {
|
|
2986
|
+
const pending = this.#selectionFenceDeferredContinuations.get(fenceGeneration) ?? 0;
|
|
2987
|
+
if (pending <= 0) return;
|
|
2988
|
+
if (pending === 1) this.#selectionFenceDeferredContinuations.delete(fenceGeneration);
|
|
2989
|
+
else this.#selectionFenceDeferredContinuations.set(fenceGeneration, pending - 1);
|
|
2990
|
+
this.#resolveSessionSettlement();
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
/**
|
|
2994
|
+
* Continuations parked behind selection fences that idle waits must observe.
|
|
2995
|
+
*
|
|
2996
|
+
* Only deferrals whose fence has already settled are counted: their
|
|
2997
|
+
* deferred scheduling tail is (re-)entering within microtask time and the
|
|
2998
|
+
* session is not idle until it does. A deferral behind a still-pending
|
|
2999
|
+
* fence is invisible by construction — the fence owner's own completion may
|
|
3000
|
+
* causally depend on the very settlement the waiter would block on, so
|
|
3001
|
+
* counting it would reintroduce the selection self-deadlock (#4519). It
|
|
3002
|
+
* becomes observable the moment its fence settles, and either the
|
|
3003
|
+
* synchronous re-entry (`#endSelectionFenceDeferralTracking`) or the
|
|
3004
|
+
* promise-settled safety net clears it.
|
|
3005
|
+
*/
|
|
3006
|
+
#pendingSelectionFenceDeferredContinuations(ignoreGenerationFrom?: number): number {
|
|
3007
|
+
let total = 0;
|
|
3008
|
+
for (const [generation, count] of this.#selectionFenceDeferredContinuations) {
|
|
3009
|
+
if (ignoreGenerationFrom !== undefined && generation >= ignoreGenerationFrom) continue;
|
|
3010
|
+
total += count;
|
|
3011
|
+
}
|
|
3012
|
+
return total;
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
/**
|
|
3016
|
+
* Deferral generations that causally-upstream settlement work must ignore:
|
|
3017
|
+
* a deferral parked behind a still-pending selection fence cannot settle
|
|
3018
|
+
* before that fence resolves, and the fence owner's completion may itself
|
|
3019
|
+
* depend on this very settlement. Returns the oldest still-pending fence
|
|
3020
|
+
* generation, or undefined when no fence is pending.
|
|
3021
|
+
*/
|
|
3022
|
+
#turnSettlementDeferralFloor(): number | undefined {
|
|
3023
|
+
return this.#pendingSelectionFences > 0 ? this.#oldestPendingSelectionFenceGeneration : undefined;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
/**
|
|
3027
|
+
* Wait until every follow-up reservation minted before `ownEpoch` has been
|
|
3028
|
+
* released, so a later dispatch can never durably enqueue ahead of an
|
|
3029
|
+
* earlier reserved follow-up. Resolves immediately when no earlier
|
|
3030
|
+
* reservation remains.
|
|
3031
|
+
*/
|
|
3032
|
+
async #waitForEarlierFollowUpReservations(
|
|
3033
|
+
ownEpoch: number,
|
|
3034
|
+
requesterSignal?: AbortSignal,
|
|
3035
|
+
admissionSignal?: AbortSignal,
|
|
3036
|
+
): Promise<void> {
|
|
3037
|
+
while ([...this.#activeFollowUpReservationEpochs].some(epoch => epoch < ownEpoch)) {
|
|
3038
|
+
if (requesterSignal?.aborted || admissionSignal?.aborted) throw promptPreflightCancelledError();
|
|
3039
|
+
const drained = Promise.withResolvers<void>();
|
|
3040
|
+
this.#followUpReservationDrainWaiters.add(drained.resolve);
|
|
3041
|
+
await drained.promise;
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
2602
3044
|
|
|
2603
|
-
#isSessionSettlementPending(): boolean {
|
|
3045
|
+
#isSessionSettlementPending(ignoreSelectionFenceGeneration?: number): boolean {
|
|
2604
3046
|
return (
|
|
2605
3047
|
this.#promptInFlightCount > 0 ||
|
|
2606
3048
|
this.#agentEventHandlersInFlight > 0 ||
|
|
2607
3049
|
this.#agentEndPublicationInFlight > 0 ||
|
|
2608
3050
|
this.#pendingAgentEndContinuationHolds.size > 0 ||
|
|
3051
|
+
this.#pendingSelectionFenceDeferredContinuations(ignoreSelectionFenceGeneration) > 0 ||
|
|
2609
3052
|
this.#pendingAgentEndEmit !== undefined
|
|
2610
3053
|
);
|
|
2611
3054
|
}
|
|
2612
3055
|
|
|
2613
3056
|
#resolveSessionSettlement(): void {
|
|
2614
|
-
if (this.#isSessionSettlementPending() || !this.#sessionSettlementResolve)
|
|
3057
|
+
if (this.#isSessionSettlementPending() || !this.#sessionSettlementResolve) {
|
|
3058
|
+
this.#wakeScopedSettlementWaiters();
|
|
3059
|
+
return;
|
|
3060
|
+
}
|
|
2615
3061
|
const resolve = this.#sessionSettlementResolve;
|
|
2616
3062
|
this.#sessionSettlementResolve = undefined;
|
|
2617
3063
|
this.#sessionSettlementPromise = undefined;
|
|
2618
3064
|
resolve();
|
|
3065
|
+
this.#wakeScopedSettlementWaiters();
|
|
3066
|
+
}
|
|
3067
|
+
/** Re-evaluate every scoped settlement waiter (selection-internal drains). */
|
|
3068
|
+
#wakeScopedSettlementWaiters(): void {
|
|
3069
|
+
for (const check of [...this.#scopedSettlementWaiters]) check();
|
|
2619
3070
|
}
|
|
2620
3071
|
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
3072
|
+
/**
|
|
3073
|
+
* Wait for session settlement. `ignoreSelectionFenceGeneration` carries the
|
|
3074
|
+
* selection's own fence generation for its internal drain only (see
|
|
3075
|
+
* {@link waitForIdle}); external callers observe every unresolved
|
|
3076
|
+
* fence-deferred continuation.
|
|
3077
|
+
*
|
|
3078
|
+
* A scoped waiter registers a wake callback instead of sharing the strict
|
|
3079
|
+
* settlement promise: the strict promise may stay pending on a
|
|
3080
|
+
* fence-deferred continuation (correctly), and the selection that owns that
|
|
3081
|
+
* fence must not block behind it (#4519).
|
|
3082
|
+
*/
|
|
3083
|
+
async #waitForSessionSettlement(ignoreSelectionFenceGeneration?: number): Promise<void> {
|
|
3084
|
+
while (this.#isSessionSettlementPending(ignoreSelectionFenceGeneration)) {
|
|
3085
|
+
if (ignoreSelectionFenceGeneration === undefined) {
|
|
3086
|
+
if (!this.#sessionSettlementPromise) {
|
|
3087
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
3088
|
+
this.#sessionSettlementPromise = promise;
|
|
3089
|
+
this.#sessionSettlementResolve = resolve;
|
|
3090
|
+
}
|
|
3091
|
+
await this.#sessionSettlementPromise;
|
|
3092
|
+
continue;
|
|
3093
|
+
}
|
|
3094
|
+
const wake = Promise.withResolvers<void>();
|
|
3095
|
+
const check = () => {
|
|
3096
|
+
if (!this.#isSessionSettlementPending(ignoreSelectionFenceGeneration)) wake.resolve();
|
|
3097
|
+
};
|
|
3098
|
+
this.#scopedSettlementWaiters.add(check);
|
|
3099
|
+
check();
|
|
3100
|
+
try {
|
|
3101
|
+
await wake.promise;
|
|
3102
|
+
} finally {
|
|
3103
|
+
this.#scopedSettlementWaiters.delete(check);
|
|
2627
3104
|
}
|
|
2628
|
-
await this.#sessionSettlementPromise;
|
|
2629
3105
|
}
|
|
2630
3106
|
}
|
|
2631
3107
|
|
|
@@ -2662,7 +3138,12 @@ export class AgentSession {
|
|
|
2662
3138
|
// A post-prompt continuation runs before its predecessor prompt returns. Its
|
|
2663
3139
|
// publication must settle, but session-wide settlement can still be owned by
|
|
2664
3140
|
// that predecessor; waiting here would make each prompt wait on the other.
|
|
2665
|
-
|
|
3141
|
+
// Deferrals behind still-pending selection fences are excluded here: they
|
|
3142
|
+
// cannot settle before their fence resolves, and the fence owner may be
|
|
3143
|
+
// waiting on this very settlement (#4519).
|
|
3144
|
+
if (!predecessorPromptStillInFlight) {
|
|
3145
|
+
await this.#waitForSessionSettlement(this.#turnSettlementDeferralFloor());
|
|
3146
|
+
}
|
|
2666
3147
|
if (flushError) throw flushError;
|
|
2667
3148
|
}
|
|
2668
3149
|
|
|
@@ -2738,7 +3219,14 @@ export class AgentSession {
|
|
|
2738
3219
|
// Persist before notifying synchronous subscribers: a subscriber may start a
|
|
2739
3220
|
// successor prompt from agent_end, whose running state must serialize after
|
|
2740
3221
|
// this terminal boundary rather than be overwritten by it.
|
|
2741
|
-
|
|
3222
|
+
//
|
|
3223
|
+
// Awaited, not fire-and-forget: this is the terminal boundary, and the write
|
|
3224
|
+
// runs under the native identity-bound state-file lock. A detached write
|
|
3225
|
+
// outlives the session that owns it — under `bun test --isolate` the pending
|
|
3226
|
+
// unlink lands after the runtime has torn down the context it was scheduled in
|
|
3227
|
+
// and segfaults the process, and in production it lets a settled session's
|
|
3228
|
+
// last write race whatever reclaims its lock next.
|
|
3229
|
+
await this.#queueCoordinatorRuntimeStatePersist(pending);
|
|
2742
3230
|
this.#emit(pending);
|
|
2743
3231
|
extensionDelivery = this.#queueExtensionEvent(
|
|
2744
3232
|
pending,
|
|
@@ -2760,9 +3248,31 @@ export class AgentSession {
|
|
|
2760
3248
|
|
|
2761
3249
|
constructor(config: AgentSessionConfig) {
|
|
2762
3250
|
this.agent = config.agent;
|
|
3251
|
+
this.agent.setProvisionalAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
3252
|
+
const contentIndex = assistantMessageEvent.contentIndex ?? 0;
|
|
3253
|
+
const block = message.content[contentIndex];
|
|
3254
|
+
if (block?.type === "toolCall" && block.id) this.#provisionalStreamingToolCallIds.add(block.id);
|
|
3255
|
+
if (
|
|
3256
|
+
assistantMessageEvent.type !== "toolcall_start" &&
|
|
3257
|
+
assistantMessageEvent.type !== "toolcall_delta" &&
|
|
3258
|
+
assistantMessageEvent.type !== "toolcall_end"
|
|
3259
|
+
) {
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
3262
|
+
const event: AgentEvent = {
|
|
3263
|
+
type: "message_update",
|
|
3264
|
+
message,
|
|
3265
|
+
assistantMessageEvent,
|
|
3266
|
+
};
|
|
3267
|
+
void this.#preCacheStreamingEditFile(event);
|
|
3268
|
+
if (assistantMessageEvent.type === "toolcall_delta" || assistantMessageEvent.type === "toolcall_end") {
|
|
3269
|
+
this.#maybeAbortStreamingEdit(event, this.#promptGeneration);
|
|
3270
|
+
}
|
|
3271
|
+
});
|
|
2763
3272
|
this.agent.bindRunCancellationDomainBridge(this.#runCancellationDomains, this.#agentSessionClaimKey);
|
|
2764
3273
|
this.sessionManager = config.sessionManager;
|
|
2765
3274
|
this.settings = config.settings;
|
|
3275
|
+
this.#requestedAgentDir = config.agentDir ? path.resolve(config.agentDir) : undefined;
|
|
2766
3276
|
this.sessionManager.setSessionMemoryMode(this.settings.get("sessionMemory.mode"));
|
|
2767
3277
|
this.#unregisterSessionMemorySettings = this.settings.onChanged(settingPath => {
|
|
2768
3278
|
if (settingPath === "sessionMemory.mode") {
|
|
@@ -2811,6 +3321,7 @@ export class AgentSession {
|
|
|
2811
3321
|
this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
2812
3322
|
this.#recoveryHydrationContext = config.recoveryHydrationContext;
|
|
2813
3323
|
this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
|
|
3324
|
+
this.#disposeAsyncJobManager = config.disposeAsyncJobManager ?? true;
|
|
2814
3325
|
this.#retainedMemorySampler = config.retainedMemorySampler;
|
|
2815
3326
|
this.#ownedMcpManager = config.ownedMcpManager;
|
|
2816
3327
|
this.#startupTurnBarrier = config.startupTurnBarrier;
|
|
@@ -2834,6 +3345,7 @@ export class AgentSession {
|
|
|
2834
3345
|
this.#providerSessionState = config.providerSessionState;
|
|
2835
3346
|
}
|
|
2836
3347
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
3348
|
+
this.#builtinToolIdentities = new WeakSet(config.builtinToolIdentities ?? []);
|
|
2837
3349
|
this.#workflowGateToolSession = config.workflowGateToolSession;
|
|
2838
3350
|
this.#requestedToolNames = config.requestedToolNames;
|
|
2839
3351
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
@@ -2870,16 +3382,80 @@ export class AgentSession {
|
|
|
2870
3382
|
this.#bindWorkflowGateEmitter();
|
|
2871
3383
|
this.yieldQueue = new YieldQueue({
|
|
2872
3384
|
isStreaming: () => this.isStreaming || this.#handoffTransitionActive,
|
|
2873
|
-
injectStreaming: message =>
|
|
3385
|
+
injectStreaming: message => {
|
|
3386
|
+
// Mandated boundary comment (corrected turn semantics): turn-scope
|
|
3387
|
+
// abort blocks only deliveries whose origin is a continuation of the
|
|
3388
|
+
// aborted turn. Owned-completion deliveries from work deliberately
|
|
3389
|
+
// left running are intentionally allowed to resume the agent through
|
|
3390
|
+
// the normal followUp/prompt path and receive a fresh turn attempt.
|
|
3391
|
+
// A denied owned-completion entry (owned scope, forged tuple, or
|
|
3392
|
+
// missing scope) is DROPPED here — it must never call followUp/prompt.
|
|
3393
|
+
const action = ownedCompletionResumeAction(message);
|
|
3394
|
+
if (action === "drop") {
|
|
3395
|
+
// A denied owned-completion entry is terminal: settle its
|
|
3396
|
+
// registration before discarding it. An owned_unsettled abort
|
|
3397
|
+
// (e.g. a job missing the settlement grace) leaves the tuple in
|
|
3398
|
+
// the registry, and without this settlement repeated occurrences
|
|
3399
|
+
// would retain terminal registrations until the bounded registry
|
|
3400
|
+
// saturates and later owned aborts fail closed (review thread
|
|
3401
|
+
// P2).
|
|
3402
|
+
this.#settleDeliveredOwnedRegistrations([message]);
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
// Defer the fresh lineage allocation to the ACTUAL resume admission:
|
|
3406
|
+
// while another prompt is streaming, followUp only queues, so
|
|
3407
|
+
// mutating the session-wide epoch/lineage here would corrupt the
|
|
3408
|
+
// ACTIVE turn's lineage if it is terminal-aborted meanwhile (review
|
|
3409
|
+
// thread P2). The queued resume is admitted through
|
|
3410
|
+
// #promptWithMessage (resetRetryReplaySafety), which allocates the
|
|
3411
|
+
// fresh attempt epoch at turn start. The idle injector, which calls
|
|
3412
|
+
// agent.prompt directly, allocates right before admission.
|
|
3413
|
+
this.agent.followUp(message);
|
|
3414
|
+
},
|
|
2874
3415
|
injectIdle: async messages => {
|
|
2875
|
-
|
|
3416
|
+
// Mandated boundary comment (corrected turn semantics): same origin
|
|
3417
|
+
// split as the streaming injector — an allowed owned-completion
|
|
3418
|
+
// delivery starts a fresh turn attempt/lineage and is not a
|
|
3419
|
+
// continuation of the aborted turn. Denied owned-completion entries
|
|
3420
|
+
// are dropped (mixed batches split before injection).
|
|
3421
|
+
const dropped = messages.filter(message => ownedCompletionResumeAction(message) === "drop");
|
|
3422
|
+
const survivors = messages.filter(message => ownedCompletionResumeAction(message) !== "drop");
|
|
3423
|
+
// Settle the dropped subset (owned scope, forged tuple, vanished
|
|
3424
|
+
// scope): the denied envelopes are terminal and their registrations
|
|
3425
|
+
// must not occupy the bounded registry (review thread P2).
|
|
3426
|
+
if (dropped.length > 0) this.#settleDeliveredOwnedRegistrations(dropped);
|
|
3427
|
+
const first = survivors[0];
|
|
2876
3428
|
if (!first) return;
|
|
2877
3429
|
await this.#awaitStartupTurnBarrier();
|
|
2878
3430
|
if (this.#isDisposed) return;
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
3431
|
+
// A user prompt may have started during the barrier/scheduling
|
|
3432
|
+
// delay: if the session is now streaming, mutating the epoch and
|
|
3433
|
+
// lineage here would corrupt the ACTIVE user turn (and
|
|
3434
|
+
// agent.prompt would then reject as busy, losing the drained
|
|
3435
|
+
// completion). Route the survivors through followUp — the
|
|
3436
|
+
// streaming injector's path — which allocates the fresh resume
|
|
3437
|
+
// lineage at actual admission (review thread P1).
|
|
3438
|
+
if (this.isStreaming) {
|
|
3439
|
+
for (const message of survivors) this.agent.followUp(message);
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
3442
|
+
if (survivors.some(message => ownedCompletionResumeAction(message) === "fresh"))
|
|
3443
|
+
this.#resumeFromOwnedCompletion();
|
|
3444
|
+
try {
|
|
3445
|
+
if (survivors.length === 1) {
|
|
3446
|
+
await this.agent.prompt(first, this.#managedFallbackPromptOptions());
|
|
3447
|
+
} else {
|
|
3448
|
+
await this.agent.prompt(survivors, this.#managedFallbackPromptOptions());
|
|
3449
|
+
}
|
|
3450
|
+
} finally {
|
|
3451
|
+
// The owned completions were delivered OR the prompt attempt
|
|
3452
|
+
// failed (e.g. provider rejection): either way the yield
|
|
3453
|
+
// queue already drained the entries, so this is their only
|
|
3454
|
+
// delivery boundary — settle the registrations even on
|
|
3455
|
+
// failure, otherwise repeated failed idle resumptions leak
|
|
3456
|
+
// terminal tuples into the global registries until capacity
|
|
3457
|
+
// is exhausted (review thread P2).
|
|
3458
|
+
this.#settleDeliveredOwnedRegistrations(survivors);
|
|
2883
3459
|
}
|
|
2884
3460
|
},
|
|
2885
3461
|
scheduleIdleFlush: run => {
|
|
@@ -2972,9 +3548,83 @@ export class AgentSession {
|
|
|
2972
3548
|
this.#agentRegistry = config.agentRegistry;
|
|
2973
3549
|
this.#providerSessionId = config.providerSessionId;
|
|
2974
3550
|
this.#credentialSessionId = config.credentialSessionId;
|
|
3551
|
+
this.#credentialStoreIdentity = config.credentialStoreIdentity;
|
|
2975
3552
|
this.#providerCacheSessionId = config.providerCacheSessionId;
|
|
3553
|
+
this.#asyncJobProviderSessionId = config.asyncJobProviderSessionId;
|
|
2976
3554
|
// Per-tool TTSR reminders are folded into the matched tool's result via this hook.
|
|
2977
|
-
this.agent.afterToolCall = ctx =>
|
|
3555
|
+
this.agent.afterToolCall = ctx => {
|
|
3556
|
+
settleToolLineageRegistrationWindow(ctx.toolCall.id, this.#ownedRegistrationEndpoint());
|
|
3557
|
+
return this.#ttsrAfterToolCall(ctx);
|
|
3558
|
+
};
|
|
3559
|
+
// Bind immutable lineage/attempt metadata to each tool call id before the
|
|
3560
|
+
// tool executes. Background registrations made inside the tool (task, Bash)
|
|
3561
|
+
// read this binding synchronously so their completion can later be
|
|
3562
|
+
// classified as exact owned work instead of a turn continuation. Bindings
|
|
3563
|
+
// intentionally survive the tool call: resumed registrations re-use the
|
|
3564
|
+
// original tool call id and must retain the same owned-completion origin.
|
|
3565
|
+
// They are superseded by a rebind on the same id or by bounded eviction.
|
|
3566
|
+
this.agent.beforeToolCall = ctx => {
|
|
3567
|
+
const lineageIdHash = this.#turnLineageIdHash;
|
|
3568
|
+
if (lineageIdHash) {
|
|
3569
|
+
bindToolLineage(ctx.toolCall.id, {
|
|
3570
|
+
lineageIdHash,
|
|
3571
|
+
promptAttemptEpoch: this.#promptGeneration,
|
|
3572
|
+
endpointGeneration: this.#terminalEndpointGeneration,
|
|
3573
|
+
// Endpoint identity: the owned-registration registry is
|
|
3574
|
+
// process-global and job ids restart per manager, so
|
|
3575
|
+
// concurrent sessions must not collide (review thread P1).
|
|
3576
|
+
// Subagents inherit the parent's manager, so the key follows
|
|
3577
|
+
// the MANAGER's registered endpoint — the child's own
|
|
3578
|
+
// sessionManager id is never registered, and the inherited
|
|
3579
|
+
// manager's completion callback resolves registrations via
|
|
3580
|
+
// AsyncJobManager.endpointIdOf(manager) (review thread P1).
|
|
3581
|
+
endpointId: this.#ownedRegistrationEndpoint(),
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
return undefined;
|
|
3585
|
+
};
|
|
3586
|
+
// A queued owned-completion follow-up is consumed by the agent loop
|
|
3587
|
+
// DIRECTLY (getFollowUpMessages), never through #promptWithMessage, so
|
|
3588
|
+
// the fresh attempt/lineage promised for the resume is allocated HERE at
|
|
3589
|
+
// actual resume admission — when the loop dequeues the follow-up for the
|
|
3590
|
+
// next turn, the previously streaming turn has ended, so mutating the
|
|
3591
|
+
// session-wide epoch/lineage is safe and its tools bind the fresh lineage.
|
|
3592
|
+
this.agent.onFollowUpConsumed = messages => {
|
|
3593
|
+
// A follow-up whose owned-completion origin is DENIED — an owned
|
|
3594
|
+
// scope landed after the result was queued, or the tuple is
|
|
3595
|
+
// forged/vanished-disabled — must NOT resume the agent: remove it
|
|
3596
|
+
// from the dequeued batch before the loop processes it (review
|
|
3597
|
+
// thread P1). This is the final consumption boundary for
|
|
3598
|
+
// follow-up-delivered owned completions.
|
|
3599
|
+
const dropped: AgentMessage[] = [];
|
|
3600
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3601
|
+
if (ownedCompletionResumeAction(messages[i]) === "drop") {
|
|
3602
|
+
dropped.push(messages[i]);
|
|
3603
|
+
messages.splice(i, 1);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
// An allowed owned-completion resume allocates the fresh lineage at
|
|
3607
|
+
// actual admission (see the comment above).
|
|
3608
|
+
if (messages.some(message => ownedCompletionResumeAction(message) === "fresh")) {
|
|
3609
|
+
this.#resumeFromOwnedCompletion();
|
|
3610
|
+
}
|
|
3611
|
+
// Monitor task-notifications now carry the owned-completion envelope
|
|
3612
|
+
// (see tools/monitor.ts), so the general drop + fresh-admission paths
|
|
3613
|
+
// above cover them exactly like async results — no monitor-specific
|
|
3614
|
+
// filter is needed.
|
|
3615
|
+
// Every consumed envelope (delivered OR dropped) is settled: the
|
|
3616
|
+
// dropped ones are captured above so their now-terminal
|
|
3617
|
+
// registrations are retired too — otherwise an owned_unsettled
|
|
3618
|
+
// abort's later drop would keep occupying the global registration
|
|
3619
|
+
// and retained-policy capacities (review thread P2).
|
|
3620
|
+
this.#settleDeliveredOwnedRegistrations([...messages, ...dropped]);
|
|
3621
|
+
// Transfer SDK requester ownership correlation at the ACTUAL dequeue:
|
|
3622
|
+
// every follow-up consumed by this batch's run fires its hook — even
|
|
3623
|
+
// when the batch is drained by a continuation the message did not
|
|
3624
|
+
// schedule (a skipped continuation must never discard the correlation
|
|
3625
|
+
// of work that is still consumed; review thread P2).
|
|
3626
|
+
this.#fireQueuedPromotionHooks(messages);
|
|
3627
|
+
};
|
|
2978
3628
|
this.agent.providerSessionState = this.#providerSessionState;
|
|
2979
3629
|
this.#syncAgentSessionId();
|
|
2980
3630
|
this.#removeEphemeralCustomMessages();
|
|
@@ -3631,7 +4281,12 @@ export class AgentSession {
|
|
|
3631
4281
|
}
|
|
3632
4282
|
|
|
3633
4283
|
getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
|
|
3634
|
-
|
|
4284
|
+
// The session's OWNED manager first: /jobs, the status header, and the
|
|
4285
|
+
// active-background count must reflect THIS session's jobs — with
|
|
4286
|
+
// concurrent top-level sessions the process-global instance belongs to
|
|
4287
|
+
// a different session and would report no running jobs for A or show
|
|
4288
|
+
// B's (review thread P2).
|
|
4289
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
3635
4290
|
if (!manager) return null;
|
|
3636
4291
|
const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
|
|
3637
4292
|
const running = manager.getRunningJobs(ownerFilter).map(job => ({
|
|
@@ -3664,7 +4319,12 @@ export class AgentSession {
|
|
|
3664
4319
|
*/
|
|
3665
4320
|
#cancelOwnAsyncJobs(): void {
|
|
3666
4321
|
if (!this.#agentId) return;
|
|
3667
|
-
|
|
4322
|
+
// The session's OWNED manager first: with concurrent top-level sessions
|
|
4323
|
+
// the process-global instance may belong to a different session, and a
|
|
4324
|
+
// transition (newSession/fork/handoff/switch) must cancel THIS session's
|
|
4325
|
+
// jobs — otherwise the predecessor's stale completion reaches the
|
|
4326
|
+
// successor as ordinary work (review thread P1).
|
|
4327
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
3668
4328
|
if (!manager) return;
|
|
3669
4329
|
// Run owner cleanups first so cron timers (and any other owner-scoped
|
|
3670
4330
|
// resource cleanup) cannot register fresh jobs while we tear down the
|
|
@@ -3675,7 +4335,7 @@ export class AgentSession {
|
|
|
3675
4335
|
|
|
3676
4336
|
async #settleOwnAsyncJobsBeforeArtifactRetirement(): Promise<void> {
|
|
3677
4337
|
const ownerId = this.#agentId;
|
|
3678
|
-
const manager = AsyncJobManager.instance();
|
|
4338
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
3679
4339
|
if (!ownerId || !manager) return;
|
|
3680
4340
|
const lease = manager.beginOwnerSubagentShutdown(ownerId);
|
|
3681
4341
|
if (!lease) throw new Error("Owned async cleanup is already in progress before artifact retirement.");
|
|
@@ -3699,6 +4359,7 @@ export class AgentSession {
|
|
|
3699
4359
|
manager: AsyncJobManager,
|
|
3700
4360
|
lease: OwnerSubagentShutdownLease,
|
|
3701
4361
|
ownerId: string,
|
|
4362
|
+
predecessorEndpointId: string,
|
|
3702
4363
|
): void {
|
|
3703
4364
|
const finalization = (async () => {
|
|
3704
4365
|
while (!this.#isDisposed) {
|
|
@@ -3709,7 +4370,11 @@ export class AgentSession {
|
|
|
3709
4370
|
if (!(await manager.waitForOwnerInFlightDeliveries(ownerId)))
|
|
3710
4371
|
throw new Error("owner_delivery_settlement_timeout");
|
|
3711
4372
|
if (!(await manager.cancelAndSettleOwnerJobs(ownerId))) throw new Error("owner_job_settlement_timeout");
|
|
3712
|
-
if (this.#isDisposed)
|
|
4373
|
+
if (this.#isDisposed) {
|
|
4374
|
+
retireOwnedRegistrationsForEndpoint(predecessorEndpointId);
|
|
4375
|
+
break;
|
|
4376
|
+
}
|
|
4377
|
+
retireOwnedRegistrationsForEndpoint(predecessorEndpointId);
|
|
3713
4378
|
this.sessionManager.retireEphemeralArtifactsAfterTransition();
|
|
3714
4379
|
await this.#runToolSessionTransitionCleanups();
|
|
3715
4380
|
manager.finishOwnerSubagentShutdown(lease, "commit");
|
|
@@ -3730,7 +4395,14 @@ export class AgentSession {
|
|
|
3730
4395
|
|
|
3731
4396
|
#suppressOwnAsyncJobDeliveries(): void {
|
|
3732
4397
|
if (!this.#agentId) return;
|
|
3733
|
-
|
|
4398
|
+
// The session's OWNED manager first: with concurrent top-level sessions
|
|
4399
|
+
// the process-global instance belongs to a different session, and a
|
|
4400
|
+
// transition (clearContext/handoff/switch) must acknowledge THIS
|
|
4401
|
+
// session's pending deliveries — otherwise an already-completed
|
|
4402
|
+
// delivery pending in A's manager is injected after A's context was
|
|
4403
|
+
// cleared while a same-owner delivery in B's manager is discarded
|
|
4404
|
+
// (review thread P2).
|
|
4405
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
3734
4406
|
if (!manager) return;
|
|
3735
4407
|
const pendingJobIds = manager.getDeliveryState({ ownerId: this.#agentId }).pendingJobIds;
|
|
3736
4408
|
if (pendingJobIds.length > 0) {
|
|
@@ -3800,7 +4472,78 @@ export class AgentSession {
|
|
|
3800
4472
|
return queued;
|
|
3801
4473
|
}
|
|
3802
4474
|
|
|
4475
|
+
/**
|
|
4476
|
+
* Immutable coordinator observations, keyed by the exact agent event object they were
|
|
4477
|
+
* taken for.
|
|
4478
|
+
*
|
|
4479
|
+
* Weak because an observation only matters for an event that is eventually published:
|
|
4480
|
+
* a maintenance checkpoint, a rejected terminal, and a superseded run's late event are
|
|
4481
|
+
* observed here but never emitted, and must not be persisted merely because they were
|
|
4482
|
+
* seen.
|
|
4483
|
+
*/
|
|
4484
|
+
#coordinatorToolObservations = new WeakMap<object, CoordinatorToolObservation>();
|
|
4485
|
+
|
|
4486
|
+
/**
|
|
4487
|
+
* Capture what is true at the SYNCHRONOUS agent-event boundary, before any async work.
|
|
4488
|
+
*
|
|
4489
|
+
* The label comes from the tool OBJECT the producer bound to this exact event when it
|
|
4490
|
+
* dispatched the call — never from re-resolving `event.toolName` here. `agent.state.tools`
|
|
4491
|
+
* is mutable: a tool refresh or MCP reload between dispatch and this observation would
|
|
4492
|
+
* otherwise attribute the call to whatever happens to hold its wire name now, publishing
|
|
4493
|
+
* a built-in label for a colliding custom tool that actually ran, or the reverse.
|
|
4494
|
+
*
|
|
4495
|
+
* The clock is captured here for the same reason: it keeps moving while subscribers run.
|
|
4496
|
+
*
|
|
4497
|
+
* A tool END is deliberately NOT labelled: its label comes from the correlation its own
|
|
4498
|
+
* START recorded, so one call cannot open under one label and close under another.
|
|
4499
|
+
*
|
|
4500
|
+
* A pairing-only event is not observed at all. The loop emits a start/end pair for a
|
|
4501
|
+
* call it skipped or aborted before dispatch so the stream keeps its shape, but this
|
|
4502
|
+
* snapshot is a claim about what is RUNNING — and nothing ran. Observing it would put a
|
|
4503
|
+
* tool into `active_tools` for the window between the two synthetic writes.
|
|
4504
|
+
*/
|
|
4505
|
+
#observeCoordinatorToolEvent(event: AgentEvent): void {
|
|
4506
|
+
if (event.type !== "tool_execution_start" && event.type !== "tool_execution_end") return;
|
|
4507
|
+
if (isNonDispatchedToolEvent(event)) return;
|
|
4508
|
+
const label =
|
|
4509
|
+
event.type === "tool_execution_start"
|
|
4510
|
+
? canonicalCoordinatorToolLabel(dispatchedToolIdentity(event), tool =>
|
|
4511
|
+
this.#builtinToolIdentities.has(tool),
|
|
4512
|
+
)
|
|
4513
|
+
: UNPROVEN_TOOL_LABEL;
|
|
4514
|
+
this.#coordinatorToolObservations.set(event, Object.freeze({ label, observedAt: new Date().toISOString() }));
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4517
|
+
#canonicalMessageAdmissionTail: CanonicalMessageAdmissionSlot = { promise: Promise.resolve(), released: true };
|
|
4518
|
+
|
|
4519
|
+
#reserveCanonicalMessageAdmission(event: AgentEvent): CanonicalMessageAdmission | undefined {
|
|
4520
|
+
if (event.type !== "message_end") return undefined;
|
|
4521
|
+
const predecessor = this.#canonicalMessageAdmissionTail;
|
|
4522
|
+
const settled = Promise.withResolvers<void>();
|
|
4523
|
+
const slot: CanonicalMessageAdmissionSlot = { promise: settled.promise, released: false };
|
|
4524
|
+
let released = false;
|
|
4525
|
+
const release = () => {
|
|
4526
|
+
if (released) return;
|
|
4527
|
+
released = true;
|
|
4528
|
+
slot.released = true;
|
|
4529
|
+
settled.resolve();
|
|
4530
|
+
};
|
|
4531
|
+
// The reservation is owned by this emission's handler: keying it by the
|
|
4532
|
+
// event object would let a replayed/bridged duplicate emission overwrite
|
|
4533
|
+
// it and leave the first handler awaiting a promise only its own handler
|
|
4534
|
+
// will ever release.
|
|
4535
|
+
this.#canonicalMessageAdmissionTail = slot;
|
|
4536
|
+
return { predecessor, release };
|
|
4537
|
+
}
|
|
4538
|
+
|
|
3803
4539
|
#trackAgentEvent = (event: AgentEvent): Promise<void> => {
|
|
4540
|
+
// First statement of the listener: the observation must precede every claim,
|
|
4541
|
+
// reservation, and async hop this handler performs.
|
|
4542
|
+
this.#observeCoordinatorToolEvent(event);
|
|
4543
|
+
// Reserve canonical message order synchronously. Agent listeners are not
|
|
4544
|
+
// awaited, so a tool-result spill may yield while a later continuation
|
|
4545
|
+
// otherwise overtakes it in persisted/display context.
|
|
4546
|
+
const canonicalAdmission = this.#reserveCanonicalMessageAdmission(event);
|
|
3804
4547
|
const terminalOwner = event.type === "agent_end" ? getAgentTerminalOwnerContext(event) : undefined;
|
|
3805
4548
|
const maintenanceCheckpoint =
|
|
3806
4549
|
event.type === "agent_end" && event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted";
|
|
@@ -3854,14 +4597,15 @@ export class AgentSession {
|
|
|
3854
4597
|
this.#postPromptLeases.set(eventLease.resourceRunId, eventLease);
|
|
3855
4598
|
if (eventLease) {
|
|
3856
4599
|
await this.#runResourceLeaseContext.run(eventLease, () =>
|
|
3857
|
-
this.#handleAgentEvent(event, activePromptHandle),
|
|
4600
|
+
this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission),
|
|
3858
4601
|
);
|
|
3859
4602
|
} else {
|
|
3860
|
-
await this.#handleAgentEvent(event, activePromptHandle);
|
|
4603
|
+
await this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission);
|
|
3861
4604
|
}
|
|
3862
4605
|
} catch (error) {
|
|
3863
4606
|
logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
|
|
3864
4607
|
} finally {
|
|
4608
|
+
canonicalAdmission?.release();
|
|
3865
4609
|
if (eventLease) {
|
|
3866
4610
|
const pendingAgentEnd =
|
|
3867
4611
|
event.type === "agent_end" && !maintenanceCheckpoint && this.#pendingAgentEndEmit === event
|
|
@@ -3884,13 +4628,43 @@ export class AgentSession {
|
|
|
3884
4628
|
return handler;
|
|
3885
4629
|
};
|
|
3886
4630
|
|
|
3887
|
-
|
|
4631
|
+
/** Serializes sidecar writes in publication order, independent of write latency. */
|
|
4632
|
+
#coordinatorPersistQueue: Promise<void> = Promise.resolve();
|
|
4633
|
+
|
|
4634
|
+
/**
|
|
4635
|
+
* Reserve this event's place in the sidecar write order.
|
|
4636
|
+
*
|
|
4637
|
+
* Called immediately BEFORE local delivery so a synchronous subscriber that re-enters
|
|
4638
|
+
* the session cannot get its own, later event persisted first. The returned promise is
|
|
4639
|
+
* only awaited where durability must precede delivery (terminal `agent_end`).
|
|
4640
|
+
*
|
|
4641
|
+
* A pairing-only tool event takes no place in that order at all — neither its start nor
|
|
4642
|
+
* its end. It describes a call that was never dispatched, and this file is read as the
|
|
4643
|
+
* answer to "what is this session doing right now".
|
|
4644
|
+
*/
|
|
4645
|
+
#queueCoordinatorRuntimeStatePersist(event: AgentSessionEvent): Promise<void> {
|
|
4646
|
+
if (isNonDispatchedToolEvent(event)) return Promise.resolve();
|
|
4647
|
+
const observation = this.#coordinatorToolObservations.get(event);
|
|
4648
|
+
const run = () => this.#persistRuntimeStateInBackground(event, observation);
|
|
4649
|
+
const queued = this.#coordinatorPersistQueue.then(run, run);
|
|
4650
|
+
this.#coordinatorPersistQueue = queued.catch(() => {});
|
|
4651
|
+
return queued;
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
async #persistRuntimeStateInBackground(
|
|
4655
|
+
event: AgentSessionEvent,
|
|
4656
|
+
observation: CoordinatorToolObservation | undefined,
|
|
4657
|
+
): Promise<void> {
|
|
3888
4658
|
try {
|
|
3889
|
-
await persistCoordinatorRuntimeStateFromEvent(
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
4659
|
+
await persistCoordinatorRuntimeStateFromEvent(
|
|
4660
|
+
event,
|
|
4661
|
+
{
|
|
4662
|
+
sessionId: this.sessionId,
|
|
4663
|
+
cwd: this.sessionManager.getCwd(),
|
|
4664
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
4665
|
+
},
|
|
4666
|
+
observation,
|
|
4667
|
+
);
|
|
3894
4668
|
} catch {
|
|
3895
4669
|
logger.warn("Failed to persist coordinator runtime state", { event: event.type });
|
|
3896
4670
|
}
|
|
@@ -3938,7 +4712,6 @@ export class AgentSession {
|
|
|
3938
4712
|
if (event.type === "agent_end" && event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted")
|
|
3939
4713
|
return;
|
|
3940
4714
|
|
|
3941
|
-
const persistRuntimeState = () => this.#persistRuntimeStateInBackground(event);
|
|
3942
4715
|
// Hold agent_end until the prompt's finally and all earlier async event work
|
|
3943
4716
|
// have unwound. Subscribers treat this event as the ready signal; flushing it
|
|
3944
4717
|
// from abort while either barrier is active permits a successor to race the
|
|
@@ -3951,7 +4724,7 @@ export class AgentSession {
|
|
|
3951
4724
|
if (event.type === "agent_end") {
|
|
3952
4725
|
// Start the durable terminal write before synchronous subscribers can
|
|
3953
4726
|
// re-enter prompt(), so a successor's running transition serializes after it.
|
|
3954
|
-
await
|
|
4727
|
+
await this.#queueCoordinatorRuntimeStatePersist(event);
|
|
3955
4728
|
this.#emit(event);
|
|
3956
4729
|
await this.#emitExtensionEvent(event);
|
|
3957
4730
|
return;
|
|
@@ -3961,13 +4734,22 @@ export class AgentSession {
|
|
|
3961
4734
|
// auto-continuation gates, goal reminders, and tests all observe these events
|
|
3962
4735
|
// synchronously. Coordinator sidecar writes and extension hooks are secondary
|
|
3963
4736
|
// sinks, so they must not delay or suppress local delivery.
|
|
4737
|
+
//
|
|
4738
|
+
// The sidecar write is ENQUEUED before delivery and awaited by nobody: a
|
|
4739
|
+
// synchronous subscriber that re-enters the session and emits its own event still
|
|
4740
|
+
// lands behind this one, while nothing about that subscriber's latency reaches the
|
|
4741
|
+
// snapshot.
|
|
4742
|
+
void this.#queueCoordinatorRuntimeStatePersist(event);
|
|
3964
4743
|
this.#emit(event);
|
|
3965
|
-
void persistRuntimeState();
|
|
3966
4744
|
await this.#emitExtensionEvent(event);
|
|
3967
4745
|
}
|
|
3968
4746
|
|
|
3969
4747
|
// Track last assistant message for auto-compaction check
|
|
3970
4748
|
#lastAssistantMessage: AssistantMessage | undefined = undefined;
|
|
4749
|
+
// Admission slot of the last message_end per assistant message, so the
|
|
4750
|
+
// agent_end handler can join the terminal's canonical admission before any
|
|
4751
|
+
// post-turn write reaches the branch.
|
|
4752
|
+
#lastAssistantAdmissionByMessage = new WeakMap<AssistantMessage, CanonicalMessageAdmission | undefined>();
|
|
3971
4753
|
// Provider context construction must wait for this chain. Agent event listeners
|
|
3972
4754
|
// are synchronous dispatch only; their async work cannot otherwise gate the
|
|
3973
4755
|
// next tool-result provider request.
|
|
@@ -4035,7 +4817,11 @@ export class AgentSession {
|
|
|
4035
4817
|
}
|
|
4036
4818
|
|
|
4037
4819
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
4038
|
-
#handleAgentEvent = async (
|
|
4820
|
+
#handleAgentEvent = async (
|
|
4821
|
+
event: AgentEvent,
|
|
4822
|
+
activePromptHandle?: string,
|
|
4823
|
+
canonicalAdmission?: CanonicalMessageAdmission,
|
|
4824
|
+
): Promise<void> => {
|
|
4039
4825
|
const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
|
|
4040
4826
|
|
|
4041
4827
|
if (
|
|
@@ -4182,10 +4968,31 @@ export class AgentSession {
|
|
|
4182
4968
|
this.#silentAbortPending = false;
|
|
4183
4969
|
}
|
|
4184
4970
|
|
|
4185
|
-
// Canonical persistence
|
|
4186
|
-
//
|
|
4187
|
-
//
|
|
4971
|
+
// Canonical persistence follows synchronous message_end reservation order.
|
|
4972
|
+
// Only the admission predecessor and this event's own pre-admission work are
|
|
4973
|
+
// inside the lane; release before extension delivery and unrelated post-work.
|
|
4974
|
+
// An already-released predecessor must not cost a microtask: external emitters
|
|
4975
|
+
// and tests rely on canonical append being visible synchronously after
|
|
4976
|
+
// emitExternalEvent returns whenever no admission is actually contended.
|
|
4977
|
+
// Track the terminal assistant synchronously, before any admission wait:
|
|
4978
|
+
// externally emitted terminals (host bridges, replays, tests) dispatch
|
|
4979
|
+
// agent_end immediately after message_end, and the agent_end handler's
|
|
4980
|
+
// post-turn read must see THIS stop even when this admission is still
|
|
4981
|
+
// parked behind a contended predecessor — otherwise post-turn logic
|
|
4982
|
+
// (deep-interview continuation, compaction, retry classification) runs
|
|
4983
|
+
// against the previous turn's assistant. The per-message admission slot
|
|
4984
|
+
// also lets agent_end processing wait for this admission to finish, so
|
|
4985
|
+
// post-turn writes (continuation reminders, compaction rewrites) never
|
|
4986
|
+
// reorder ahead of the branch entries they respond to.
|
|
4987
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
4988
|
+
this.#lastAssistantMessage = event.message;
|
|
4989
|
+
this.#lastAssistantAdmissionByMessage.set(event.message, canonicalAdmission);
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4188
4992
|
if (event.type === "message_end") {
|
|
4993
|
+
if (canonicalAdmission && !canonicalAdmission.predecessor.released) {
|
|
4994
|
+
await canonicalAdmission.predecessor.promise;
|
|
4995
|
+
}
|
|
4189
4996
|
if (
|
|
4190
4997
|
(event.message.role === "hookMessage" || event.message.role === "custom") &&
|
|
4191
4998
|
!(event.message.role === "custom" && event.message.customType === "hindsight-recall")
|
|
@@ -4216,6 +5023,47 @@ export class AgentSession {
|
|
|
4216
5023
|
try {
|
|
4217
5024
|
this.sessionManager.appendMessage(event.message);
|
|
4218
5025
|
} catch (error) {
|
|
5026
|
+
// Typed near-limit append (#4566): the transcript hit the managed
|
|
5027
|
+
// per-file cap and even the live-entry rewrite could not hold this
|
|
5028
|
+
// entry. The edit/effect itself may already be committed; surface a
|
|
5029
|
+
// structured tool-result outcome stating that and the exact
|
|
5030
|
+
// continuation path instead of a generic fatal abort. The typed
|
|
5031
|
+
// error already performed deterministic recovery, so the session is
|
|
5032
|
+
// not poisoned and the turn ends with actionable state.
|
|
5033
|
+
if (error instanceof SessionNearLimitAppendError) {
|
|
5034
|
+
this.agent.abort();
|
|
5035
|
+
if (event.message.role === "toolResult") {
|
|
5036
|
+
event.message.isError = true;
|
|
5037
|
+
const committed = error.entryRetained
|
|
5038
|
+
? "The edit committed and its receipt is retained in the live session; it will persist on the next successful write."
|
|
5039
|
+
: "The edit committed on disk but its receipt could not be retained in the live session.";
|
|
5040
|
+
event.message.content = [
|
|
5041
|
+
{
|
|
5042
|
+
type: "text",
|
|
5043
|
+
text: [
|
|
5044
|
+
"Session transcript reached the managed per-file limit; this result could not be recorded durably.",
|
|
5045
|
+
committed,
|
|
5046
|
+
"Continue by compacting the session (`/compact`) or exporting to a fresh session (`gjc export <session-file>`); re-verify the edited file before relying on it.",
|
|
5047
|
+
].join("\n"),
|
|
5048
|
+
},
|
|
5049
|
+
];
|
|
5050
|
+
event.message.details = {
|
|
5051
|
+
...(event.message.details && typeof event.message.details === "object"
|
|
5052
|
+
? event.message.details
|
|
5053
|
+
: {}),
|
|
5054
|
+
failureKind: "persistence",
|
|
5055
|
+
nearLimitAppend: {
|
|
5056
|
+
code: error.code,
|
|
5057
|
+
entryBytes: error.entryBytes,
|
|
5058
|
+
liveBytes: error.liveBytes,
|
|
5059
|
+
capBytes: error.capBytes,
|
|
5060
|
+
entryRetained: error.entryRetained,
|
|
5061
|
+
},
|
|
5062
|
+
};
|
|
5063
|
+
this.agent.touchContext();
|
|
5064
|
+
}
|
|
5065
|
+
return;
|
|
5066
|
+
}
|
|
4219
5067
|
if (
|
|
4220
5068
|
event.message.role !== "toolResult" ||
|
|
4221
5069
|
event.message.toolName !== "todo_write" ||
|
|
@@ -4281,6 +5129,7 @@ export class AgentSession {
|
|
|
4281
5129
|
}
|
|
4282
5130
|
}
|
|
4283
5131
|
}
|
|
5132
|
+
canonicalAdmission?.release();
|
|
4284
5133
|
}
|
|
4285
5134
|
|
|
4286
5135
|
// Deobfuscate assistant message content for display emission — the LLM echoes back
|
|
@@ -4478,6 +5327,7 @@ export class AgentSession {
|
|
|
4478
5327
|
|
|
4479
5328
|
if (
|
|
4480
5329
|
event.type === "message_update" &&
|
|
5330
|
+
!this.#isProvisionalStreamingToolEvent(event) &&
|
|
4481
5331
|
(event.assistantMessageEvent.type === "toolcall_start" ||
|
|
4482
5332
|
event.assistantMessageEvent.type === "toolcall_delta" ||
|
|
4483
5333
|
event.assistantMessageEvent.type === "toolcall_end")
|
|
@@ -4487,6 +5337,7 @@ export class AgentSession {
|
|
|
4487
5337
|
|
|
4488
5338
|
if (
|
|
4489
5339
|
event.type === "message_update" &&
|
|
5340
|
+
!this.#isProvisionalStreamingToolEvent(event) &&
|
|
4490
5341
|
(event.assistantMessageEvent.type === "toolcall_end" || event.assistantMessageEvent.type === "toolcall_delta")
|
|
4491
5342
|
) {
|
|
4492
5343
|
this.#maybeAbortStreamingEdit(event, this.#promptGeneration);
|
|
@@ -4498,9 +5349,9 @@ export class AgentSession {
|
|
|
4498
5349
|
this.#markTtsrInjected(this.#extractTtsrRuleNames(event.message.details));
|
|
4499
5350
|
}
|
|
4500
5351
|
|
|
4501
|
-
//
|
|
5352
|
+
// (#lastAssistantMessage is captured synchronously before the admission
|
|
5353
|
+
// wait above; the block below handles assistant side effects only.)
|
|
4502
5354
|
if (event.message.role === "assistant") {
|
|
4503
|
-
this.#lastAssistantMessage = event.message;
|
|
4504
5355
|
const assistantMsg = event.message as AssistantMessage;
|
|
4505
5356
|
const currentGrantsAnthropicPriority =
|
|
4506
5357
|
this.serviceTier === "priority" || this.serviceTier === "claude-only";
|
|
@@ -4550,6 +5401,9 @@ export class AgentSession {
|
|
|
4550
5401
|
// #resolveRetry() is idempotent, so the later tail call is a no-op.
|
|
4551
5402
|
this.#resolveRetry();
|
|
4552
5403
|
}
|
|
5404
|
+
if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "aborted") {
|
|
5405
|
+
this.#providerRetryMaxAttempts = undefined;
|
|
5406
|
+
}
|
|
4553
5407
|
}
|
|
4554
5408
|
|
|
4555
5409
|
if (event.message.role === "toolResult") {
|
|
@@ -4623,6 +5477,13 @@ export class AgentSession {
|
|
|
4623
5477
|
}
|
|
4624
5478
|
}
|
|
4625
5479
|
|
|
5480
|
+
if (
|
|
5481
|
+
event.type === "agent_end" &&
|
|
5482
|
+
!(event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted")
|
|
5483
|
+
) {
|
|
5484
|
+
this.#releaseDeferredSdkFollowUps();
|
|
5485
|
+
}
|
|
5486
|
+
|
|
4626
5487
|
// Check auto-retry and auto-compaction after agent completes
|
|
4627
5488
|
if (event.type === "agent_end") {
|
|
4628
5489
|
// Cooperative mid-run maintenance interruption (issue #2035). The loop
|
|
@@ -4677,6 +5538,16 @@ export class AgentSession {
|
|
|
4677
5538
|
.find((message): message is AssistantMessage => message.role === "assistant");
|
|
4678
5539
|
const msg = this.#lastAssistantMessage ?? fallbackAssistant;
|
|
4679
5540
|
this.#lastAssistantMessage = undefined;
|
|
5541
|
+
// Join the terminal's canonical admission before any post-turn write:
|
|
5542
|
+
// an externally emitted terminal dispatches agent_end while its own
|
|
5543
|
+
// admission may still be parked behind a contended predecessor, and a
|
|
5544
|
+
// continuation reminder or compaction rewrite that runs first would
|
|
5545
|
+
// persist ahead of the branch entries it responds to.
|
|
5546
|
+
const terminalAdmission = msg ? this.#lastAssistantAdmissionByMessage.get(msg) : undefined;
|
|
5547
|
+
if (msg) this.#lastAssistantAdmissionByMessage.delete(msg);
|
|
5548
|
+
if (terminalAdmission && !terminalAdmission.predecessor.released) {
|
|
5549
|
+
await terminalAdmission.predecessor.promise;
|
|
5550
|
+
}
|
|
4680
5551
|
if (!msg) {
|
|
4681
5552
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
4682
5553
|
this.#resolveRetry();
|
|
@@ -4805,8 +5676,14 @@ export class AgentSession {
|
|
|
4805
5676
|
this.#postPromptTasksPromise = undefined;
|
|
4806
5677
|
}
|
|
4807
5678
|
|
|
4808
|
-
#trackPostPromptTask(
|
|
5679
|
+
#trackPostPromptTask(
|
|
5680
|
+
task: Promise<void>,
|
|
5681
|
+
selectionFenceGeneration: number,
|
|
5682
|
+
lease?: RunResourceProducerLease,
|
|
5683
|
+
leaseTask: Promise<void> = task,
|
|
5684
|
+
): void {
|
|
4809
5685
|
this.#postPromptTasks.add(task);
|
|
5686
|
+
this.#postPromptTaskSelectionFenceGenerations.set(task, selectionFenceGeneration);
|
|
4810
5687
|
this.#ensurePostPromptTasksPromise();
|
|
4811
5688
|
if (lease) {
|
|
4812
5689
|
lease.track("post_prompt", "agent-session", leaseTask);
|
|
@@ -4816,6 +5693,7 @@ export class AgentSession {
|
|
|
4816
5693
|
.catch(() => {})
|
|
4817
5694
|
.finally(() => {
|
|
4818
5695
|
this.#postPromptTasks.delete(task);
|
|
5696
|
+
this.#postPromptTaskSelectionFenceGenerations.delete(task);
|
|
4819
5697
|
if (this.#postPromptTasks.size === 0) this.#resolvePostPromptTasks();
|
|
4820
5698
|
});
|
|
4821
5699
|
}
|
|
@@ -4828,8 +5706,14 @@ export class AgentSession {
|
|
|
4828
5706
|
onSkip?: () => void;
|
|
4829
5707
|
resourceRunId?: string;
|
|
4830
5708
|
leaseTask?: Promise<void>;
|
|
5709
|
+
selectionFenceGeneration?: number;
|
|
4831
5710
|
},
|
|
4832
5711
|
): Promise<void> {
|
|
5712
|
+
const selectionFenceGeneration =
|
|
5713
|
+
options?.selectionFenceGeneration ??
|
|
5714
|
+
this.#selectionFenceGenerationContext.getStore() ??
|
|
5715
|
+
this.#sessionAdmissionContext.getStore()?.selectionFenceGeneration ??
|
|
5716
|
+
this.#selectionFenceGeneration;
|
|
4833
5717
|
const delayMs = options?.delayMs ?? 0;
|
|
4834
5718
|
const resourceRunId = options?.resourceRunId;
|
|
4835
5719
|
const contextualLease = this.#runResourceLeaseContext.getStore();
|
|
@@ -4867,12 +5751,17 @@ export class AgentSession {
|
|
|
4867
5751
|
options.onSkip?.();
|
|
4868
5752
|
return;
|
|
4869
5753
|
}
|
|
4870
|
-
await task(signal);
|
|
5754
|
+
await this.#selectionFenceGenerationContext.run(selectionFenceGeneration, () => task(signal));
|
|
4871
5755
|
};
|
|
4872
5756
|
const scheduled = reservation?.ok
|
|
4873
5757
|
? this.#runResourceLeaseContext.run(reservation.lease, runScheduled)
|
|
4874
5758
|
: runScheduled();
|
|
4875
|
-
this.#trackPostPromptTask(
|
|
5759
|
+
this.#trackPostPromptTask(
|
|
5760
|
+
scheduled,
|
|
5761
|
+
selectionFenceGeneration,
|
|
5762
|
+
reservation?.ok ? reservation.lease : undefined,
|
|
5763
|
+
options?.leaseTask,
|
|
5764
|
+
);
|
|
4876
5765
|
return scheduled;
|
|
4877
5766
|
}
|
|
4878
5767
|
|
|
@@ -4886,9 +5775,10 @@ export class AgentSession {
|
|
|
4886
5775
|
skipCompactionCheck?: boolean;
|
|
4887
5776
|
suppressPredecessorAgentEnd?: boolean;
|
|
4888
5777
|
shouldContinue?: () => boolean;
|
|
4889
|
-
onSkip?: (
|
|
5778
|
+
onSkip?: (
|
|
5779
|
+
reason: "generation_changed" | "aborted_signal" | "queue_drained" | "handoff_in_progress" | "terminal_turn",
|
|
5780
|
+
) => void;
|
|
4890
5781
|
allowDuringCancelAndSubmit?: boolean;
|
|
4891
|
-
rescheduleOnBusy?: boolean;
|
|
4892
5782
|
onError?: (error: unknown) => void;
|
|
4893
5783
|
resourceRunId?: string;
|
|
4894
5784
|
maintenanceContinuation?: boolean;
|
|
@@ -4897,17 +5787,66 @@ export class AgentSession {
|
|
|
4897
5787
|
// queued turn without replaying the non-assistant tail. Every other
|
|
4898
5788
|
// scheduled continuation keeps continue() semantics.
|
|
4899
5789
|
continueQueuedOnly?: boolean;
|
|
5790
|
+
/** Reschedule with capped exponential backoff when the continuation hits AgentBusyError. */
|
|
5791
|
+
rescheduleOnBusy?: boolean;
|
|
5792
|
+
/** Called when the scheduled continuation accepts its run (before agent_start). */
|
|
5793
|
+
onRunAccepted?: (handle: AttemptRunHandle) => void;
|
|
5794
|
+
/** Internal causal generation retained when a later continuation is deferred behind selection. */
|
|
5795
|
+
selectionFenceGeneration?: number;
|
|
5796
|
+
/** Internal predecessor publication hold retained across selection-fence deferral. */
|
|
5797
|
+
predecessorAgentEndHold?: symbol;
|
|
5798
|
+
/** Internal predecessor terminal event sequestered while waiting behind selection. */
|
|
5799
|
+
deferredPredecessorAgentEnd?: AgentSessionEvent;
|
|
4900
5800
|
}): Promise<void> {
|
|
4901
|
-
const
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
5801
|
+
const continuationAdmission = this.#captureScheduledContinuationAdmission();
|
|
5802
|
+
const selectionFenceGeneration =
|
|
5803
|
+
options?.selectionFenceGeneration ??
|
|
5804
|
+
this.#selectionFenceGenerationContext.getStore() ??
|
|
5805
|
+
this.#sessionAdmissionContext.getStore()?.selectionFenceGeneration ??
|
|
5806
|
+
continuationAdmission?.entry.selectionFenceGeneration ??
|
|
5807
|
+
this.#selectionFenceGeneration;
|
|
5808
|
+
if (this.#pendingSelectionFences > 0 && selectionFenceGeneration === this.#selectionFenceGeneration) {
|
|
5809
|
+
const deferredPredecessorAgentEnd =
|
|
5810
|
+
options?.deferredPredecessorAgentEnd ??
|
|
5811
|
+
(options?.suppressPredecessorAgentEnd
|
|
5812
|
+
? this.#claimDeferredAgentEndForContinuation(this.#reserveDeferredAgentEndForContinuation())
|
|
5813
|
+
: undefined);
|
|
5814
|
+
const precedingSelectionFence = this.#selectionFenceTail;
|
|
5815
|
+
const deferredPromptGeneration = options?.generation ?? this.#promptGeneration;
|
|
5816
|
+
const deferredScheduling = precedingSelectionFence.then(() => {
|
|
5817
|
+
try {
|
|
5818
|
+
this.#scheduleAgentContinue({
|
|
5819
|
+
...options,
|
|
5820
|
+
generation: deferredPromptGeneration,
|
|
5821
|
+
selectionFenceGeneration,
|
|
5822
|
+
deferredPredecessorAgentEnd,
|
|
5823
|
+
});
|
|
5824
|
+
} finally {
|
|
5825
|
+
// The recursive call synchronously re-reserved its settlement
|
|
5826
|
+
// markers (predecessor hold / post-prompt task); end the limbo
|
|
5827
|
+
// window so the counter never spans the continuation run itself.
|
|
5828
|
+
this.#endSelectionFenceDeferralTracking(selectionFenceGeneration);
|
|
5829
|
+
}
|
|
5830
|
+
});
|
|
5831
|
+
this.#trackSelectionFenceDeferredContinuation(selectionFenceGeneration, deferredScheduling);
|
|
5832
|
+
return Promise.resolve();
|
|
5833
|
+
}
|
|
5834
|
+
const predecessorAgentEndHold =
|
|
5835
|
+
options?.predecessorAgentEndHold ??
|
|
5836
|
+
(options?.deferredPredecessorAgentEnd
|
|
5837
|
+
? this.#restoreAndReserveDeferredAgentEndForContinuation(options.deferredPredecessorAgentEnd)
|
|
5838
|
+
: options?.suppressPredecessorAgentEnd
|
|
5839
|
+
? this.#reserveDeferredAgentEndForContinuation()
|
|
5840
|
+
: undefined);
|
|
5841
|
+
let terminalized = false;
|
|
5842
|
+
const skip = (
|
|
5843
|
+
reason: "generation_changed" | "aborted_signal" | "queue_drained" | "handoff_in_progress" | "terminal_turn",
|
|
5844
|
+
) => {
|
|
5845
|
+
if (terminalized) return;
|
|
5846
|
+
terminalized = true;
|
|
5847
|
+
this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
5848
|
+
options?.onSkip?.(reason);
|
|
5849
|
+
};
|
|
4911
5850
|
const fail = (error: unknown) => {
|
|
4912
5851
|
if (terminalized) return;
|
|
4913
5852
|
terminalized = true;
|
|
@@ -4924,126 +5863,161 @@ export class AgentSession {
|
|
|
4924
5863
|
const settleLease = () => leaseSettlement.resolve();
|
|
4925
5864
|
return this.#schedulePostPromptTask(
|
|
4926
5865
|
async scheduledSignal => {
|
|
4927
|
-
const canContinue = (): boolean => {
|
|
4928
|
-
if (scheduledSignal.aborted || this.#isDisposed) {
|
|
4929
|
-
skip("aborted_signal");
|
|
4930
|
-
return false;
|
|
4931
|
-
}
|
|
4932
|
-
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
4933
|
-
skip("generation_changed");
|
|
4934
|
-
return false;
|
|
4935
|
-
}
|
|
4936
|
-
if (this.#cancelAndSubmitInProgress && !options?.allowDuringCancelAndSubmit) {
|
|
4937
|
-
skip("queue_drained");
|
|
4938
|
-
return false;
|
|
4939
|
-
}
|
|
4940
|
-
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
4941
|
-
skip("queue_drained");
|
|
4942
|
-
return false;
|
|
4943
|
-
}
|
|
4944
|
-
return true;
|
|
4945
|
-
};
|
|
4946
|
-
if (!canContinue()) {
|
|
4947
|
-
settleLease();
|
|
4948
|
-
return;
|
|
4949
|
-
}
|
|
4950
|
-
await this.#awaitStartupTurnBarrier();
|
|
4951
|
-
if (!canContinue()) {
|
|
4952
|
-
settleLease();
|
|
4953
|
-
return;
|
|
4954
|
-
}
|
|
4955
5866
|
try {
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
}
|
|
4964
|
-
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
4965
|
-
skip("generation_changed");
|
|
4966
|
-
return;
|
|
4967
|
-
}
|
|
4968
|
-
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
4969
|
-
skip("queue_drained");
|
|
4970
|
-
return;
|
|
4971
|
-
}
|
|
4972
|
-
// A continuation scheduled before a handoff engaged must not start a
|
|
4973
|
-
// turn against the session being handed off (or the restored
|
|
4974
|
-
// predecessor). rearmIdle / normal delivery resumes after the fence.
|
|
4975
|
-
if (this.#handoffTransitionActive) {
|
|
4976
|
-
skip("handoff_in_progress");
|
|
4977
|
-
return;
|
|
4978
|
-
}
|
|
4979
|
-
const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
|
|
4980
|
-
let predecessorAccepted = false;
|
|
4981
|
-
const releasePredecessor = () => {
|
|
4982
|
-
if (predecessorAccepted) return;
|
|
4983
|
-
predecessorAccepted = true;
|
|
4984
|
-
this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
|
|
4985
|
-
};
|
|
4986
|
-
const hasQueuedMessages = this.agent.hasQueuedMessages();
|
|
4987
|
-
const startsQueuedSuccessor =
|
|
4988
|
-
hasQueuedMessages &&
|
|
4989
|
-
(options?.continueQueuedOnly === true || this.agent.state.messages.at(-1)?.role === "assistant");
|
|
4990
|
-
const continueQueued = options?.continueQueuedOnly
|
|
4991
|
-
? this.agent.continueQueuedMessages.bind(this.agent)
|
|
4992
|
-
: this.agent.continue.bind(this.agent);
|
|
4993
|
-
try {
|
|
4994
|
-
await continueQueued({
|
|
4995
|
-
...this.#managedFallbackPromptOptions(),
|
|
4996
|
-
maintenanceContinuation: options?.maintenanceContinuation,
|
|
4997
|
-
// Reset only after continue() has claimed the queued turn. Skipped or stale
|
|
4998
|
-
// continuations retain predecessor accounting, and resetAttemptBudget keeps
|
|
4999
|
-
// the sticky fallback cursor unchanged.
|
|
5000
|
-
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
5001
|
-
this.#acceptRunHandle(handle);
|
|
5002
|
-
settleLease();
|
|
5003
|
-
releasePredecessor();
|
|
5004
|
-
if (startsQueuedSuccessor) {
|
|
5005
|
-
this.#defaultFallbackChain().resetAttemptBudget();
|
|
5006
|
-
this.#overflowMaintenanceAttempts = 0;
|
|
5867
|
+
await this.#withSessionAdmission(
|
|
5868
|
+
"prompt",
|
|
5869
|
+
async () => {
|
|
5870
|
+
const canContinue = (): boolean => {
|
|
5871
|
+
if (scheduledSignal.aborted || this.#isDisposed) {
|
|
5872
|
+
skip("aborted_signal");
|
|
5873
|
+
return false;
|
|
5007
5874
|
}
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
predecessorAgentEnd,
|
|
5025
|
-
);
|
|
5026
|
-
logger.debug("agent.continue busy after scheduling; rescheduling", {
|
|
5027
|
-
error: error.message,
|
|
5028
|
-
attempt: busyReschedules,
|
|
5029
|
-
delayMs: nextDelayMs,
|
|
5030
|
-
});
|
|
5031
|
-
void scheduleAttempt(nextDelayMs);
|
|
5875
|
+
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
5876
|
+
skip("generation_changed");
|
|
5877
|
+
return false;
|
|
5878
|
+
}
|
|
5879
|
+
if (this.#cancelAndSubmitInProgress && !options?.allowDuringCancelAndSubmit) {
|
|
5880
|
+
skip("queue_drained");
|
|
5881
|
+
return false;
|
|
5882
|
+
}
|
|
5883
|
+
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
5884
|
+
skip("queue_drained");
|
|
5885
|
+
return false;
|
|
5886
|
+
}
|
|
5887
|
+
return true;
|
|
5888
|
+
};
|
|
5889
|
+
if (!canContinue()) {
|
|
5890
|
+
settleLease();
|
|
5032
5891
|
return;
|
|
5033
5892
|
}
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5893
|
+
await this.#awaitStartupTurnBarrier();
|
|
5894
|
+
if (!canContinue()) {
|
|
5895
|
+
settleLease();
|
|
5896
|
+
return;
|
|
5897
|
+
}
|
|
5898
|
+
try {
|
|
5899
|
+
if (!options?.skipCompactionCheck) {
|
|
5900
|
+
await this.#checkEstimatedContextBeforePrompt();
|
|
5901
|
+
if (!canContinue()) return;
|
|
5902
|
+
}
|
|
5903
|
+
if (scheduledSignal.aborted) {
|
|
5904
|
+
skip("aborted_signal");
|
|
5905
|
+
return;
|
|
5906
|
+
}
|
|
5907
|
+
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
5908
|
+
skip("generation_changed");
|
|
5909
|
+
return;
|
|
5910
|
+
}
|
|
5911
|
+
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
5912
|
+
skip("queue_drained");
|
|
5913
|
+
return;
|
|
5914
|
+
}
|
|
5915
|
+
// A scheduled same-turn continuation of a terminally aborted turn is
|
|
5916
|
+
// denied at the final synchronous boundary before agent.continue
|
|
5917
|
+
// entry; no await intervenes between this check and method entry.
|
|
5918
|
+
// Owned-completion deliveries are NOT affected (they use followUp).
|
|
5919
|
+
if (this.#isTurnContinuationBlocked()) {
|
|
5920
|
+
skip("terminal_turn");
|
|
5921
|
+
return;
|
|
5922
|
+
}
|
|
5923
|
+
// A continuation scheduled before a handoff engaged must not start a
|
|
5924
|
+
// turn against the session being handed off (or the restored
|
|
5925
|
+
// predecessor). rearmIdle / normal delivery resumes after the fence.
|
|
5926
|
+
if (this.#handoffTransitionActive) {
|
|
5927
|
+
skip("handoff_in_progress");
|
|
5928
|
+
return;
|
|
5929
|
+
}
|
|
5930
|
+
const predecessorAgentEnd =
|
|
5931
|
+
this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
|
|
5932
|
+
let predecessorAccepted = false;
|
|
5933
|
+
const releasePredecessor = () => {
|
|
5934
|
+
if (predecessorAccepted) return;
|
|
5935
|
+
predecessorAccepted = true;
|
|
5936
|
+
this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
|
|
5937
|
+
};
|
|
5938
|
+
const hasQueuedMessages = this.agent.hasQueuedMessages();
|
|
5939
|
+
const startsQueuedSuccessor =
|
|
5940
|
+
hasQueuedMessages &&
|
|
5941
|
+
(options?.continueQueuedOnly === true ||
|
|
5942
|
+
this.agent.state.messages.at(-1)?.role === "assistant");
|
|
5943
|
+
const continueQueued = options?.continueQueuedOnly
|
|
5944
|
+
? this.agent.continueQueuedMessages.bind(this.agent)
|
|
5945
|
+
: this.agent.continue.bind(this.agent);
|
|
5946
|
+
try {
|
|
5947
|
+
await continueQueued({
|
|
5948
|
+
...this.#managedFallbackPromptOptions(),
|
|
5949
|
+
maintenanceContinuation: options?.maintenanceContinuation,
|
|
5950
|
+
// Reset only after continue() has claimed the queued turn. Skipped or stale
|
|
5951
|
+
// continuations retain predecessor accounting, and resetAttemptBudget keeps
|
|
5952
|
+
// the sticky fallback cursor unchanged.
|
|
5953
|
+
onRunAccepted: (handle: AttemptRunHandle, acceptance) => {
|
|
5954
|
+
this.#fireQueuedPromotionHooks(acceptance.consumedQueuedMessages);
|
|
5955
|
+
for (const message of acceptance.consumedQueuedMessages) {
|
|
5956
|
+
const sdkRunToken = this.#sdkRunTokensByQueuedMessage.get(message);
|
|
5957
|
+
if (sdkRunToken) {
|
|
5958
|
+
this.#sdkRunTokensByAttemptScope.set(handle.scope, sdkRunToken);
|
|
5959
|
+
break;
|
|
5960
|
+
}
|
|
5961
|
+
}
|
|
5962
|
+
this.#acceptRunHandle(handle);
|
|
5963
|
+
options?.onRunAccepted?.(handle);
|
|
5964
|
+
settleLease();
|
|
5965
|
+
releasePredecessor();
|
|
5966
|
+
if (startsQueuedSuccessor) {
|
|
5967
|
+
this.#defaultFallbackChain().resetAttemptBudget();
|
|
5968
|
+
this.#overflowMaintenanceAttempts = 0;
|
|
5969
|
+
}
|
|
5970
|
+
},
|
|
5971
|
+
});
|
|
5972
|
+
releasePredecessor();
|
|
5973
|
+
} catch (error) {
|
|
5974
|
+
if (
|
|
5975
|
+
options?.rescheduleOnBusy &&
|
|
5976
|
+
this.#isAgentBusyError(error) &&
|
|
5977
|
+
!predecessorAccepted &&
|
|
5978
|
+
!terminalized &&
|
|
5979
|
+
canContinue()
|
|
5980
|
+
) {
|
|
5981
|
+
if (busyReschedules < AGENT_CONTINUE_BUSY_MAX_RESCHEDULES) {
|
|
5982
|
+
busyReschedules += 1;
|
|
5983
|
+
const nextDelayMs = agentContinueBusyRescheduleDelayMs(busyReschedules);
|
|
5984
|
+
this.#retainDeferredAgentEndAfterContinuationBusy(
|
|
5985
|
+
predecessorAgentEndHold,
|
|
5986
|
+
predecessorAgentEnd,
|
|
5987
|
+
);
|
|
5988
|
+
logger.debug("agent.continue busy after scheduling; rescheduling", {
|
|
5989
|
+
error: error.message,
|
|
5990
|
+
attempt: busyReschedules,
|
|
5991
|
+
delayMs: nextDelayMs,
|
|
5992
|
+
});
|
|
5993
|
+
void scheduleAttempt(nextDelayMs);
|
|
5994
|
+
return;
|
|
5995
|
+
}
|
|
5996
|
+
logger.warn("agent.continue busy reschedule budget exhausted; giving up", {
|
|
5997
|
+
attempts: busyReschedules,
|
|
5998
|
+
error: error.message,
|
|
5999
|
+
});
|
|
6000
|
+
}
|
|
6001
|
+
if (!predecessorAccepted)
|
|
6002
|
+
this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
|
|
6003
|
+
throw error;
|
|
6004
|
+
}
|
|
6005
|
+
} catch (error) {
|
|
6006
|
+
fail(error);
|
|
6007
|
+
} finally {
|
|
6008
|
+
settleLease();
|
|
6009
|
+
}
|
|
6010
|
+
},
|
|
6011
|
+
scheduledSignal,
|
|
6012
|
+
continuationAdmission,
|
|
6013
|
+
{
|
|
6014
|
+
bypassSelectionFenceGeneration: selectionFenceGeneration,
|
|
6015
|
+
allowPromptContinuationReentry: true,
|
|
6016
|
+
},
|
|
6017
|
+
);
|
|
5043
6018
|
} catch (error) {
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
settleLease();
|
|
6019
|
+
if (scheduledSignal.aborted) skip("aborted_signal");
|
|
6020
|
+
else fail(error);
|
|
5047
6021
|
}
|
|
5048
6022
|
},
|
|
5049
6023
|
{
|
|
@@ -5054,12 +6028,12 @@ export class AgentSession {
|
|
|
5054
6028
|
},
|
|
5055
6029
|
resourceRunId: options?.resourceRunId,
|
|
5056
6030
|
leaseTask: leaseSettlement.promise,
|
|
6031
|
+
selectionFenceGeneration,
|
|
5057
6032
|
},
|
|
5058
6033
|
);
|
|
5059
6034
|
};
|
|
5060
6035
|
return scheduleAttempt();
|
|
5061
6036
|
}
|
|
5062
|
-
|
|
5063
6037
|
#logCompactionContinuationSkipped(
|
|
5064
6038
|
source: "auto_continue_prompt" | "queued_continue" | "overflow_retry",
|
|
5065
6039
|
reason: string,
|
|
@@ -5120,7 +6094,6 @@ export class AgentSession {
|
|
|
5120
6094
|
generation,
|
|
5121
6095
|
suppressPredecessorAgentEnd: true,
|
|
5122
6096
|
resourceRunId,
|
|
5123
|
-
rescheduleOnBusy: true,
|
|
5124
6097
|
onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
|
|
5125
6098
|
onError: error => this.#logCompactionContinuationError("overflow_retry", error),
|
|
5126
6099
|
});
|
|
@@ -5137,8 +6110,13 @@ export class AgentSession {
|
|
|
5137
6110
|
return false;
|
|
5138
6111
|
}
|
|
5139
6112
|
|
|
5140
|
-
#scheduleAutoContinuePrompt(
|
|
5141
|
-
|
|
6113
|
+
#scheduleAutoContinuePrompt(
|
|
6114
|
+
generation: number,
|
|
6115
|
+
requireUnfinishedWork = true,
|
|
6116
|
+
resourceRunId?: string,
|
|
6117
|
+
deferredSelectionFenceGeneration?: number,
|
|
6118
|
+
deferredPredecessorAgentEnd?: AgentSessionEvent,
|
|
6119
|
+
): void {
|
|
5142
6120
|
const scheduledGeneration = generation;
|
|
5143
6121
|
const continuationAuthorized = async (
|
|
5144
6122
|
signal: AbortSignal,
|
|
@@ -5156,6 +6134,13 @@ export class AgentSession {
|
|
|
5156
6134
|
);
|
|
5157
6135
|
return false;
|
|
5158
6136
|
}
|
|
6137
|
+
// A same-turn auto-continue of a terminally aborted turn is denied
|
|
6138
|
+
// (corrected turn semantics); owned-completion deliveries are not
|
|
6139
|
+
// affected — they flow through followUp as fresh turns.
|
|
6140
|
+
if (this.#isTurnContinuationBlocked()) {
|
|
6141
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "terminal_turn");
|
|
6142
|
+
return false;
|
|
6143
|
+
}
|
|
5159
6144
|
const authorized = requireUnfinishedWork
|
|
5160
6145
|
? this.#hasUnfinishedWork(snapshot) || hasPendingNextTurnMessages
|
|
5161
6146
|
: snapshot.queuedMessages ||
|
|
@@ -5165,81 +6150,95 @@ export class AgentSession {
|
|
|
5165
6150
|
if (!authorized) this.emitNotice("info", "Auto-continue skipped: no unfinished work detected");
|
|
5166
6151
|
return authorized;
|
|
5167
6152
|
};
|
|
5168
|
-
|
|
5169
|
-
const
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
if (
|
|
5207
|
-
|
|
5208
|
-
const nextDelayMs = agentContinueBusyRescheduleDelayMs(busyReschedules);
|
|
5209
|
-
logger.debug("Auto-compaction continuation busy; rescheduling", {
|
|
5210
|
-
source: "auto_continue_prompt",
|
|
5211
|
-
reason: "queue_drained",
|
|
5212
|
-
error: error.message,
|
|
5213
|
-
attempt: busyReschedules,
|
|
5214
|
-
delayMs: nextDelayMs,
|
|
5215
|
-
});
|
|
5216
|
-
rescheduled = true;
|
|
5217
|
-
scheduleAttempt(nextDelayMs);
|
|
6153
|
+
const continuationAdmission = this.#captureScheduledContinuationAdmission();
|
|
6154
|
+
const selectionFenceGeneration =
|
|
6155
|
+
deferredSelectionFenceGeneration ??
|
|
6156
|
+
this.#selectionFenceGenerationContext.getStore() ??
|
|
6157
|
+
this.#sessionAdmissionContext.getStore()?.selectionFenceGeneration ??
|
|
6158
|
+
continuationAdmission?.entry.selectionFenceGeneration ??
|
|
6159
|
+
this.#selectionFenceGeneration;
|
|
6160
|
+
if (this.#pendingSelectionFences > 0 && selectionFenceGeneration === this.#selectionFenceGeneration) {
|
|
6161
|
+
const predecessorAgentEnd =
|
|
6162
|
+
deferredPredecessorAgentEnd ??
|
|
6163
|
+
this.#claimDeferredAgentEndForContinuation(this.#reserveDeferredAgentEndForContinuation());
|
|
6164
|
+
const precedingSelectionFence = this.#selectionFenceTail;
|
|
6165
|
+
const deferredScheduling = precedingSelectionFence.then(() => {
|
|
6166
|
+
try {
|
|
6167
|
+
this.#scheduleAutoContinuePrompt(
|
|
6168
|
+
generation,
|
|
6169
|
+
requireUnfinishedWork,
|
|
6170
|
+
resourceRunId,
|
|
6171
|
+
selectionFenceGeneration,
|
|
6172
|
+
predecessorAgentEnd,
|
|
6173
|
+
);
|
|
6174
|
+
} finally {
|
|
6175
|
+
this.#endSelectionFenceDeferralTracking(selectionFenceGeneration);
|
|
6176
|
+
}
|
|
6177
|
+
});
|
|
6178
|
+
this.#trackSelectionFenceDeferredContinuation(selectionFenceGeneration, deferredScheduling);
|
|
6179
|
+
return;
|
|
6180
|
+
}
|
|
6181
|
+
const predecessorAgentEndHold = deferredPredecessorAgentEnd
|
|
6182
|
+
? this.#restoreAndReserveDeferredAgentEndForContinuation(deferredPredecessorAgentEnd)
|
|
6183
|
+
: this.#reserveDeferredAgentEndForContinuation();
|
|
6184
|
+
void this.#schedulePostPromptTask(
|
|
6185
|
+
async signal => {
|
|
6186
|
+
try {
|
|
6187
|
+
await this.#withSessionAdmission(
|
|
6188
|
+
"prompt",
|
|
6189
|
+
async () => {
|
|
6190
|
+
await Promise.resolve();
|
|
6191
|
+
if (signal.aborted) {
|
|
6192
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
5218
6193
|
return;
|
|
5219
6194
|
}
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
6195
|
+
if (this.#promptGeneration !== scheduledGeneration) {
|
|
6196
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
|
|
6197
|
+
return;
|
|
6198
|
+
}
|
|
6199
|
+
if (!(await continuationAuthorized(signal))) return;
|
|
6200
|
+
await this.#promptWithMessage(
|
|
6201
|
+
{
|
|
6202
|
+
role: "developer",
|
|
6203
|
+
content: [{ type: "text", text: autoContinuePrompt }],
|
|
6204
|
+
attribution: "agent",
|
|
6205
|
+
timestamp: Date.now(),
|
|
6206
|
+
},
|
|
6207
|
+
autoContinuePrompt,
|
|
6208
|
+
{
|
|
6209
|
+
skipPostPromptRecoveryWait: true,
|
|
6210
|
+
skipCompactionCheck: true,
|
|
6211
|
+
predecessorAgentEndHold,
|
|
6212
|
+
onFinalPreflight: ({ hasPendingNextTurnMessages }) =>
|
|
6213
|
+
continuationAuthorized(signal, hasPendingNextTurnMessages),
|
|
6214
|
+
},
|
|
6215
|
+
);
|
|
6216
|
+
},
|
|
6217
|
+
signal,
|
|
6218
|
+
continuationAdmission,
|
|
6219
|
+
{ bypassSelectionFenceGeneration: selectionFenceGeneration },
|
|
6220
|
+
);
|
|
6221
|
+
} catch (error) {
|
|
6222
|
+
if (signal.aborted) {
|
|
5236
6223
|
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
5237
|
-
|
|
5238
|
-
}
|
|
6224
|
+
return;
|
|
6225
|
+
}
|
|
6226
|
+
this.#logCompactionContinuationError("auto_continue_prompt", error);
|
|
6227
|
+
} finally {
|
|
6228
|
+
this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
6229
|
+
}
|
|
6230
|
+
},
|
|
6231
|
+
{
|
|
6232
|
+
delayMs: 0,
|
|
6233
|
+
generation: scheduledGeneration,
|
|
6234
|
+
resourceRunId,
|
|
6235
|
+
selectionFenceGeneration,
|
|
6236
|
+
onSkip: () => {
|
|
6237
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
6238
|
+
this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
5239
6239
|
},
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
scheduleAttempt();
|
|
6240
|
+
},
|
|
6241
|
+
);
|
|
5243
6242
|
}
|
|
5244
6243
|
|
|
5245
6244
|
async #cancelPostPromptTasks(): Promise<void> {
|
|
@@ -5265,6 +6264,7 @@ export class AgentSession {
|
|
|
5265
6264
|
this.#postPromptTasksAbortController.abort();
|
|
5266
6265
|
this.#postPromptTasksAbortController = new AbortController();
|
|
5267
6266
|
this.#postPromptTasks.clear();
|
|
6267
|
+
this.#postPromptTaskSelectionFenceGenerations.clear();
|
|
5268
6268
|
this.#releaseDeferredAgentEndContinuations();
|
|
5269
6269
|
this.#resolveTtsrResume();
|
|
5270
6270
|
this.#resolvePostPromptTasks();
|
|
@@ -5301,6 +6301,19 @@ export class AgentSession {
|
|
|
5301
6301
|
}
|
|
5302
6302
|
}
|
|
5303
6303
|
|
|
6304
|
+
/**
|
|
6305
|
+
* A selection's scoped idle drain must not wait on work parked behind its
|
|
6306
|
+
* own fence, but it still needs to yield to post-prompt work that predates
|
|
6307
|
+
* that fence. Otherwise the drain can repeatedly await already-settled
|
|
6308
|
+
* session work while a timer-backed predecessor task is starved.
|
|
6309
|
+
*/
|
|
6310
|
+
async #waitForPostPromptTasksBeforeSelectionFence(selectionFenceGeneration: number): Promise<void> {
|
|
6311
|
+
const precedingTasks = [...this.#postPromptTasks].filter(
|
|
6312
|
+
task => (this.#postPromptTaskSelectionFenceGenerations.get(task) ?? 0) < selectionFenceGeneration,
|
|
6313
|
+
);
|
|
6314
|
+
if (precedingTasks.length > 0) await Promise.allSettled(precedingTasks);
|
|
6315
|
+
}
|
|
6316
|
+
|
|
5304
6317
|
/** Get TTSR injection payload and clear pending injections. */
|
|
5305
6318
|
#getTtsrInjectionContent(): { content: string; rules: Rule[] } | undefined {
|
|
5306
6319
|
if (this.#pendingTtsrInjections.length === 0) return undefined;
|
|
@@ -5659,6 +6672,13 @@ export class AgentSession {
|
|
|
5659
6672
|
}
|
|
5660
6673
|
}
|
|
5661
6674
|
|
|
6675
|
+
#isProvisionalStreamingToolEvent(event: AgentEvent): boolean {
|
|
6676
|
+
if (event.type !== "message_update") return false;
|
|
6677
|
+
const contentIndex = event.assistantMessageEvent.contentIndex ?? 0;
|
|
6678
|
+
const block = event.message.role === "assistant" ? event.message.content[contentIndex] : undefined;
|
|
6679
|
+
return block?.type === "toolCall" && this.#provisionalStreamingToolCallIds.has(block.id);
|
|
6680
|
+
}
|
|
6681
|
+
|
|
5662
6682
|
#streamingEditPrecachePending = new Set<string>();
|
|
5663
6683
|
async #preCacheFileAsync(resolvedPath: string): Promise<void> {
|
|
5664
6684
|
if (this.#streamingEditFileCache.has(resolvedPath)) return;
|
|
@@ -6005,7 +7025,16 @@ export class AgentSession {
|
|
|
6005
7025
|
try {
|
|
6006
7026
|
if (event.type === "agent_start") {
|
|
6007
7027
|
this.#turnIndex = 0;
|
|
6008
|
-
await this.#extensionRunner.emit(
|
|
7028
|
+
await this.#extensionRunner.emit(
|
|
7029
|
+
{
|
|
7030
|
+
type: "agent_start",
|
|
7031
|
+
...(deliveryScope
|
|
7032
|
+
? { sdkRunToken: this.#sdkRunTokensByAttemptScope.get(deliveryScope as AttemptScope) }
|
|
7033
|
+
: {}),
|
|
7034
|
+
},
|
|
7035
|
+
undefined,
|
|
7036
|
+
deliveryScope,
|
|
7037
|
+
);
|
|
6009
7038
|
} else if (event.type === "agent_end") {
|
|
6010
7039
|
await this.#extensionRunner.emit(
|
|
6011
7040
|
{
|
|
@@ -6243,6 +7272,81 @@ export class AgentSession {
|
|
|
6243
7272
|
);
|
|
6244
7273
|
}
|
|
6245
7274
|
|
|
7275
|
+
/**
|
|
7276
|
+
* Re-register the session's AsyncJobManager under the successor endpoint
|
|
7277
|
+
* id after a committed session-identity transition (newSession, fork,
|
|
7278
|
+
* handoff, branch, switchSession). Tool-side lineage bindings made after
|
|
7279
|
+
* the transition use the successor id; the process-global endpoint
|
|
7280
|
+
* registry must follow, otherwise AsyncJobManager.endpointIdOf() keeps
|
|
7281
|
+
* resolving the predecessor and a queued subagent resume can neither
|
|
7282
|
+
* resolve its lineage nor register its owned tuple (review thread P1).
|
|
7283
|
+
*/
|
|
7284
|
+
#asyncJobEndpointId(sessionId: string, sessionFile: string | undefined): string {
|
|
7285
|
+
return deriveAsyncJobEndpointId(this.#asyncJobProviderSessionId, sessionId, sessionFile);
|
|
7286
|
+
}
|
|
7287
|
+
|
|
7288
|
+
#assertJobManagerEndpointAdmission(successorSessionId: string, successorSessionFile: string | undefined): void {
|
|
7289
|
+
const successorEndpointId = this.#asyncJobEndpointId(successorSessionId, successorSessionFile);
|
|
7290
|
+
const ownManager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
7291
|
+
const successorOwner = AsyncJobManager.forEndpoint(successorEndpointId);
|
|
7292
|
+
if (ownManager && successorOwner !== undefined && successorOwner !== ownManager) {
|
|
7293
|
+
throw new Error(
|
|
7294
|
+
`Session identity transition rejected: endpoint "${successorEndpointId}" is owned by another live session's job manager.`,
|
|
7295
|
+
);
|
|
7296
|
+
}
|
|
7297
|
+
}
|
|
7298
|
+
|
|
7299
|
+
#rekeyJobManagerForSessionIdentity(
|
|
7300
|
+
previousSessionId: string,
|
|
7301
|
+
previousSessionFile: string | undefined,
|
|
7302
|
+
options: { retirePredecessorRegistrations?: boolean } = {},
|
|
7303
|
+
): void {
|
|
7304
|
+
const previousEndpointId = this.#asyncJobEndpointId(previousSessionId, previousSessionFile);
|
|
7305
|
+
const currentEndpointId = this.#asyncJobEndpointId(
|
|
7306
|
+
this.sessionManager.getSessionId(),
|
|
7307
|
+
this.sessionManager.getSessionFile(),
|
|
7308
|
+
);
|
|
7309
|
+
const predecessorOwner = AsyncJobManager.forEndpoint(previousEndpointId);
|
|
7310
|
+
// Rekey and retire ONLY when the predecessor key belongs to THIS
|
|
7311
|
+
// session's manager (the session-owned manager, else the process-global
|
|
7312
|
+
// fallback the session uses). A predecessor key held by a FOREIGN
|
|
7313
|
+
// manager means another live session currently carries this id —
|
|
7314
|
+
// neither the mapping nor the tuples keyed under it are ours to move
|
|
7315
|
+
// or retire (review thread P1).
|
|
7316
|
+
const ownManager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
7317
|
+
if (predecessorOwner !== undefined && predecessorOwner !== ownManager) return;
|
|
7318
|
+
if (predecessorOwner !== undefined) {
|
|
7319
|
+
const rekeyed = AsyncJobManager.rekeyForEndpoint(previousEndpointId, currentEndpointId, predecessorOwner);
|
|
7320
|
+
if (!rekeyed) {
|
|
7321
|
+
// The successor endpoint is owned by a FOREIGN live manager. A
|
|
7322
|
+
// silent no-op would leave THIS manager registered under the
|
|
7323
|
+
// predecessor while tools resolve the successor to the foreign
|
|
7324
|
+
// manager — jobs queried or registered in the wrong session and
|
|
7325
|
+
// owned aborts losing their causal set — and retiring the
|
|
7326
|
+
// predecessor tuples afterwards would destroy the registration
|
|
7327
|
+
// evidence of this session's still-live owned work. Fail the
|
|
7328
|
+
// transition: every caller routes throws into the existing
|
|
7329
|
+
// rollback (discardPreparedNewSessionAfterFailure /
|
|
7330
|
+
// restoreState) BEFORE any predecessor state is retired
|
|
7331
|
+
// (review thread P1).
|
|
7332
|
+
throw new Error(
|
|
7333
|
+
`Session identity transition rejected: endpoint "${currentEndpointId}" is owned by another live session's job manager.`,
|
|
7334
|
+
);
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7337
|
+
// Owned registrations created before the transition are keyed by the
|
|
7338
|
+
// PREDECESSOR endpoint. The transitions settle/cancel the session's
|
|
7339
|
+
// owned jobs before committing identity, so no live delivery boundary
|
|
7340
|
+
// depends on them; leaving them behind leaks tuples that disposal (now
|
|
7341
|
+
// keyed by the successor) can never retire, saturating the global
|
|
7342
|
+
// registry across repeated transitions. Retire them as part of the
|
|
7343
|
+
// rekey (review thread P2). A same-id transition (no-op rekey) keeps
|
|
7344
|
+
// the session's own live tuples.
|
|
7345
|
+
if (previousEndpointId !== currentEndpointId && options.retirePredecessorRegistrations !== false) {
|
|
7346
|
+
retireOwnedRegistrationsForEndpoint(previousEndpointId);
|
|
7347
|
+
}
|
|
7348
|
+
}
|
|
7349
|
+
|
|
6246
7350
|
#rekeyHindsightMemoryForCurrentSessionId(): void {
|
|
6247
7351
|
if (resolveMemoryBackendId(this.settings) !== "hindsight") return;
|
|
6248
7352
|
|
|
@@ -6314,7 +7418,13 @@ export class AgentSession {
|
|
|
6314
7418
|
await admissionClosed;
|
|
6315
7419
|
await this.#agentEndPublicationPromise;
|
|
6316
7420
|
await this.#queuedExtensionEvents;
|
|
6317
|
-
|
|
7421
|
+
// Drain the sidecar write order for the same reason the two queues above are
|
|
7422
|
+
// drained: each entry writes under the native identity-bound state-file lock, so a
|
|
7423
|
+
// still-queued write would run after the session that owns it is gone — releasing
|
|
7424
|
+
// a lock whose owner no longer exists, and under `bun test --isolate` calling into
|
|
7425
|
+
// the addon after the runtime tore down the context it was scheduled in. The chain
|
|
7426
|
+
// is already failure-absorbing, so this only waits.
|
|
7427
|
+
await this.#coordinatorPersistQueue;
|
|
6318
7428
|
this.#pendingBackgroundExchanges = [];
|
|
6319
7429
|
this.yieldQueue.clear();
|
|
6320
7430
|
|
|
@@ -6326,6 +7436,7 @@ export class AgentSession {
|
|
|
6326
7436
|
} catch (error) {
|
|
6327
7437
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
6328
7438
|
}
|
|
7439
|
+
this.#workflowGateEmitter?.fence?.();
|
|
6329
7440
|
this.#workflowGateEmitter = undefined;
|
|
6330
7441
|
this.#notifyWorkflowGateEmitterChanged(this.sessionId, undefined);
|
|
6331
7442
|
await this.#flushWorkerIntegrationAttempt();
|
|
@@ -6337,7 +7448,7 @@ export class AgentSession {
|
|
|
6337
7448
|
this.#cancelOwnAsyncJobs();
|
|
6338
7449
|
await Promise.allSettled(this.#deferredOwnerShutdownFinalizations);
|
|
6339
7450
|
const ownedAsyncManager = this.#ownedAsyncJobManager;
|
|
6340
|
-
if (ownedAsyncManager) {
|
|
7451
|
+
if (ownedAsyncManager && this.#disposeAsyncJobManager) {
|
|
6341
7452
|
const drained = await ownedAsyncManager.dispose({ timeoutMs: 3_000 });
|
|
6342
7453
|
const deliveryState = ownedAsyncManager.getDeliveryState();
|
|
6343
7454
|
if (drained === false && deliveryState) {
|
|
@@ -6392,6 +7503,7 @@ export class AgentSession {
|
|
|
6392
7503
|
await this.memoryBackend.dispose();
|
|
6393
7504
|
if (this.#workspaceTreeService) await this.#workspaceTreeService.dispose();
|
|
6394
7505
|
if (this.#networkPrewarmService) await this.#networkPrewarmService.dispose();
|
|
7506
|
+
this.#modelRegistry.authStorage?.releaseCredentialScope(this.credentialSessionId);
|
|
6395
7507
|
await this.sessionManager.close();
|
|
6396
7508
|
this.#closeAllProviderSessions("dispose");
|
|
6397
7509
|
const hindsightState = this.getHindsightSessionState();
|
|
@@ -6506,24 +7618,58 @@ export class AgentSession {
|
|
|
6506
7618
|
}
|
|
6507
7619
|
|
|
6508
7620
|
/** Wait until streaming and session settlement work are fully settled. */
|
|
6509
|
-
|
|
7621
|
+
/**
|
|
7622
|
+
* Wait until streaming and session settlement work are fully settled.
|
|
7623
|
+
*
|
|
7624
|
+
* The internal-only `ignoreSelectionFenceGeneration` is used exclusively by
|
|
7625
|
+
* `setDefaultModelSelection`'s mid-selection drain: the selection must not
|
|
7626
|
+
* wait on work parked behind its own (or a later) fence — its agent run,
|
|
7627
|
+
* recovery, and deferrals — because that work is waiting on the selection
|
|
7628
|
+
* itself (#4519). External callers observe everything.
|
|
7629
|
+
*/
|
|
7630
|
+
async waitForIdle(ignoreSelectionFenceGeneration?: number): Promise<void> {
|
|
6510
7631
|
while (true) {
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
7632
|
+
if (ignoreSelectionFenceGeneration === undefined) {
|
|
7633
|
+
await this.agent.waitForIdle();
|
|
7634
|
+
await this.#waitForPostPromptRecovery();
|
|
7635
|
+
} else {
|
|
7636
|
+
await this.#waitForPostPromptTasksBeforeSelectionFence(ignoreSelectionFenceGeneration);
|
|
7637
|
+
}
|
|
7638
|
+
await this.#waitForSessionSettlement(ignoreSelectionFenceGeneration);
|
|
6514
7639
|
if (
|
|
6515
7640
|
!this.agent.state.isStreaming &&
|
|
6516
7641
|
!this.#retryPromise &&
|
|
6517
7642
|
!this.#ttsrResumePromise &&
|
|
6518
7643
|
!this.#postPromptTasksPromise &&
|
|
6519
|
-
!this.#isSessionSettlementPending()
|
|
7644
|
+
!this.#isSessionSettlementPending(ignoreSelectionFenceGeneration)
|
|
6520
7645
|
)
|
|
6521
7646
|
return;
|
|
6522
7647
|
}
|
|
6523
7648
|
}
|
|
6524
7649
|
|
|
7650
|
+
/**
|
|
7651
|
+
* Deterministically await every in-flight agent-event handler (including the
|
|
7652
|
+
* synchronous canonical persistence each one performs) plus the durable
|
|
7653
|
+
* transcript flush, without waiting for a prompt/turn lifecycle.
|
|
7654
|
+
*
|
|
7655
|
+
* This is the explicit signal replacement for real-time settle sleeps after
|
|
7656
|
+
* externally emitted events (`agent.emitExternalEvent`): the dispatcher
|
|
7657
|
+
* increments `#agentEventHandlersInFlight` synchronously before this call can
|
|
7658
|
+
* observe it, and each handler decrements it in its `finally`, so the
|
|
7659
|
+
* settlement promise below resolves exactly when the last handler — and
|
|
7660
|
+
* therefore its `sessionManager.appendMessage`/`_persist` work — has finished.
|
|
7661
|
+
* `flush()` then queues deterministically behind the SessionManager persist
|
|
7662
|
+
* queue, so every seeded entry is canonical AND on disk before the caller
|
|
7663
|
+
* proceeds.
|
|
7664
|
+
*/
|
|
7665
|
+
async awaitSessionSettlement(): Promise<void> {
|
|
7666
|
+
await this.awaitPendingContextTransformations();
|
|
7667
|
+
await this.#waitForSessionSettlement();
|
|
7668
|
+
await this.sessionManager.flush();
|
|
7669
|
+
}
|
|
7670
|
+
|
|
6525
7671
|
async drainAsyncJobDeliveriesForAcp(options?: { timeoutMs?: number }): Promise<boolean> {
|
|
6526
|
-
const manager = AsyncJobManager.instance();
|
|
7672
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
6527
7673
|
if (!manager) return false;
|
|
6528
7674
|
const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
|
|
6529
7675
|
const before = manager.getDeliveryState(ownerFilter);
|
|
@@ -6547,7 +7693,7 @@ export class AgentSession {
|
|
|
6547
7693
|
* any remaining queued/delivering work is observable and can block mutation.
|
|
6548
7694
|
*/
|
|
6549
7695
|
getAsyncDeliveryStateForAcp(): { queued: number; delivering: boolean } {
|
|
6550
|
-
const manager = AsyncJobManager.instance();
|
|
7696
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
6551
7697
|
if (!manager) return { queued: 0, delivering: false };
|
|
6552
7698
|
const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
|
|
6553
7699
|
return manager.getDeliveryState(ownerFilter);
|
|
@@ -7105,9 +8251,15 @@ export class AgentSession {
|
|
|
7105
8251
|
activeSkillState: this.getActiveSkillState(),
|
|
7106
8252
|
sessionId: this.sessionManager.getSessionId(),
|
|
7107
8253
|
}),
|
|
8254
|
+
() => this.getSessionAgentDir(),
|
|
7108
8255
|
),
|
|
7109
8256
|
),
|
|
7110
8257
|
);
|
|
8258
|
+
// The object published into `agent.state.tools` — and therefore the object the
|
|
8259
|
+
// agent loop actually dispatches to — is this guard wrapper, not the registry
|
|
8260
|
+
// entry. A wrapper built from a proven built-in inherits that proof; one built
|
|
8261
|
+
// from a custom, MCP, or extension tool inherits nothing.
|
|
8262
|
+
if (this.#builtinToolIdentities.has(tool)) this.#builtinToolIdentities.add(wrapped);
|
|
7111
8263
|
if (!wrappersByVersion) {
|
|
7112
8264
|
wrappersByVersion = new Map();
|
|
7113
8265
|
this.#guardedToolWrapperCache.set(tool, wrappersByVersion);
|
|
@@ -7233,6 +8385,9 @@ export class AgentSession {
|
|
|
7233
8385
|
const refreshedTool = await this.#reloadSshTool();
|
|
7234
8386
|
if (refreshedTool) {
|
|
7235
8387
|
this.#toolRegistry.set(refreshedTool.name, refreshedTool);
|
|
8388
|
+
// A reloaded built-in is a NEW object. Without this it would replace the proven
|
|
8389
|
+
// one and every subsequent `ssh` call would be reported as `custom`.
|
|
8390
|
+
this.#builtinToolIdentities.add(refreshedTool);
|
|
7236
8391
|
} else {
|
|
7237
8392
|
this.#toolRegistry.delete("ssh");
|
|
7238
8393
|
this.#selectedDiscoveredToolNames.delete("ssh");
|
|
@@ -7834,6 +8989,36 @@ export class AgentSession {
|
|
|
7834
8989
|
return this.#credentialSessionId ?? this.sessionId;
|
|
7835
8990
|
}
|
|
7836
8991
|
|
|
8992
|
+
/** Pin one OAuth credential for this session scope and persist the minimal intent. */
|
|
8993
|
+
async setCredentialPin(provider: string, selector: AuthCredentialSelector): Promise<void> {
|
|
8994
|
+
const scopeId = this.credentialSessionId;
|
|
8995
|
+
const authStorage = this.#modelRegistry.authStorage;
|
|
8996
|
+
const target = authStorage.resolveOAuthPinTarget(provider, selector);
|
|
8997
|
+
authStorage.setSessionCredentialSelector(scopeId, provider, target.canonicalSelector);
|
|
8998
|
+
if (target.canonicalSelector.kind === "id" && !this.#credentialStoreIdentity) return;
|
|
8999
|
+
this.sessionManager.appendCustomEntry("auth-credential-pin", {
|
|
9000
|
+
v: 1,
|
|
9001
|
+
scopeId,
|
|
9002
|
+
provider,
|
|
9003
|
+
pin: target.canonicalSelector,
|
|
9004
|
+
...(target.canonicalSelector.kind === "id" && this.#credentialStoreIdentity
|
|
9005
|
+
? { credentialStoreIdentity: this.#credentialStoreIdentity }
|
|
9006
|
+
: {}),
|
|
9007
|
+
});
|
|
9008
|
+
}
|
|
9009
|
+
|
|
9010
|
+
/** Mask persistent/global selection for this session and restore AUTO ranking. */
|
|
9011
|
+
async setCredentialAuto(provider: string): Promise<void> {
|
|
9012
|
+
const scopeId = this.credentialSessionId;
|
|
9013
|
+
this.#modelRegistry.authStorage.setSessionCredentialAuto(provider, scopeId);
|
|
9014
|
+
this.sessionManager.appendCustomEntry("auth-credential-pin", {
|
|
9015
|
+
v: 1,
|
|
9016
|
+
scopeId,
|
|
9017
|
+
provider,
|
|
9018
|
+
pin: { auto: true },
|
|
9019
|
+
});
|
|
9020
|
+
}
|
|
9021
|
+
|
|
7837
9022
|
/** Current session display name, if set */
|
|
7838
9023
|
get sessionName(): string | undefined {
|
|
7839
9024
|
return this.sessionManager.getSessionName();
|
|
@@ -7887,8 +9072,12 @@ export class AgentSession {
|
|
|
7887
9072
|
async invokeSkill(
|
|
7888
9073
|
name: string,
|
|
7889
9074
|
args = "",
|
|
7890
|
-
options?: Pick<
|
|
9075
|
+
options?: Pick<
|
|
9076
|
+
PromptOptions,
|
|
9077
|
+
"onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared" | "preflightSignal"
|
|
9078
|
+
>,
|
|
7891
9079
|
): Promise<{ name: string; path: string; args?: string; lineCount?: number }> {
|
|
9080
|
+
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
7892
9081
|
const skillName = name.trim();
|
|
7893
9082
|
if (!skillName) throw Object.assign(new Error("skill.invoke requires a skill name."), { code: "invalid_input" });
|
|
7894
9083
|
if (typeof args !== "string")
|
|
@@ -7900,18 +9089,24 @@ export class AgentSession {
|
|
|
7900
9089
|
throw Object.assign(new Error(`Skill ${skillName} was not found.${availableHint}`), { code: "invalid_input" });
|
|
7901
9090
|
}
|
|
7902
9091
|
const deepInterviewUserIntentEpoch = this.#claimDeepInterviewUserIntent();
|
|
7903
|
-
const activation = await
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
9092
|
+
const activation = await awaitPromptInvocationPreflight(
|
|
9093
|
+
resolveSubskillActivationForSkillInvocation({
|
|
9094
|
+
cwd: this.sessionManager.getCwd(),
|
|
9095
|
+
sessionId: this.sessionId,
|
|
9096
|
+
skillName: skill.name,
|
|
9097
|
+
args,
|
|
9098
|
+
}),
|
|
9099
|
+
options?.preflightSignal,
|
|
9100
|
+
);
|
|
9101
|
+
const built = await awaitPromptInvocationPreflight(
|
|
9102
|
+
buildSkillPromptMessage(skill, activation.cleanedArgs, {
|
|
9103
|
+
subskillActivation: activation.activation,
|
|
9104
|
+
subskillActivationSet: activation.activeSubskillsToPersist,
|
|
9105
|
+
cwd: this.sessionManager.getCwd(),
|
|
9106
|
+
sessionId: this.sessionId,
|
|
9107
|
+
}),
|
|
9108
|
+
options?.preflightSignal,
|
|
9109
|
+
);
|
|
7915
9110
|
const skillPromptMessage = {
|
|
7916
9111
|
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
7917
9112
|
content: built.message,
|
|
@@ -7920,6 +9115,7 @@ export class AgentSession {
|
|
|
7920
9115
|
attribution: "user" as const,
|
|
7921
9116
|
};
|
|
7922
9117
|
this.#deepInterviewPreclaimedCustomInputEpochs.set(skillPromptMessage, deepInterviewUserIntentEpoch);
|
|
9118
|
+
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
7923
9119
|
options?.onSkillPrepared?.({
|
|
7924
9120
|
name: skill.name,
|
|
7925
9121
|
path: skill.filePath,
|
|
@@ -8153,6 +9349,11 @@ export class AgentSession {
|
|
|
8153
9349
|
const wrappedTool = wrapToolWithMetaNotice(createdAskTool as unknown as AgentTool);
|
|
8154
9350
|
askTool = this.#extensionRunner ? new ExtensionToolWrapper(wrappedTool, this.#extensionRunner) : wrappedTool;
|
|
8155
9351
|
this.#toolRegistry.set(askTool.name, askTool);
|
|
9352
|
+
// Built here from the built-in `ask` descriptor rather than by the session
|
|
9353
|
+
// builder, so its provenance has to be recorded here too. Only this
|
|
9354
|
+
// construction path qualifies: an `ask` already in the registry is left alone,
|
|
9355
|
+
// because whatever put it there owns its provenance.
|
|
9356
|
+
this.#builtinToolIdentities.add(askTool);
|
|
8156
9357
|
}
|
|
8157
9358
|
|
|
8158
9359
|
try {
|
|
@@ -8601,6 +9802,8 @@ export class AgentSession {
|
|
|
8601
9802
|
*/
|
|
8602
9803
|
async prompt(text: string, options?: PromptOptions): Promise<void> {
|
|
8603
9804
|
this.#assertRecoveryHydrationPromoted();
|
|
9805
|
+
const owner = this.#sessionAdmissionContext.getStore();
|
|
9806
|
+
if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
|
|
8604
9807
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
8605
9808
|
|
|
8606
9809
|
if (expandPromptTemplates && text.startsWith("/skill:") && !options?.images?.length) {
|
|
@@ -8612,12 +9815,13 @@ export class AgentSession {
|
|
|
8612
9815
|
await this.invokeSkill(
|
|
8613
9816
|
invocation.skill.name,
|
|
8614
9817
|
invocation.args,
|
|
8615
|
-
options?.onPreflightAccepted || options?.onPreflightAcceptCommit
|
|
9818
|
+
options?.onPreflightAccepted || options?.onPreflightAcceptCommit || options?.preflightSignal
|
|
8616
9819
|
? {
|
|
8617
9820
|
...(options.onPreflightAccepted ? { onPreflightAccepted: options.onPreflightAccepted } : {}),
|
|
8618
9821
|
...(options.onPreflightAcceptCommit
|
|
8619
9822
|
? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
|
|
8620
9823
|
: {}),
|
|
9824
|
+
...(options.preflightSignal ? { preflightSignal: options.preflightSignal } : {}),
|
|
8621
9825
|
}
|
|
8622
9826
|
: undefined,
|
|
8623
9827
|
);
|
|
@@ -8654,6 +9858,13 @@ export class AgentSession {
|
|
|
8654
9858
|
assertImagePlaceholdersHavePayload(expandedText, options?.images);
|
|
8655
9859
|
const workflowIntentDiff = options?.synthetic ? null : buildWorkflowIntentDiff(expandedText);
|
|
8656
9860
|
const claimsGenuineUserIntent = !options?.synthetic && options?.attribution !== "agent";
|
|
9861
|
+
const admissionGeneration = this.#promptGeneration;
|
|
9862
|
+
const admissionSignal = options?.preflightSignal
|
|
9863
|
+
? AbortSignal.any([this.#promptPreflightAbortController.signal, options.preflightSignal])
|
|
9864
|
+
: this.#promptPreflightAbortController.signal;
|
|
9865
|
+
if (this.#pendingSelectionFences > 0) {
|
|
9866
|
+
await awaitPromptInvocationPreflight(this.#selectionFenceTail, admissionSignal);
|
|
9867
|
+
}
|
|
8657
9868
|
const deepInterviewUserIntentEpoch =
|
|
8658
9869
|
claimsGenuineUserIntent && !this.isStreaming ? this.#claimDeepInterviewUserIntent() : undefined;
|
|
8659
9870
|
|
|
@@ -8678,64 +9889,67 @@ export class AgentSession {
|
|
|
8678
9889
|
return;
|
|
8679
9890
|
}
|
|
8680
9891
|
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
// Skip eager todo prelude when the user has already queued a directive
|
|
8690
|
-
const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
|
|
8691
|
-
const eagerTodoPrelude =
|
|
8692
|
-
!options?.synthetic && !hasPendingUserDirective ? this.#createEagerTodoPrelude(expandedText) : undefined;
|
|
9892
|
+
await this.#withSessionAdmission(
|
|
9893
|
+
"prompt",
|
|
9894
|
+
async admission => {
|
|
9895
|
+
this.#throwIfPromptPreflightCancelled(admissionGeneration, admissionSignal);
|
|
9896
|
+
if (workflowIntentDiff) {
|
|
9897
|
+
this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
|
|
9898
|
+
}
|
|
8693
9899
|
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
9900
|
+
// Skip eager todo prelude when the user has already queued a directive
|
|
9901
|
+
const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
|
|
9902
|
+
const eagerTodoPrelude =
|
|
9903
|
+
!options?.synthetic && !hasPendingUserDirective ? this.#createEagerTodoPrelude(expandedText) : undefined;
|
|
8698
9904
|
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
content: userContent,
|
|
8704
|
-
attribution: promptAttribution,
|
|
8705
|
-
timestamp: Date.now(),
|
|
8706
|
-
}
|
|
8707
|
-
: { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
|
|
8708
|
-
if (deepInterviewUserIntentEpoch !== undefined)
|
|
8709
|
-
this.#deepInterviewGenuineUserMessageEpochs.set(message, deepInterviewUserIntentEpoch);
|
|
8710
|
-
await this.refreshGjcSubskillTools();
|
|
9905
|
+
const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
|
|
9906
|
+
if (options?.images) {
|
|
9907
|
+
userContent.push(...options.images);
|
|
9908
|
+
}
|
|
8711
9909
|
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
9910
|
+
const promptAttribution = options?.attribution ?? (options?.synthetic ? "agent" : "user");
|
|
9911
|
+
const message = options?.synthetic
|
|
9912
|
+
? {
|
|
9913
|
+
role: "developer" as const,
|
|
9914
|
+
content: userContent,
|
|
9915
|
+
attribution: promptAttribution,
|
|
9916
|
+
timestamp: Date.now(),
|
|
9917
|
+
}
|
|
9918
|
+
: { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
|
|
9919
|
+
if (deepInterviewUserIntentEpoch !== undefined)
|
|
9920
|
+
this.#deepInterviewGenuineUserMessageEpochs.set(message, deepInterviewUserIntentEpoch);
|
|
9921
|
+
await this.refreshGjcSubskillTools();
|
|
9922
|
+
|
|
9923
|
+
if (eagerTodoPrelude?.toolChoice) {
|
|
9924
|
+
this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
|
|
9925
|
+
label: "eager-todo",
|
|
9926
|
+
});
|
|
9927
|
+
}
|
|
8717
9928
|
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
9929
|
+
try {
|
|
9930
|
+
await this.#promptWithMessage(message, expandedText, {
|
|
9931
|
+
...options,
|
|
9932
|
+
prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
|
|
9933
|
+
admissionLease: admission,
|
|
9934
|
+
resetRetryReplaySafety: true,
|
|
9935
|
+
});
|
|
9936
|
+
} finally {
|
|
9937
|
+
// Clean up residual eager-todo directive if the prompt never consumed it
|
|
9938
|
+
// (e.g., compaction aborted, validation failed).
|
|
9939
|
+
this.#toolChoiceQueue.removeByLabel("eager-todo");
|
|
9940
|
+
}
|
|
9941
|
+
if (!options?.synthetic) {
|
|
9942
|
+
await this.#enforcePlanModeToolDecision();
|
|
9943
|
+
}
|
|
9944
|
+
},
|
|
9945
|
+
options?.preflightSignal,
|
|
9946
|
+
);
|
|
8734
9947
|
}
|
|
8735
9948
|
|
|
8736
9949
|
async #syncSkillPromptActiveState(
|
|
8737
9950
|
message: Pick<CustomMessage<unknown>, "customType" | "details">,
|
|
8738
9951
|
active: boolean,
|
|
9952
|
+
persistActiveState = true,
|
|
8739
9953
|
): Promise<void> {
|
|
8740
9954
|
if (message.customType !== SKILL_PROMPT_MESSAGE_TYPE) return;
|
|
8741
9955
|
const details = message.details;
|
|
@@ -8759,8 +9973,12 @@ export class AgentSession {
|
|
|
8759
9973
|
// initial mode-state + active row only when the skill is not already
|
|
8760
9974
|
// active, so the mutation guard and Stop hook engage immediately instead
|
|
8761
9975
|
// of relying on the skill prompt to run its own state-init steps.
|
|
8762
|
-
if (active) {
|
|
8763
|
-
await ensureWorkflowSkillActivationState({
|
|
9976
|
+
if (active && persistActiveState) {
|
|
9977
|
+
await ensureWorkflowSkillActivationState({
|
|
9978
|
+
cwd: this.sessionManager.getCwd(),
|
|
9979
|
+
skill,
|
|
9980
|
+
sessionId,
|
|
9981
|
+
});
|
|
8764
9982
|
const subskillDetails = details as {
|
|
8765
9983
|
subskillActivation?: LoadedSubskillActivation;
|
|
8766
9984
|
subskillActivationSet?: LoadedSubskillActivation[];
|
|
@@ -8794,9 +10012,10 @@ export class AgentSession {
|
|
|
8794
10012
|
async #syncSkillPromptActiveStateSafely(
|
|
8795
10013
|
message: Pick<CustomMessage<unknown>, "customType" | "details">,
|
|
8796
10014
|
active: boolean,
|
|
10015
|
+
persistActiveState = true,
|
|
8797
10016
|
): Promise<void> {
|
|
8798
10017
|
try {
|
|
8799
|
-
await this.#syncSkillPromptActiveState(message, active);
|
|
10018
|
+
await this.#syncSkillPromptActiveState(message, active, persistActiveState);
|
|
8800
10019
|
} catch {
|
|
8801
10020
|
// Skill HUD state is observational; a filesystem write failure must not
|
|
8802
10021
|
// interrupt the prompt turn it is visualizing. The native Stop hook still
|
|
@@ -8804,13 +10023,48 @@ export class AgentSession {
|
|
|
8804
10023
|
}
|
|
8805
10024
|
}
|
|
8806
10025
|
|
|
10026
|
+
async #seedSkillPromptActiveStateSafely(
|
|
10027
|
+
message: Pick<CustomMessage<unknown>, "customType" | "details">,
|
|
10028
|
+
): Promise<WorkflowSkillActivationSeed | undefined> {
|
|
10029
|
+
if (message.customType !== SKILL_PROMPT_MESSAGE_TYPE) return undefined;
|
|
10030
|
+
const details = message.details;
|
|
10031
|
+
if (!details || typeof details !== "object") return undefined;
|
|
10032
|
+
const name = (details as { name?: unknown }).name;
|
|
10033
|
+
if (typeof name !== "string" || !name.trim()) return undefined;
|
|
10034
|
+
try {
|
|
10035
|
+
const subskillDetails = details as {
|
|
10036
|
+
subskillActivation?: LoadedSubskillActivation;
|
|
10037
|
+
subskillActivationSet?: LoadedSubskillActivation[];
|
|
10038
|
+
};
|
|
10039
|
+
const activations = subskillDetails.subskillActivationSet?.length
|
|
10040
|
+
? subskillDetails.subskillActivationSet
|
|
10041
|
+
: subskillDetails.subskillActivation
|
|
10042
|
+
? [subskillDetails.subskillActivation]
|
|
10043
|
+
: [];
|
|
10044
|
+
return await ensureWorkflowSkillActivationSeed({
|
|
10045
|
+
cwd: this.sessionManager.getCwd(),
|
|
10046
|
+
skill: name.trim(),
|
|
10047
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
10048
|
+
activeSubskills: activations.length ? activations.map(toActiveSubskillEntry) : undefined,
|
|
10049
|
+
});
|
|
10050
|
+
} catch {
|
|
10051
|
+
return undefined;
|
|
10052
|
+
}
|
|
10053
|
+
}
|
|
10054
|
+
|
|
8807
10055
|
async promptCustomMessage<T = unknown>(
|
|
8808
10056
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
8809
10057
|
options?: Pick<
|
|
8810
10058
|
PromptOptions,
|
|
8811
|
-
|
|
10059
|
+
| "streamingBehavior"
|
|
10060
|
+
| "toolChoice"
|
|
10061
|
+
| "followUpQueuePolicy"
|
|
10062
|
+
| "onPreflightAccepted"
|
|
10063
|
+
| "onPreflightAcceptCommit"
|
|
10064
|
+
| "preflightSignal"
|
|
8812
10065
|
>,
|
|
8813
10066
|
): Promise<void> {
|
|
10067
|
+
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
8814
10068
|
const textContent =
|
|
8815
10069
|
typeof message.content === "string"
|
|
8816
10070
|
? message.content
|
|
@@ -8841,31 +10095,61 @@ export class AgentSession {
|
|
|
8841
10095
|
|
|
8842
10096
|
const admissionGeneration = this.#promptGeneration;
|
|
8843
10097
|
const admissionSignal = this.#promptPreflightAbortController.signal;
|
|
8844
|
-
await this.#withSessionAdmission(
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
10098
|
+
await this.#withSessionAdmission(
|
|
10099
|
+
"prompt",
|
|
10100
|
+
async admission => {
|
|
10101
|
+
this.#throwIfPromptPreflightCancelled(admissionGeneration, admissionSignal);
|
|
10102
|
+
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
10103
|
+
const customMessage: CustomMessage<T> = {
|
|
10104
|
+
role: "custom",
|
|
10105
|
+
customType: message.customType,
|
|
10106
|
+
content: message.content,
|
|
10107
|
+
display: message.display,
|
|
10108
|
+
details: message.details,
|
|
10109
|
+
attribution: message.attribution ?? "agent",
|
|
10110
|
+
timestamp: Date.now(),
|
|
10111
|
+
};
|
|
10112
|
+
if (deepInterviewUserIntentEpoch !== undefined)
|
|
10113
|
+
this.#deepInterviewGenuineUserMessageEpochs.set(customMessage, deepInterviewUserIntentEpoch);
|
|
10114
|
+
|
|
10115
|
+
let activationSeed: WorkflowSkillActivationSeed | undefined;
|
|
10116
|
+
let preflightCancelled = false;
|
|
10117
|
+
let durableAcceptanceCompleted = false;
|
|
10118
|
+
const commitAcceptance = async () => {
|
|
10119
|
+
activationSeed = await this.#seedSkillPromptActiveStateSafely(customMessage);
|
|
10120
|
+
await this.#syncSkillPromptActiveStateSafely(customMessage, true, activationSeed?.seeded !== true);
|
|
10121
|
+
if (options?.preflightSignal?.aborted) {
|
|
10122
|
+
await activationSeed?.rollback();
|
|
10123
|
+
throw promptPreflightCancelledError();
|
|
10124
|
+
}
|
|
10125
|
+
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
10126
|
+
else options?.onPreflightAccepted?.();
|
|
10127
|
+
durableAcceptanceCompleted = true;
|
|
10128
|
+
if (options?.preflightSignal?.aborted) {
|
|
10129
|
+
await activationSeed?.rollback();
|
|
10130
|
+
throw promptPreflightCancelledError();
|
|
10131
|
+
}
|
|
10132
|
+
};
|
|
10133
|
+
try {
|
|
10134
|
+
await this.#promptWithMessage(customMessage, textContent, {
|
|
10135
|
+
...options,
|
|
10136
|
+
onPreflightAccepted: undefined,
|
|
10137
|
+
onPreflightAcceptCommit: commitAcceptance,
|
|
10138
|
+
admissionLease: admission,
|
|
10139
|
+
resetRetryReplaySafety: true,
|
|
10140
|
+
});
|
|
10141
|
+
} catch (error) {
|
|
10142
|
+
if (isPromptPreflightCancelledError(error) || !durableAcceptanceCompleted) {
|
|
10143
|
+
preflightCancelled = true;
|
|
10144
|
+
await activationSeed?.rollback();
|
|
10145
|
+
}
|
|
10146
|
+
throw error;
|
|
10147
|
+
} finally {
|
|
10148
|
+
if (!preflightCancelled) await this.#syncSkillPromptActiveStateSafely(customMessage, false);
|
|
10149
|
+
}
|
|
10150
|
+
},
|
|
10151
|
+
options?.preflightSignal,
|
|
10152
|
+
);
|
|
8869
10153
|
}
|
|
8870
10154
|
|
|
8871
10155
|
async #promptWithMessage(
|
|
@@ -8873,7 +10157,12 @@ export class AgentSession {
|
|
|
8873
10157
|
expandedText: string,
|
|
8874
10158
|
options?: Pick<
|
|
8875
10159
|
PromptOptions,
|
|
8876
|
-
|
|
10160
|
+
| "toolChoice"
|
|
10161
|
+
| "images"
|
|
10162
|
+
| "skipCompactionCheck"
|
|
10163
|
+
| "onPreflightAccepted"
|
|
10164
|
+
| "onPreflightAcceptCommit"
|
|
10165
|
+
| "preflightSignal"
|
|
8877
10166
|
> & {
|
|
8878
10167
|
prependMessages?: AgentMessage[];
|
|
8879
10168
|
skipPostPromptRecoveryWait?: boolean;
|
|
@@ -8885,16 +10174,59 @@ export class AgentSession {
|
|
|
8885
10174
|
},
|
|
8886
10175
|
): Promise<void> {
|
|
8887
10176
|
this.#assertNoHandoffTransition();
|
|
8888
|
-
|
|
10177
|
+
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
10178
|
+
await awaitPromptInvocationPreflight(this.#agentEndPublicationPromise, options?.preflightSignal);
|
|
8889
10179
|
// Re-check after the publication await: a handoff can engage during that
|
|
8890
10180
|
// window, and #beginInFlight below would otherwise start a turn against the
|
|
8891
10181
|
// session being handed off.
|
|
8892
10182
|
this.#assertNoHandoffTransition();
|
|
8893
10183
|
this.#beginInFlight();
|
|
10184
|
+
// Discard hidden next-turn successors queued by a PREVIOUS turn that a
|
|
10185
|
+
// terminal abort closed. This must run BEFORE the admission bump below:
|
|
10186
|
+
// once the new root turn advances the epoch, the fence lookup can no
|
|
10187
|
+
// longer find the aborted turn's scope, and the pending messages would
|
|
10188
|
+
// otherwise be injected into this new prompt (review thread P2).
|
|
10189
|
+
if (this.#pendingNextTurnMessages.length > 0 && this.#isTurnContinuationBlocked()) {
|
|
10190
|
+
// Discard only TURN-origin hidden successors of the fenced attempt;
|
|
10191
|
+
// external/background next-turn context (extension triggers,
|
|
10192
|
+
// autoresearch) must survive (review thread P2). AUTHORIZED
|
|
10193
|
+
// owned-completion envelopes are preserved too: a monitor
|
|
10194
|
+
// notification queued via the deferred path (default origin "turn")
|
|
10195
|
+
// is an authorized completion from work left running by a PRIOR
|
|
10196
|
+
// scope:"turn" abort and must still reach followUp/prompt (review
|
|
10197
|
+
// thread P1).
|
|
10198
|
+
this.#pendingNextTurnMessages = this.#pendingNextTurnMessages.filter(
|
|
10199
|
+
entry => entry.origin !== "turn" || ownedCompletionResumeAction(entry.message as never) !== "ordinary",
|
|
10200
|
+
);
|
|
10201
|
+
}
|
|
10202
|
+
// NEW ROOT TURN: advance the attempt epoch before minting the lineage so
|
|
10203
|
+
// consecutive non-aborted turns never share (lineageIdHash, epoch). A
|
|
10204
|
+
// terminal abort of turn B must never capture turn A's left-running
|
|
10205
|
+
// owned work, and turn A's completion must never classify as a resume of
|
|
10206
|
+
// B. Same-turn continuations (auto-continue/retry, resetRetryReplaySafety
|
|
10207
|
+
// unset) keep the turn's epoch and lineage.
|
|
10208
|
+
if (options?.resetRetryReplaySafety) {
|
|
10209
|
+
this.#promptGeneration++;
|
|
10210
|
+
this.#promptPreflightAbortController.abort();
|
|
10211
|
+
this.#promptPreflightAbortController = new AbortController();
|
|
10212
|
+
}
|
|
8894
10213
|
const predecessorAgentEndHold =
|
|
8895
10214
|
options?.predecessorAgentEndHold ?? this.#reserveDeferredAgentEndForContinuation();
|
|
8896
10215
|
const generation = this.#promptGeneration;
|
|
8897
|
-
|
|
10216
|
+
// Mint the immutable lineage identity for this prompt turn before the
|
|
10217
|
+
// model runs; beforeToolCall attaches this lineage + attempt epoch to
|
|
10218
|
+
// each tool call id so background registrations can be classified by
|
|
10219
|
+
// source later (terminal-abort owned-completion vs turn-continuation).
|
|
10220
|
+
this.#turnLineageIdHash = mintTurnLineageIdHash(
|
|
10221
|
+
this.sessionManager.getSessionId?.() ?? "local",
|
|
10222
|
+
generation,
|
|
10223
|
+
this.#terminalLineageSecret,
|
|
10224
|
+
);
|
|
10225
|
+
|
|
10226
|
+
const sessionPreflightSignal = this.#promptPreflightAbortController.signal;
|
|
10227
|
+
const preflightSignal = options?.preflightSignal
|
|
10228
|
+
? AbortSignal.any([sessionPreflightSignal, options.preflightSignal])
|
|
10229
|
+
: sessionPreflightSignal;
|
|
8898
10230
|
const rosterClaim = this.#claimIrcRosterCandidate();
|
|
8899
10231
|
let hasPendingNextTurnMessages = false;
|
|
8900
10232
|
let pendingNextTurnMessageCount = 0;
|
|
@@ -8941,7 +10273,7 @@ export class AgentSession {
|
|
|
8941
10273
|
await this.#checkEstimatedContextBeforePrompt([
|
|
8942
10274
|
...(options?.prependMessages ?? []),
|
|
8943
10275
|
message,
|
|
8944
|
-
...this.#pendingNextTurnMessages,
|
|
10276
|
+
...this.#pendingNextTurnMessages.map(entry => entry.message),
|
|
8945
10277
|
]);
|
|
8946
10278
|
}
|
|
8947
10279
|
|
|
@@ -9100,8 +10432,14 @@ export class AgentSession {
|
|
|
9100
10432
|
messages.push(message);
|
|
9101
10433
|
|
|
9102
10434
|
// Re-present captured pending next-turn messages (never drained here).
|
|
9103
|
-
|
|
9104
|
-
|
|
10435
|
+
// Reclassify deferred envelopes before injecting them into the new
|
|
10436
|
+
// root turn: a monitor notification queued during/after a scope:"owned"
|
|
10437
|
+
// abort must be dropped (and fresh ones allocate a fresh lineage)
|
|
10438
|
+
// even though the deferred queueing path never ran the classifier
|
|
10439
|
+
// (review thread P1).
|
|
10440
|
+
for (const entry of this.#pendingNextTurnMessages.slice(0, pendingNextTurnMessageCount)) {
|
|
10441
|
+
if (ownedCompletionResumeAction(entry.message as never) === "drop") continue;
|
|
10442
|
+
messages.push(entry.message);
|
|
9105
10443
|
}
|
|
9106
10444
|
messages.push(...fileMentionMessages);
|
|
9107
10445
|
if (hindsightRecall) {
|
|
@@ -9145,6 +10483,15 @@ export class AgentSession {
|
|
|
9145
10483
|
attemptMessages = undefined;
|
|
9146
10484
|
},
|
|
9147
10485
|
};
|
|
10486
|
+
// Abort can race asynchronous preflight work. The injection signatures were
|
|
10487
|
+
// consumed while building context, but no prompt was accepted, so reset them.
|
|
10488
|
+
if (this.#isPromptPreflightCancelled(generation, preflightSignal) || options?.preflightSignal?.aborted) {
|
|
10489
|
+
this.#resetInjectedContextSignatures();
|
|
10490
|
+
// Ack-waiting callers are told the preflight never ran; direct callers
|
|
10491
|
+
// (aborted after setup) resolve gracefully as before f24f46ff5.
|
|
10492
|
+
if (options?.onPreflightAccepted || options?.onPreflightAcceptCommit) throw promptPreflightCancelledError();
|
|
10493
|
+
return;
|
|
10494
|
+
}
|
|
9148
10495
|
|
|
9149
10496
|
const agentPromptOptions = {
|
|
9150
10497
|
...(options?.toolChoice ? { toolChoice: options.toolChoice } : undefined),
|
|
@@ -9160,11 +10507,20 @@ export class AgentSession {
|
|
|
9160
10507
|
this.getHindsightSessionState()?.markRecallSnippetInjected(hindsightRecall);
|
|
9161
10508
|
}
|
|
9162
10509
|
if (pendingNextTurnMessageCount > 0) {
|
|
10510
|
+
// The captured prefix is consumed here whether its entries were
|
|
10511
|
+
// injected or dropped. Settle every owned envelope at this
|
|
10512
|
+
// consumption boundary; the helper retains registrations whose
|
|
10513
|
+
// jobs are still running, while non-persistent monitor jobs
|
|
10514
|
+
// already cancelled after delivery cannot produce another
|
|
10515
|
+
// settlement (review thread P2).
|
|
10516
|
+
const consumedPrefix = this.#pendingNextTurnMessages.slice(0, pendingNextTurnMessageCount);
|
|
10517
|
+
this.#settleDeliveredOwnedRegistrations(consumedPrefix.map(entry => entry.message));
|
|
9163
10518
|
this.#pendingNextTurnMessages.splice(0, pendingNextTurnMessageCount);
|
|
9164
10519
|
}
|
|
9165
10520
|
if (this.#cancelAndSubmitInProgress) this.#cancelAndSubmitPendingNextTurnDrained = true;
|
|
9166
10521
|
},
|
|
9167
10522
|
};
|
|
10523
|
+
|
|
9168
10524
|
await this.#promptAgentWithIdleRetry(preSubmit, agentPromptOptions, predecessorAgentEndHold, {
|
|
9169
10525
|
signal: preflightSignal,
|
|
9170
10526
|
resourceRunId: this.#runResourceLeaseContext.getStore()?.resourceRunId,
|
|
@@ -9449,15 +10805,21 @@ export class AgentSession {
|
|
|
9449
10805
|
async #queueSteer(
|
|
9450
10806
|
text: string,
|
|
9451
10807
|
images?: ImageContent[],
|
|
9452
|
-
options?: { claimsGenuineUserIntent?: boolean },
|
|
10808
|
+
options?: { claimsGenuineUserIntent?: boolean; onPromoted?: () => void; external?: boolean },
|
|
9453
10809
|
): Promise<void> {
|
|
9454
10810
|
this.#assertNoHandoffTransition();
|
|
9455
10811
|
assertImagePlaceholdersHavePayload(text, images);
|
|
9456
10812
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
9457
|
-
this.#
|
|
10813
|
+
const displayEntry = this.#createQueuedDisplayEntry(displayText);
|
|
10814
|
+
this.#steeringMessages.push(displayEntry);
|
|
9458
10815
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
9459
10816
|
if (images && images.length > 0) content.push(...images);
|
|
9460
10817
|
const message = { role: "user" as const, content, attribution: "user" as const, timestamp: Date.now() };
|
|
10818
|
+
if (options?.external) {
|
|
10819
|
+
this.#externalSteerMessages.add(message);
|
|
10820
|
+
this.#externalSteerAdmissionSeq.set(message, ++this.#steeringAdmissionSeq);
|
|
10821
|
+
if (options.onPromoted) this.#steerPromotionHooks.set(message, options.onPromoted);
|
|
10822
|
+
}
|
|
9461
10823
|
if (options?.claimsGenuineUserIntent) {
|
|
9462
10824
|
const epoch = this.#claimDeepInterviewUserIntent();
|
|
9463
10825
|
this.#deepInterviewGenuineUserMessageEpochs.set(message, epoch);
|
|
@@ -9474,6 +10836,7 @@ export class AgentSession {
|
|
|
9474
10836
|
if (!this.#cancelAndSubmitInProgress && this.#canAutoContinueForSteer()) {
|
|
9475
10837
|
this.#scheduleAgentContinue({
|
|
9476
10838
|
shouldContinue: () => this.#canAutoContinueForSteer() && this.agent.hasQueuedSteering(),
|
|
10839
|
+
rescheduleOnBusy: true,
|
|
9477
10840
|
continueQueuedOnly: true,
|
|
9478
10841
|
});
|
|
9479
10842
|
}
|
|
@@ -9485,29 +10848,95 @@ export class AgentSession {
|
|
|
9485
10848
|
async #queueFollowUp(
|
|
9486
10849
|
text: string,
|
|
9487
10850
|
images?: ImageContent[],
|
|
9488
|
-
options?: {
|
|
9489
|
-
|
|
10851
|
+
options?: {
|
|
10852
|
+
forceOneAtATime?: boolean;
|
|
10853
|
+
claimsGenuineUserIntent?: boolean;
|
|
10854
|
+
onPromoted?: () => void;
|
|
10855
|
+
sdkRunToken?: string;
|
|
10856
|
+
},
|
|
10857
|
+
): Promise<QueuedFollowUpOwner> {
|
|
9490
10858
|
this.#assertNoHandoffTransition();
|
|
9491
10859
|
assertImagePlaceholdersHavePayload(text, images);
|
|
9492
10860
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
9493
|
-
this
|
|
10861
|
+
const queueWasEmpty = !this.agent.hasQueuedMessages();
|
|
10862
|
+
const displayEntry = this.#createQueuedDisplayEntry(displayText);
|
|
10863
|
+
this.#followUpMessages.push(displayEntry);
|
|
9494
10864
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
9495
10865
|
if (images && images.length > 0) content.push(...images);
|
|
9496
10866
|
const message = { role: "user" as const, content, attribution: "user" as const, timestamp: Date.now() };
|
|
10867
|
+
this.#externalFollowUps.add(message);
|
|
10868
|
+
if (options?.onPromoted) this.#followUpPromotionHooks.set(message, options.onPromoted);
|
|
9497
10869
|
if (options?.claimsGenuineUserIntent) {
|
|
9498
10870
|
const epoch = this.#claimDeepInterviewUserIntent();
|
|
9499
10871
|
this.#deepInterviewGenuineUserMessageEpochs.set(message, epoch);
|
|
9500
10872
|
}
|
|
9501
|
-
this.
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
//
|
|
9508
|
-
//
|
|
9509
|
-
//
|
|
9510
|
-
|
|
10873
|
+
if (options?.sdkRunToken) this.#sdkRunTokensByQueuedMessage.set(message, options.sdkRunToken);
|
|
10874
|
+
if (options?.sdkRunToken && (this.agent.state.isStreaming || this.agent.hasQueuedMessages())) {
|
|
10875
|
+
this.#deferredSdkFollowUps.push(message);
|
|
10876
|
+
} else {
|
|
10877
|
+
this.agent.followUp(message, options?.forceOneAtATime ? { forceOneAtATime: true } : undefined);
|
|
10878
|
+
}
|
|
10879
|
+
// When this is the first queued message and the session is in a resumable
|
|
10880
|
+
// assistant-ended state, schedule an immediate continue so it is delivered
|
|
10881
|
+
// without waiting for the next user turn. A later accepted follow-up must
|
|
10882
|
+
// not start unrelated queued work ahead of it, because that work has a
|
|
10883
|
+
// different cancellation and terminal owner.
|
|
10884
|
+
if (queueWasEmpty)
|
|
10885
|
+
this.#scheduleQueuedFollowUpContinuation(() =>
|
|
10886
|
+
this.agent.snapshotFollowUp().some(candidate => candidate === message),
|
|
10887
|
+
);
|
|
10888
|
+
return {
|
|
10889
|
+
cancel: () => {
|
|
10890
|
+
const deferredIndex = this.#deferredSdkFollowUps.indexOf(message);
|
|
10891
|
+
let removed = false;
|
|
10892
|
+
if (deferredIndex !== -1) {
|
|
10893
|
+
this.#deferredSdkFollowUps.splice(deferredIndex, 1);
|
|
10894
|
+
removed = true;
|
|
10895
|
+
} else {
|
|
10896
|
+
removed = this.agent.removeQueuedMessages(candidate => candidate === message).followUp > 0;
|
|
10897
|
+
// This message was already released from the deferred queue; its
|
|
10898
|
+
// scheduled continuation was cancelled before it started. No further
|
|
10899
|
+
// agent_end may arrive to release the next deferred follow-up, so
|
|
10900
|
+
// advance the queue here to keep the next accepted SDK request moving.
|
|
10901
|
+
if (removed) this.#releaseDeferredSdkFollowUps();
|
|
10902
|
+
}
|
|
10903
|
+
if (removed) {
|
|
10904
|
+
this.#followUpMessages = this.#followUpMessages.filter(entry => entry !== displayEntry);
|
|
10905
|
+
this.#deepInterviewGenuineUserMessageEpochs.delete(message);
|
|
10906
|
+
this.#sdkRunTokensByQueuedMessage.delete(message);
|
|
10907
|
+
// A canceled follow-up is never promoted: release its
|
|
10908
|
+
// promotion hook so the callback is not retained for the
|
|
10909
|
+
// session lifetime (review thread P2).
|
|
10910
|
+
this.#followUpPromotionHooks.delete(message);
|
|
10911
|
+
}
|
|
10912
|
+
return removed;
|
|
10913
|
+
},
|
|
10914
|
+
};
|
|
10915
|
+
}
|
|
10916
|
+
#releaseDeferredSdkFollowUps(): void {
|
|
10917
|
+
// A deferred SDK follow-up must become the sole first message at the next
|
|
10918
|
+
// acceptance so its run token is bound to the agent_start. Releasing it
|
|
10919
|
+
// behind still-queued work reproduces the token-less mid-run consumption
|
|
10920
|
+
// hazard, so wait for the queue to drain; the next agent_end retries.
|
|
10921
|
+
if (this.agent.hasQueuedMessages()) return;
|
|
10922
|
+
const message = this.#deferredSdkFollowUps.shift();
|
|
10923
|
+
if (!message) return;
|
|
10924
|
+
this.agent.followUp(message, { forceOneAtATime: true });
|
|
10925
|
+
this.#scheduleAgentContinue({
|
|
10926
|
+
shouldContinue: () => this.#canStartDeferredSdkFollowUp() && this.agent.hasQueuedMessages(),
|
|
10927
|
+
rescheduleOnBusy: true,
|
|
10928
|
+
continueQueuedOnly: true,
|
|
10929
|
+
});
|
|
10930
|
+
}
|
|
10931
|
+
#canStartDeferredSdkFollowUp(): boolean {
|
|
10932
|
+
if (this.agent.state.isStreaming) return false;
|
|
10933
|
+
if (this.isCompacting) return false;
|
|
10934
|
+
if (this.isBashRunning) return false;
|
|
10935
|
+
if (this.isEvalRunning) return false;
|
|
10936
|
+
if (this.isRetrying) return false;
|
|
10937
|
+
const messages = this.agent.state.messages;
|
|
10938
|
+
const last = messages[messages.length - 1];
|
|
10939
|
+
return last?.role === "assistant" || last?.role === "bashExecution" || last?.role === "pythonExecution";
|
|
9511
10940
|
}
|
|
9512
10941
|
|
|
9513
10942
|
/**
|
|
@@ -9523,10 +10952,17 @@ export class AgentSession {
|
|
|
9523
10952
|
const last = messages[messages.length - 1];
|
|
9524
10953
|
return last?.role === "assistant" || last?.role === "bashExecution" || last?.role === "pythonExecution";
|
|
9525
10954
|
}
|
|
9526
|
-
#scheduleQueuedFollowUpContinuation(): void {
|
|
9527
|
-
if (
|
|
10955
|
+
#scheduleQueuedFollowUpContinuation(ownsQueuedMessage: (() => boolean) | undefined = undefined): void {
|
|
10956
|
+
if (
|
|
10957
|
+
!this.#cancelAndSubmitInProgress &&
|
|
10958
|
+
this.#canAutoContinueForFollowUp() &&
|
|
10959
|
+
this.agent.hasQueuedMessages() &&
|
|
10960
|
+
(ownsQueuedMessage?.() ?? true)
|
|
10961
|
+
) {
|
|
9528
10962
|
this.#scheduleAgentContinue({
|
|
9529
|
-
shouldContinue: () =>
|
|
10963
|
+
shouldContinue: () =>
|
|
10964
|
+
this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages() && (ownsQueuedMessage?.() ?? true),
|
|
10965
|
+
rescheduleOnBusy: true,
|
|
9530
10966
|
continueQueuedOnly: true,
|
|
9531
10967
|
});
|
|
9532
10968
|
}
|
|
@@ -9548,16 +10984,16 @@ export class AgentSession {
|
|
|
9548
10984
|
}
|
|
9549
10985
|
|
|
9550
10986
|
queueDeferredMessage(message: CustomMessage): void {
|
|
9551
|
-
this.#queueHiddenNextTurnMessage(message, true);
|
|
10987
|
+
this.#queueHiddenNextTurnMessage(message, true, "turn");
|
|
9552
10988
|
}
|
|
9553
10989
|
|
|
9554
10990
|
queueDeferredMessageForTests(message: CustomMessage, triggerTurn = true): void {
|
|
9555
|
-
this.#queueHiddenNextTurnMessage(message, triggerTurn);
|
|
10991
|
+
this.#queueHiddenNextTurnMessage(message, triggerTurn, "turn");
|
|
9556
10992
|
}
|
|
9557
10993
|
|
|
9558
10994
|
/** Read-only test seam for the hidden next-turn context queue. */
|
|
9559
10995
|
getPendingNextTurnMessagesForTests(): readonly CustomMessage[] {
|
|
9560
|
-
return this.#pendingNextTurnMessages.
|
|
10996
|
+
return this.#pendingNextTurnMessages.map(entry => entry.message);
|
|
9561
10997
|
}
|
|
9562
10998
|
|
|
9563
10999
|
/** Test-only abort outcome override; undefined retains the production abort race. */
|
|
@@ -9565,12 +11001,16 @@ export class AgentSession {
|
|
|
9565
11001
|
this.#cancelAndSubmitAbortOutcomeProviderForTests = provider;
|
|
9566
11002
|
}
|
|
9567
11003
|
|
|
9568
|
-
#queueHiddenNextTurnMessage(
|
|
11004
|
+
#queueHiddenNextTurnMessage(
|
|
11005
|
+
message: CustomMessage,
|
|
11006
|
+
triggerTurn: boolean,
|
|
11007
|
+
origin: "turn" | "external" = "external",
|
|
11008
|
+
): void {
|
|
9569
11009
|
// A hidden next-turn message queued during a handoff transition would be
|
|
9570
11010
|
// dropped when the successor clears predecessor queues; reject it as busy so
|
|
9571
11011
|
// the caller can retry against the settled session.
|
|
9572
11012
|
this.#assertNoHandoffTransition();
|
|
9573
|
-
this.#pendingNextTurnMessages.push(message);
|
|
11013
|
+
this.#pendingNextTurnMessages.push({ message, origin });
|
|
9574
11014
|
if (!triggerTurn) return;
|
|
9575
11015
|
const generation = this.#promptGeneration;
|
|
9576
11016
|
if (this.#scheduledHiddenNextTurnGeneration === generation) {
|
|
@@ -9585,6 +11025,27 @@ export class AgentSession {
|
|
|
9585
11025
|
if (this.#pendingNextTurnMessages.length === 0) {
|
|
9586
11026
|
return;
|
|
9587
11027
|
}
|
|
11028
|
+
// Terminal abort closed this turn's continuation fence: a hidden
|
|
11029
|
+
// next-turn successor queued by the aborted turn must NOT start,
|
|
11030
|
+
// even though the scheduler's generation check still passes
|
|
11031
|
+
// (the abort preserves the epoch so the gate can find the scope).
|
|
11032
|
+
if (this.#isTurnContinuationBlocked()) {
|
|
11033
|
+
// Terminal abort closed this turn's continuation fence:
|
|
11034
|
+
// DISCARD only the turn-origin hidden successors queued by the
|
|
11035
|
+
// aborted turn so they cannot be drained into a later explicit
|
|
11036
|
+
// prompt; external/background next-turn context survives
|
|
11037
|
+
// (review thread P2). AUTHORIZED owned-completion envelopes
|
|
11038
|
+
// survive too: a left-running owned job's completion queued
|
|
11039
|
+
// via sendCustomMessage (default origin "turn") is a promised
|
|
11040
|
+
// resume of the root worker, not a continuation of the aborted
|
|
11041
|
+
// attempt, and #promptQueuedHiddenNextTurnMessages classifies
|
|
11042
|
+
// it fresh (review thread P1).
|
|
11043
|
+
this.#pendingNextTurnMessages = this.#pendingNextTurnMessages.filter(
|
|
11044
|
+
entry =>
|
|
11045
|
+
entry.origin !== "turn" || ownedCompletionResumeAction(entry.message as never) !== "ordinary",
|
|
11046
|
+
);
|
|
11047
|
+
return;
|
|
11048
|
+
}
|
|
9588
11049
|
try {
|
|
9589
11050
|
await this.#promptQueuedHiddenNextTurnMessages();
|
|
9590
11051
|
} catch {
|
|
@@ -9609,12 +11070,47 @@ export class AgentSession {
|
|
|
9609
11070
|
|
|
9610
11071
|
const queuedMessages = [...this.#pendingNextTurnMessages];
|
|
9611
11072
|
this.#pendingNextTurnMessages = [];
|
|
9612
|
-
|
|
11073
|
+
// Reclassify deferred envelopes at the drain boundary: a monitor
|
|
11074
|
+
// notification queued via the deferAgentInitiatedTurns branch never ran
|
|
11075
|
+
// ownedCompletionResumeAction at queue time, so a scope:"owned" abort
|
|
11076
|
+
// that landed since may now classify the envelope as drop — drop it and
|
|
11077
|
+
// allocate a fresh lineage for fresh envelopes (review thread P1).
|
|
11078
|
+
const dropped: Array<{ message: CustomMessage }> = [];
|
|
11079
|
+
const reclassified = queuedMessages.filter(entry => {
|
|
11080
|
+
if (ownedCompletionResumeAction(entry.message as never) === "drop") {
|
|
11081
|
+
dropped.push(entry);
|
|
11082
|
+
return false;
|
|
11083
|
+
}
|
|
11084
|
+
return true;
|
|
11085
|
+
});
|
|
11086
|
+
// A reclassified drop (owned scope, terminal job) never reaches a
|
|
11087
|
+
// follow-up/prompt boundary: settle its registrations here so the
|
|
11088
|
+
// terminal tuple does not occupy the global registration and
|
|
11089
|
+
// retained-policy capacities indefinitely (review thread P2).
|
|
11090
|
+
if (dropped.length > 0) {
|
|
11091
|
+
this.#settleDeliveredOwnedRegistrations(dropped.map(entry => entry.message));
|
|
11092
|
+
}
|
|
11093
|
+
if (reclassified.length === 0) {
|
|
11094
|
+
return;
|
|
11095
|
+
}
|
|
11096
|
+
// Allocate a fresh root-turn lineage when the drained batch contains a
|
|
11097
|
+
// fresh owned envelope OR an EXTERNAL hidden trigger: an external
|
|
11098
|
+
// nextTurn message has no owned envelope, so without this it would
|
|
11099
|
+
// reuse the previous turn's identical lineage+epoch and a later
|
|
11100
|
+
// scope:"owned" abort of the external turn could capture the previous
|
|
11101
|
+
// turn's jobs (review thread P1).
|
|
11102
|
+
if (
|
|
11103
|
+
reclassified.some(entry => ownedCompletionResumeAction(entry.message as never) === "fresh") ||
|
|
11104
|
+
reclassified.some(entry => entry.origin === "external")
|
|
11105
|
+
) {
|
|
11106
|
+
this.#resumeFromOwnedCompletion();
|
|
11107
|
+
}
|
|
11108
|
+
const message = reclassified[reclassified.length - 1]?.message;
|
|
9613
11109
|
if (!message) {
|
|
9614
11110
|
return;
|
|
9615
11111
|
}
|
|
9616
11112
|
|
|
9617
|
-
const prependMessages =
|
|
11113
|
+
const prependMessages = reclassified.slice(0, -1).map(entry => entry.message);
|
|
9618
11114
|
const textContent = this.#getCustomMessageTextContent(message);
|
|
9619
11115
|
await this.#syncSkillPromptActiveStateSafely(message, true);
|
|
9620
11116
|
try {
|
|
@@ -9623,13 +11119,47 @@ export class AgentSession {
|
|
|
9623
11119
|
skipPostPromptRecoveryWait: true,
|
|
9624
11120
|
});
|
|
9625
11121
|
} catch (error) {
|
|
9626
|
-
|
|
11122
|
+
// Requeue only the SURVIVING reclassified entries: a completion
|
|
11123
|
+
// denied by scope:"owned" was settled (dropped) above and must
|
|
11124
|
+
// never reach a later prompt — once its scope and now-unoccupied
|
|
11125
|
+
// policy tombstone are evicted by the bounded registries, the
|
|
11126
|
+
// restored external monitor message would classify as ordinary and
|
|
11127
|
+
// breach the owned abort's zero-delivery guarantee (review thread
|
|
11128
|
+
// P2).
|
|
11129
|
+
this.#pendingNextTurnMessages = [...reclassified, ...this.#pendingNextTurnMessages];
|
|
9627
11130
|
throw error;
|
|
9628
11131
|
} finally {
|
|
9629
11132
|
await this.#syncSkillPromptActiveStateSafely(message, false);
|
|
9630
11133
|
}
|
|
9631
11134
|
}
|
|
9632
11135
|
|
|
11136
|
+
/** Unregister owned-completion registrations once their delivery is settled
|
|
11137
|
+
* (delivered as a fresh/ordinary turn or dropped by an owned scope) AND the
|
|
11138
|
+
* underlying job is TERMINAL. A monitor notification reuses the still-running
|
|
11139
|
+
* monitor's registration, so removing it after an intermediate notification
|
|
11140
|
+
* would let a later scope:"owned" abort miss the monitor and make subsequent
|
|
11141
|
+
* notifications lose their envelope (review thread P1). */
|
|
11142
|
+
#settleDeliveredOwnedRegistrations(messages: AgentMessage[]): void {
|
|
11143
|
+
// Use the SESSION-OWNED manager first: the process-global instance is
|
|
11144
|
+
// the last-created session, so checking this session's delivery against
|
|
11145
|
+
// another session's manager (which lacks the same local job id) would
|
|
11146
|
+
// see job === undefined and remove a still-live registration (review
|
|
11147
|
+
// thread P1).
|
|
11148
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
11149
|
+
for (const message of messages) {
|
|
11150
|
+
const details = (message as { details?: { ownedCompletions?: OwnedCompletionEnvelope[] } }).details;
|
|
11151
|
+
for (const envelope of details?.ownedCompletions ?? []) {
|
|
11152
|
+
const job = manager?.getJob(envelope.registration.jobId);
|
|
11153
|
+
const status = job?.generation === envelope.registration.jobGeneration ? job?.status : undefined;
|
|
11154
|
+
// Evicted jobs have no live record (job === undefined); terminal
|
|
11155
|
+
// statuses settle the registration.
|
|
11156
|
+
if (job === undefined || status === "completed" || status === "cancelled" || status === "failed") {
|
|
11157
|
+
unregisterOwnedRegistration(envelope.registration);
|
|
11158
|
+
}
|
|
11159
|
+
}
|
|
11160
|
+
}
|
|
11161
|
+
}
|
|
11162
|
+
|
|
9633
11163
|
#getCustomMessageTextContent(message: Pick<CustomMessage, "content">): string {
|
|
9634
11164
|
if (typeof message.content === "string") {
|
|
9635
11165
|
return message.content;
|
|
@@ -9671,6 +11201,13 @@ export class AgentSession {
|
|
|
9671
11201
|
triggerTurn?: boolean;
|
|
9672
11202
|
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
9673
11203
|
followUpQueuePolicy?: "respect-mode" | "sequential";
|
|
11204
|
+
/** INTERNAL trusted option: origin of a hidden next-turn message —
|
|
11205
|
+
* "turn" (produced by the current turn's continuation machinery,
|
|
11206
|
+
* purged by a terminal abort) or "external" (background producers,
|
|
11207
|
+
* preserved). Extension sendMessage callers are classified
|
|
11208
|
+
* "external" at the trusted bridge and never supply this bit
|
|
11209
|
+
* (review thread P2). */
|
|
11210
|
+
origin?: "turn" | "external";
|
|
9674
11211
|
},
|
|
9675
11212
|
): Promise<void> {
|
|
9676
11213
|
this.#assertRecoveryHydrationPromoted();
|
|
@@ -9695,7 +11232,7 @@ export class AgentSession {
|
|
|
9695
11232
|
// while an (auto-)handoff is unwinding it.
|
|
9696
11233
|
this.#assertNoHandoffTransition();
|
|
9697
11234
|
if (options?.deliverAs === "nextTurn") {
|
|
9698
|
-
this.#queueHiddenNextTurnMessage(appMessage, options?.triggerTurn ?? false);
|
|
11235
|
+
this.#queueHiddenNextTurnMessage(appMessage, options?.triggerTurn ?? false, options?.origin ?? "turn");
|
|
9699
11236
|
return;
|
|
9700
11237
|
}
|
|
9701
11238
|
|
|
@@ -9713,9 +11250,13 @@ export class AgentSession {
|
|
|
9713
11250
|
if (options?.deliverAs === "nextTurn") {
|
|
9714
11251
|
if (options?.triggerTurn) {
|
|
9715
11252
|
if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
|
|
9716
|
-
this.#queueHiddenNextTurnMessage(appMessage, false);
|
|
11253
|
+
this.#queueHiddenNextTurnMessage(appMessage, false, options?.origin ?? "turn");
|
|
9717
11254
|
return;
|
|
9718
11255
|
}
|
|
11256
|
+
// Every direct idle admission is a NEW ROOT TURN: allocate a fresh
|
|
11257
|
+
// lineage so the turn never remints the previous turn's identical
|
|
11258
|
+
// lineage+epoch (review thread P1).
|
|
11259
|
+
this.#resumeFromOwnedCompletion();
|
|
9719
11260
|
await this.#syncSkillPromptActiveStateSafely(appMessage, true);
|
|
9720
11261
|
try {
|
|
9721
11262
|
await this.#promptWithMessage(appMessage, this.#getCustomMessageTextContent(appMessage), {
|
|
@@ -9723,6 +11264,11 @@ export class AgentSession {
|
|
|
9723
11264
|
});
|
|
9724
11265
|
} finally {
|
|
9725
11266
|
await this.#syncSkillPromptActiveStateSafely(appMessage, false);
|
|
11267
|
+
// The direct idle admission bypasses onFollowUpConsumed:
|
|
11268
|
+
// settle any delivered owned-completion envelope so a
|
|
11269
|
+
// terminal registration does not occupy the registry until
|
|
11270
|
+
// saturation (review thread P2).
|
|
11271
|
+
this.#settleDeliveredOwnedRegistrations([appMessage]);
|
|
9726
11272
|
}
|
|
9727
11273
|
return;
|
|
9728
11274
|
}
|
|
@@ -9744,6 +11290,27 @@ export class AgentSession {
|
|
|
9744
11290
|
this.#queueHiddenNextTurnMessage(appMessage, false);
|
|
9745
11291
|
return;
|
|
9746
11292
|
}
|
|
11293
|
+
// Gate owned-completion deliveries at this IDLE boundary too: the
|
|
11294
|
+
// monitor notification envelope must DROP for scope:"owned" (racing
|
|
11295
|
+
// settlement) and allocate a FRESH lineage for scope:"turn" — the
|
|
11296
|
+
// direct prompt path below bypasses the follow-up consumption
|
|
11297
|
+
// classification (review thread P1).
|
|
11298
|
+
const idleResumeAction = ownedCompletionResumeAction(appMessage as never);
|
|
11299
|
+
if (idleResumeAction === "drop") {
|
|
11300
|
+
// The notification is denied (owned scope) but its job may be
|
|
11301
|
+
// terminal: settle the envelope's registrations before
|
|
11302
|
+
// discarding it, otherwise the tuple occupies the global
|
|
11303
|
+
// registration and retained-policy bounds indefinitely
|
|
11304
|
+
// (review thread P2).
|
|
11305
|
+
this.#settleDeliveredOwnedRegistrations([appMessage]);
|
|
11306
|
+
return;
|
|
11307
|
+
}
|
|
11308
|
+
// Every direct idle admission is a NEW ROOT TURN: allocate a fresh
|
|
11309
|
+
// lineage even for ordinary triggers (cron/extension). Without this,
|
|
11310
|
+
// the turn would remint the previous turn's identical lineage+epoch
|
|
11311
|
+
// and a later scope:"owned" abort could capture the earlier turn's
|
|
11312
|
+
// unrelated jobs (review thread P1).
|
|
11313
|
+
this.#resumeFromOwnedCompletion();
|
|
9747
11314
|
await this.#syncSkillPromptActiveStateSafely(appMessage, true);
|
|
9748
11315
|
try {
|
|
9749
11316
|
await this.#promptWithMessage(appMessage, this.#getCustomMessageTextContent(appMessage), {
|
|
@@ -9751,6 +11318,13 @@ export class AgentSession {
|
|
|
9751
11318
|
});
|
|
9752
11319
|
} finally {
|
|
9753
11320
|
await this.#syncSkillPromptActiveStateSafely(appMessage, false);
|
|
11321
|
+
// The direct idle admission bypasses onFollowUpConsumed:
|
|
11322
|
+
// settle any delivered owned-completion envelope so a terminal
|
|
11323
|
+
// registration does not occupy the registry until saturation —
|
|
11324
|
+
// a non-persistent monitor cancels right after its first line
|
|
11325
|
+
// and enqueues no delivery that would settle it otherwise
|
|
11326
|
+
// (review thread P2).
|
|
11327
|
+
this.#settleDeliveredOwnedRegistrations([appMessage]);
|
|
9754
11328
|
}
|
|
9755
11329
|
return;
|
|
9756
11330
|
}
|
|
@@ -9778,13 +11352,13 @@ export class AgentSession {
|
|
|
9778
11352
|
}
|
|
9779
11353
|
const agentRemoved = this.agent.removeQueuedMessages(isMatch);
|
|
9780
11354
|
const beforeNext = this.#pendingNextTurnMessages.length;
|
|
9781
|
-
for (const
|
|
9782
|
-
if (predicate(
|
|
9783
|
-
const tag = readPendingDisplayTag(
|
|
11355
|
+
for (const entry of this.#pendingNextTurnMessages) {
|
|
11356
|
+
if (predicate(entry.message)) {
|
|
11357
|
+
const tag = readPendingDisplayTag(entry.message.details);
|
|
9784
11358
|
if (tag) removedTags.add(tag);
|
|
9785
11359
|
}
|
|
9786
11360
|
}
|
|
9787
|
-
this.#pendingNextTurnMessages = this.#pendingNextTurnMessages.filter(
|
|
11361
|
+
this.#pendingNextTurnMessages = this.#pendingNextTurnMessages.filter(entry => !predicate(entry.message));
|
|
9788
11362
|
const pendingNextTurn = beforeNext - this.#pendingNextTurnMessages.length;
|
|
9789
11363
|
let displaySteering = 0;
|
|
9790
11364
|
let displayFollowUp = 0;
|
|
@@ -9817,11 +11391,26 @@ export class AgentSession {
|
|
|
9817
11391
|
content: string | (TextContent | ImageContent)[],
|
|
9818
11392
|
options?: {
|
|
9819
11393
|
deliverAs?: "steer" | "followUp";
|
|
11394
|
+
/** Preserve a busy SDK dispatch as queued work across an admission fence. */
|
|
11395
|
+
queuedAtDispatch?: boolean;
|
|
9820
11396
|
onPreflightAccepted?: () => void;
|
|
9821
11397
|
onPreflightAcceptCommit?: () => void | Promise<void>;
|
|
11398
|
+
/** Fired when a queued submission (steering or follow-up) is promoted to its own run (SDK ownership correlation). */
|
|
11399
|
+
onQueuedPromoted?: () => void;
|
|
11400
|
+
preflightSignal?: AbortSignal;
|
|
11401
|
+
sdkRunToken?: string;
|
|
9822
11402
|
},
|
|
9823
11403
|
): Promise<void> {
|
|
9824
11404
|
this.#assertRecoveryHydrationPromoted();
|
|
11405
|
+
const owner = this.#sessionAdmissionContext.getStore();
|
|
11406
|
+
if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
|
|
11407
|
+
this.#assertSessionAdmissionOpen();
|
|
11408
|
+
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
11409
|
+
if (typeof content !== "string" && !Array.isArray(content)) {
|
|
11410
|
+
throw Object.assign(new Error("sendUserMessage requires string or content-array content."), {
|
|
11411
|
+
code: "invalid_input",
|
|
11412
|
+
});
|
|
11413
|
+
}
|
|
9825
11414
|
// Normalize content to text string + optional images
|
|
9826
11415
|
let text: string;
|
|
9827
11416
|
let images: ImageContent[] | undefined;
|
|
@@ -9842,38 +11431,145 @@ export class AgentSession {
|
|
|
9842
11431
|
if (images.length === 0) images = undefined;
|
|
9843
11432
|
}
|
|
9844
11433
|
|
|
9845
|
-
|
|
9846
|
-
|
|
9847
|
-
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
11434
|
+
const admissionSignal = options?.preflightSignal
|
|
11435
|
+
? AbortSignal.any([this.#promptPreflightAbortController.signal, options.preflightSignal])
|
|
11436
|
+
: this.#promptPreflightAbortController.signal;
|
|
11437
|
+
const preflightCancellationGeneration = this.#promptPreflightCancellationGeneration;
|
|
11438
|
+
// Classify and reserve follow-up order synchronously, before any await
|
|
11439
|
+
// (selection fence or durable acceptance): a follow-up dispatch that is
|
|
11440
|
+
// not yet durably enqueued must already count as ahead, or a later plain
|
|
11441
|
+
// prompt admitted through the same window would classify itself as fresh
|
|
11442
|
+
// delivery/steering and overtake it. The reservation is released once
|
|
11443
|
+
// the durable enqueue settles either way, so a rejected or cancelled
|
|
11444
|
+
// acceptance leaves no phantom ordering behind.
|
|
11445
|
+
const queuedPlainPrompt = options?.queuedAtDispatch === true && options.deliverAs === undefined;
|
|
11446
|
+
const hasFollowUpAhead = (): boolean =>
|
|
11447
|
+
this.#activeFollowUpReservationEpochs.size > 0 ||
|
|
11448
|
+
this.agent.snapshotFollowUp().length > 0 ||
|
|
11449
|
+
this.#deferredSdkFollowUps.length > 0;
|
|
11450
|
+
const followUpAheadAtReservation = queuedPlainPrompt && hasFollowUpAhead();
|
|
11451
|
+
const freshAtReservation =
|
|
11452
|
+
queuedPlainPrompt &&
|
|
11453
|
+
!followUpAheadAtReservation &&
|
|
11454
|
+
!this.agent.state.isStreaming &&
|
|
11455
|
+
!this.#canAutoContinueForSteer();
|
|
11456
|
+
const deliverAs =
|
|
11457
|
+
options?.deliverAs ??
|
|
11458
|
+
(queuedPlainPrompt
|
|
11459
|
+
? followUpAheadAtReservation
|
|
11460
|
+
? "followUp"
|
|
11461
|
+
: freshAtReservation
|
|
11462
|
+
? undefined
|
|
11463
|
+
: "steer"
|
|
11464
|
+
: undefined);
|
|
11465
|
+
const followUpReservationEpoch = deliverAs === "followUp" ? ++this.#followUpReservationEpoch : undefined;
|
|
11466
|
+
if (followUpReservationEpoch !== undefined) {
|
|
11467
|
+
this.#activeFollowUpReservationEpochs.add(followUpReservationEpoch);
|
|
11468
|
+
}
|
|
11469
|
+
const releaseFollowUpReservation = () => {
|
|
11470
|
+
if (followUpReservationEpoch === undefined) return;
|
|
11471
|
+
this.#activeFollowUpReservationEpochs.delete(followUpReservationEpoch);
|
|
11472
|
+
const waiters = [...this.#followUpReservationDrainWaiters];
|
|
11473
|
+
this.#followUpReservationDrainWaiters.clear();
|
|
11474
|
+
for (const waiter of waiters) waiter();
|
|
11475
|
+
};
|
|
11476
|
+
try {
|
|
11477
|
+
if (this.#pendingSelectionFences > 0) {
|
|
11478
|
+
await awaitPromptInvocationPreflight(this.#selectionFenceTail, admissionSignal);
|
|
11479
|
+
}
|
|
11480
|
+
const assertPreflightStillOpen = () => {
|
|
11481
|
+
this.#assertSessionAdmissionOpen();
|
|
11482
|
+
if (
|
|
11483
|
+
options?.preflightSignal?.aborted ||
|
|
11484
|
+
this.#promptPreflightCancellationGeneration !== preflightCancellationGeneration
|
|
11485
|
+
) {
|
|
11486
|
+
throw promptPreflightCancelledError();
|
|
11487
|
+
}
|
|
11488
|
+
};
|
|
11489
|
+
assertPreflightStillOpen();
|
|
11490
|
+
if (deliverAs === "followUp") {
|
|
11491
|
+
// Durable enqueue preserves reservation order: while an earlier
|
|
11492
|
+
// follow-up dispatch is still between its reservation and its own
|
|
11493
|
+
// durable enqueue, wait for those earlier reservations so this
|
|
11494
|
+
// dispatch can never enqueue ahead of them.
|
|
11495
|
+
if (followUpReservationEpoch !== undefined) {
|
|
11496
|
+
await this.#waitForEarlierFollowUpReservations(
|
|
11497
|
+
followUpReservationEpoch,
|
|
11498
|
+
options?.preflightSignal,
|
|
11499
|
+
admissionSignal,
|
|
11500
|
+
);
|
|
11501
|
+
}
|
|
11502
|
+
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
11503
|
+
assertPreflightStillOpen();
|
|
11504
|
+
const queuedFollowUp = await this.#queueFollowUp(text, images, {
|
|
11505
|
+
claimsGenuineUserIntent: true,
|
|
11506
|
+
forceOneAtATime: Boolean(options?.preflightSignal || options?.queuedAtDispatch),
|
|
11507
|
+
onPromoted: options?.onQueuedPromoted,
|
|
11508
|
+
sdkRunToken: options?.sdkRunToken,
|
|
11509
|
+
});
|
|
11510
|
+
const cancelQueuedFollowUp = () => queuedFollowUp.cancel();
|
|
11511
|
+
options?.preflightSignal?.addEventListener("abort", cancelQueuedFollowUp, { once: true });
|
|
11512
|
+
if (options?.preflightSignal?.aborted) cancelQueuedFollowUp();
|
|
11513
|
+
options?.onPreflightAccepted?.();
|
|
11514
|
+
return;
|
|
11515
|
+
}
|
|
11516
|
+
if (deliverAs === "steer") {
|
|
11517
|
+
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
11518
|
+
assertPreflightStillOpen();
|
|
11519
|
+
await this.#queueSteer(text, images, {
|
|
11520
|
+
claimsGenuineUserIntent: true,
|
|
11521
|
+
onPromoted: options?.onQueuedPromoted,
|
|
11522
|
+
external: true,
|
|
11523
|
+
});
|
|
11524
|
+
options?.onPreflightAccepted?.();
|
|
11525
|
+
return;
|
|
11526
|
+
}
|
|
9857
11527
|
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
|
|
9868
|
-
|
|
11528
|
+
// No explicit delivery mode: only a live stream makes prompt() throw
|
|
11529
|
+
// AgentBusyError, so queue the message as steering while streaming.
|
|
11530
|
+
// Compaction is intentionally NOT diverted here: prompt() handles an
|
|
11531
|
+
// in-flight compaction internally, and #queueSteer would otherwise park
|
|
11532
|
+
// the message in the steering queue with no turn to consume it.
|
|
11533
|
+
if (this.isStreaming) {
|
|
11534
|
+
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
11535
|
+
assertPreflightStillOpen();
|
|
11536
|
+
await this.#queueSteer(text, images, {
|
|
11537
|
+
claimsGenuineUserIntent: true,
|
|
11538
|
+
onPromoted: options?.onQueuedPromoted,
|
|
11539
|
+
external: true,
|
|
11540
|
+
});
|
|
11541
|
+
options?.onPreflightAccepted?.();
|
|
11542
|
+
return;
|
|
11543
|
+
}
|
|
9869
11544
|
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
|
|
11545
|
+
// Use prompt() with expandPromptTemplates: false to skip command handling and template expansion
|
|
11546
|
+
let queuedPromotionFired = false;
|
|
11547
|
+
const fireQueuedPromotion = () => {
|
|
11548
|
+
if (!freshAtReservation || queuedPromotionFired) return;
|
|
11549
|
+
queuedPromotionFired = true;
|
|
11550
|
+
options?.onQueuedPromoted?.();
|
|
11551
|
+
};
|
|
11552
|
+
await this.prompt(text, {
|
|
11553
|
+
expandPromptTemplates: false,
|
|
11554
|
+
images,
|
|
11555
|
+
onPreflightAccepted: () => {
|
|
11556
|
+
options?.onPreflightAccepted?.();
|
|
11557
|
+
fireQueuedPromotion();
|
|
11558
|
+
},
|
|
11559
|
+
onPreflightAcceptCommit:
|
|
11560
|
+
options?.onPreflightAcceptCommit || freshAtReservation
|
|
11561
|
+
? async () => {
|
|
11562
|
+
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
11563
|
+
else options?.onPreflightAccepted?.();
|
|
11564
|
+
assertPreflightStillOpen();
|
|
11565
|
+
fireQueuedPromotion();
|
|
11566
|
+
}
|
|
11567
|
+
: undefined,
|
|
11568
|
+
preflightSignal: options?.preflightSignal,
|
|
11569
|
+
});
|
|
11570
|
+
} finally {
|
|
11571
|
+
releaseFollowUpReservation();
|
|
11572
|
+
}
|
|
9877
11573
|
}
|
|
9878
11574
|
|
|
9879
11575
|
/**
|
|
@@ -9885,6 +11581,7 @@ export class AgentSession {
|
|
|
9885
11581
|
const followUp = this.#followUpMessages.map(e => e.text);
|
|
9886
11582
|
this.#steeringMessages = [];
|
|
9887
11583
|
this.#followUpMessages = [];
|
|
11584
|
+
this.#deferredSdkFollowUps = [];
|
|
9888
11585
|
this.agent.clearAllQueues();
|
|
9889
11586
|
return { steering, followUp };
|
|
9890
11587
|
}
|
|
@@ -10251,6 +11948,7 @@ export class AgentSession {
|
|
|
10251
11948
|
this.#markRetryReplayUnsafe();
|
|
10252
11949
|
this.abortRetry();
|
|
10253
11950
|
this.#promptGeneration++;
|
|
11951
|
+
this.#promptPreflightCancellationGeneration++;
|
|
10254
11952
|
this.#promptPreflightAbortController.abort();
|
|
10255
11953
|
this.#promptPreflightAbortController = new AbortController();
|
|
10256
11954
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
@@ -10323,6 +12021,7 @@ export class AgentSession {
|
|
|
10323
12021
|
delayMs: 1,
|
|
10324
12022
|
generation: this.#promptGeneration,
|
|
10325
12023
|
shouldContinue: () => this.agent.hasQueuedSteering(),
|
|
12024
|
+
rescheduleOnBusy: true,
|
|
10326
12025
|
continueQueuedOnly: true,
|
|
10327
12026
|
});
|
|
10328
12027
|
}
|
|
@@ -10353,20 +12052,370 @@ export class AgentSession {
|
|
|
10353
12052
|
if (outcome.kind === "error") throw outcome.cause;
|
|
10354
12053
|
}
|
|
10355
12054
|
/**
|
|
10356
|
-
*
|
|
12055
|
+
* Private terminal-abort seam: read the CURRENT turn's attempt epoch WITHOUT
|
|
12056
|
+
* interrupting it. Used to write the durable initial terminal marker BEFORE
|
|
12057
|
+
* any fence/stop effect (plan ordered step 4). Only the epoch is exposed —
|
|
12058
|
+
* never the opaque lineage handle — so no private origin metadata leaves the
|
|
12059
|
+
* session. Fails closed (undefined) when no active turn lineage exists.
|
|
12060
|
+
*/
|
|
12061
|
+
/**
|
|
12062
|
+
* Private terminal-abort seam: cancel a PENDING (not-yet-started) prompt
|
|
12063
|
+
* preflight. Aborting the preflight controller fires the captured admission
|
|
12064
|
+
* signal so #throwIfPromptPreflightCancelled throws and the pending prompt
|
|
12065
|
+
* never starts even if its SDK waiter was already settled; the controller is
|
|
12066
|
+
* reset for the next admission. No run handle exists for a preflight prompt.
|
|
12067
|
+
*/
|
|
12068
|
+
cancelPendingPreflightForTerminalAbort(): void {
|
|
12069
|
+
this.#promptPreflightCancellationGeneration++;
|
|
12070
|
+
this.#promptPreflightAbortController.abort();
|
|
12071
|
+
this.#promptPreflightAbortController = new AbortController();
|
|
12072
|
+
}
|
|
12073
|
+
/**
|
|
12074
|
+
* Capture the steering admission snapshot at abort ADMISSION: the host
|
|
12075
|
+
* invokes this before its durable marker transaction, so client steering
|
|
12076
|
+
* admitted while the abort is in flight classifies as post-snapshot and is
|
|
12077
|
+
* preserved instead of being purged at the later abortPromptAndWait
|
|
12078
|
+
* (review thread P1). Snapshots are stored PER-ADMISSION in a per-turn
|
|
12079
|
+
* FIFO: the settlement of each admission consumes its own captured
|
|
12080
|
+
* sequence, so a later overlapping abort of the same turn can never
|
|
12081
|
+
* overwrite an earlier admission's snapshot and purge an already-accepted
|
|
12082
|
+
* steer (review thread P1).
|
|
12083
|
+
*/
|
|
12084
|
+
captureTerminalAbortSteeringSnapshot(): number | undefined {
|
|
12085
|
+
if (this.#turnLineageIdHash === undefined) return undefined;
|
|
12086
|
+
const key = `${this.#turnLineageIdHash}:${this.#promptGeneration}`;
|
|
12087
|
+
const queue = this.#terminalAbortSteeringSnapshots.get(key) ?? [];
|
|
12088
|
+
const token = ++this.#terminalAbortAdmissionSeq;
|
|
12089
|
+
queue.push({ seq: this.#steeringAdmissionSeq, token });
|
|
12090
|
+
this.#terminalAbortSteeringSnapshots.set(key, queue);
|
|
12091
|
+
this.#terminalAbortSteeringSnapshotKeys.set(token, key);
|
|
12092
|
+
return token;
|
|
12093
|
+
}
|
|
12094
|
+
/**
|
|
12095
|
+
* Discard a captured snapshot whose admission never settles (a durable
|
|
12096
|
+
* same-key replay, no-effect, conflict, or marker failure): the entry is
|
|
12097
|
+
* keyed to the CURRENT turn, which is where its admission captured it.
|
|
12098
|
+
* Removing by token keeps overlapping admissions' entries independent.
|
|
12099
|
+
*/
|
|
12100
|
+
/**
|
|
12101
|
+
* Rebind a captured snapshot to the CURRENT turn: the owner-mismatch
|
|
12102
|
+
* fall-through may terminalize the aborting requester's own turn that won
|
|
12103
|
+
* the race, while the token was captured under the other connection's
|
|
12104
|
+
* turn. The entry moves to the current turn's FIFO (retaining the original
|
|
12105
|
+
* admission sequence) so the settlement's purge classifies steering
|
|
12106
|
+
* admitted since admission as post-snapshot (review thread P1).
|
|
12107
|
+
*/
|
|
12108
|
+
rebindTerminalAbortSteeringSnapshot(token: number): void {
|
|
12109
|
+
const capturedKey = this.#terminalAbortSteeringSnapshotKeys.get(token);
|
|
12110
|
+
if (capturedKey === undefined) return;
|
|
12111
|
+
if (this.#turnLineageIdHash === undefined) return;
|
|
12112
|
+
const currentKey = `${this.#turnLineageIdHash}:${this.#promptGeneration}`;
|
|
12113
|
+
if (capturedKey === currentKey) return;
|
|
12114
|
+
const capturedQueue = this.#terminalAbortSteeringSnapshots.get(capturedKey);
|
|
12115
|
+
const index = capturedQueue?.findIndex(entry => entry.token === token) ?? -1;
|
|
12116
|
+
if (index < 0) return;
|
|
12117
|
+
const [entry] = capturedQueue!.splice(index, 1);
|
|
12118
|
+
if (capturedQueue!.length === 0) this.#terminalAbortSteeringSnapshots.delete(capturedKey);
|
|
12119
|
+
const currentQueue = this.#terminalAbortSteeringSnapshots.get(currentKey) ?? [];
|
|
12120
|
+
currentQueue.push(entry);
|
|
12121
|
+
this.#terminalAbortSteeringSnapshots.set(currentKey, currentQueue);
|
|
12122
|
+
this.#terminalAbortSteeringSnapshotKeys.set(token, currentKey);
|
|
12123
|
+
}
|
|
12124
|
+
discardTerminalAbortSteeringSnapshot(token: number): void {
|
|
12125
|
+
const key = this.#terminalAbortSteeringSnapshotKeys.get(token);
|
|
12126
|
+
if (!key) return;
|
|
12127
|
+
this.#terminalAbortSteeringSnapshotKeys.delete(token);
|
|
12128
|
+
const queue = this.#terminalAbortSteeringSnapshots.get(key);
|
|
12129
|
+
if (!queue) return;
|
|
12130
|
+
const index = queue.findIndex(entry => entry.token === token);
|
|
12131
|
+
if (index >= 0) {
|
|
12132
|
+
queue.splice(index, 1);
|
|
12133
|
+
if (queue.length === 0) this.#terminalAbortSteeringSnapshots.delete(key);
|
|
12134
|
+
}
|
|
12135
|
+
}
|
|
12136
|
+
|
|
12137
|
+
getTerminalTurnEpoch(): number | undefined {
|
|
12138
|
+
const lineageIdHash = this.#turnLineageIdHash;
|
|
12139
|
+
if (!lineageIdHash) return undefined;
|
|
12140
|
+
return this.#promptGeneration;
|
|
12141
|
+
}
|
|
12142
|
+
/**
|
|
12143
|
+
* Logical endpoint used to key owned-registration lineage bindings.
|
|
12144
|
+
* Subagents inherit the parent's AsyncJobManager, whose completion callback
|
|
12145
|
+
* resolves registrations via AsyncJobManager.endpointIdOf(manager); the
|
|
12146
|
+
* child's own sessionManager id is never registered under the manager, so
|
|
12147
|
+
* binding/registering under it would make the parent's lookup miss and
|
|
12148
|
+
* demote the completion to an ordinary follow-up — purgeable by a parent
|
|
12149
|
+
* terminal turn abort and outside owned-scope delivery denial (review
|
|
12150
|
+
* thread P1). The manager's registered endpoint (owned for a top-level
|
|
12151
|
+
* session, inherited for a child) is authoritative for both binding and
|
|
12152
|
+
* lookup.
|
|
10357
12153
|
*/
|
|
10358
|
-
|
|
12154
|
+
#ownedRegistrationEndpoint(): string {
|
|
12155
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
12156
|
+
return AsyncJobManager.endpointIdOf(manager) ?? this.sessionManager.getSessionId() ?? "local";
|
|
12157
|
+
}
|
|
12158
|
+
async abortPromptAndWait(
|
|
12159
|
+
handle: string,
|
|
12160
|
+
options: {
|
|
12161
|
+
graceMs: number;
|
|
12162
|
+
terminal?: { scope: "turn" | "owned"; expectedEpoch?: number; steeringSnapshotToken?: number };
|
|
12163
|
+
},
|
|
12164
|
+
): Promise<
|
|
12165
|
+
RunSettlementProof & {
|
|
12166
|
+
terminalScope?: { scopeId: string; abortedAttemptEpoch: number; lineageIdHash: string };
|
|
12167
|
+
}
|
|
12168
|
+
> {
|
|
12169
|
+
let registeredScope: { scopeId: string; abortedAttemptEpoch: number; lineageIdHash: string } | undefined;
|
|
12170
|
+
// The purge snapshot is consumed per-admission inside the terminal
|
|
12171
|
+
// block; the rearm below (reachable only when a scope was registered,
|
|
12172
|
+
// i.e. only after the purge ran) reads the same value.
|
|
12173
|
+
let abortSteeringSnapshot: number | undefined;
|
|
12174
|
+
if (options.terminal) {
|
|
12175
|
+
// The terminal scope must be registered for the ABORTED turn's
|
|
12176
|
+
// lineage, never a successor's. A new root turn advances the attempt
|
|
12177
|
+
// epoch at admission, so when the caller captured the epoch before a
|
|
12178
|
+
// durable claim and a successor won that window, the current epoch no
|
|
12179
|
+
// longer matches — fail closed and never fence or purge the
|
|
12180
|
+
// unrelated successor's queues (review thread P1). Callers that do
|
|
12181
|
+
// not pass an expected epoch (direct seam users) retain the prior
|
|
12182
|
+
// register-regardless behavior.
|
|
12183
|
+
if (
|
|
12184
|
+
options.terminal.expectedEpoch !== undefined &&
|
|
12185
|
+
options.terminal.expectedEpoch !== this.#promptGeneration
|
|
12186
|
+
) {
|
|
12187
|
+
return { status: "unfenced", reason: "unknown_run", pending: [] };
|
|
12188
|
+
}
|
|
12189
|
+
const requestedSnapshotToken = options.terminal.steeringSnapshotToken;
|
|
12190
|
+
const currentSnapshotKey =
|
|
12191
|
+
this.#turnLineageIdHash === undefined ? undefined : `${this.#turnLineageIdHash}:${this.#promptGeneration}`;
|
|
12192
|
+
if (requestedSnapshotToken !== undefined) {
|
|
12193
|
+
const capturedKey = this.#terminalAbortSteeringSnapshotKeys.get(requestedSnapshotToken);
|
|
12194
|
+
const capturedQueue = capturedKey ? this.#terminalAbortSteeringSnapshots.get(capturedKey) : undefined;
|
|
12195
|
+
if (
|
|
12196
|
+
capturedKey !== currentSnapshotKey ||
|
|
12197
|
+
!capturedQueue?.some(entry => entry.token === requestedSnapshotToken)
|
|
12198
|
+
) {
|
|
12199
|
+
this.discardTerminalAbortSteeringSnapshot(requestedSnapshotToken);
|
|
12200
|
+
return { status: "unfenced", reason: "unknown_run", pending: [] };
|
|
12201
|
+
}
|
|
12202
|
+
}
|
|
12203
|
+
// Terminal abort (C04 mode:"terminal"): register and synchronously
|
|
12204
|
+
// close the continuation fence for the current turn BEFORE the run is
|
|
12205
|
+
// interrupted, so a later left-running owned completion classifies as
|
|
12206
|
+
// owned-completion by exact source (lineage + attempt epoch). The
|
|
12207
|
+
// owned-completion policy stays enabled for scope:"turn" — delivery
|
|
12208
|
+
// intentionally resumes the agent — and disabled for scope:"owned".
|
|
12209
|
+
// A missing lineage (no active turn) fails closed: no scope is
|
|
12210
|
+
// registered, so nothing is attributed.
|
|
12211
|
+
const lineageIdHash = this.#turnLineageIdHash;
|
|
12212
|
+
if (lineageIdHash) {
|
|
12213
|
+
const scope = registerTerminalTurnScope({
|
|
12214
|
+
lineageIdHash,
|
|
12215
|
+
promptAttemptEpoch: this.#promptGeneration,
|
|
12216
|
+
ownedCompletionPolicy: options.terminal.scope === "owned" ? "disabled" : "enabled",
|
|
12217
|
+
});
|
|
12218
|
+
// Admission can fail closed (registry saturation): the abort
|
|
12219
|
+
// proceeds without a scope, so nothing is attributed.
|
|
12220
|
+
if (scope) {
|
|
12221
|
+
registeredScope = {
|
|
12222
|
+
scopeId: scope.scopeId,
|
|
12223
|
+
abortedAttemptEpoch: scope.promptAttemptEpoch,
|
|
12224
|
+
lineageIdHash: scope.lineageIdHash,
|
|
12225
|
+
};
|
|
12226
|
+
}
|
|
12227
|
+
// Terminal abort blocks STEERING continuations of the aborted turn:
|
|
12228
|
+
// purge steering queued just before the abort won (the loop can
|
|
12229
|
+
// exit on the abort signal without polling it) so it cannot alter
|
|
12230
|
+
// the next user turn (review thread P2). The follow-up queue is
|
|
12231
|
+
// preserved — owned-completion resumes must still deliver.
|
|
12232
|
+
const turnSnapshotKey = `${lineageIdHash}:${this.#promptGeneration}`;
|
|
12233
|
+
const queuedTurnSnapshots = this.#terminalAbortSteeringSnapshots.get(turnSnapshotKey);
|
|
12234
|
+
const snapshotIndex =
|
|
12235
|
+
requestedSnapshotToken === undefined
|
|
12236
|
+
? 0
|
|
12237
|
+
: (queuedTurnSnapshots?.findIndex(entry => entry.token === requestedSnapshotToken) ?? -1);
|
|
12238
|
+
const consumedSnapshot =
|
|
12239
|
+
queuedTurnSnapshots && snapshotIndex >= 0 ? queuedTurnSnapshots.splice(snapshotIndex, 1)[0] : undefined;
|
|
12240
|
+
if (consumedSnapshot) this.#terminalAbortSteeringSnapshotKeys.delete(consumedSnapshot.token);
|
|
12241
|
+
abortSteeringSnapshot = consumedSnapshot?.seq ?? this.#steeringAdmissionSeq;
|
|
12242
|
+
if (queuedTurnSnapshots?.length === 0) {
|
|
12243
|
+
this.#terminalAbortSteeringSnapshots.delete(turnSnapshotKey);
|
|
12244
|
+
}
|
|
12245
|
+
const admittedAfterSnapshot = (message: AgentMessage): boolean =>
|
|
12246
|
+
(this.#externalSteerAdmissionSeq.get(message) ?? 0) > (abortSteeringSnapshot ?? -1);
|
|
12247
|
+
// Steering purge, STEERING queue only: the agent-wide removal
|
|
12248
|
+
// would also wipe the follow-up queue, which owned-completion
|
|
12249
|
+
// resumes must still deliver (review thread P1).
|
|
12250
|
+
const steeringBeforePurge = this.agent.snapshotSteering();
|
|
12251
|
+
this.agent.removeQueuedMessages(
|
|
12252
|
+
message => !(this.#externalSteerMessages.has(message) && admittedAfterSnapshot(message)),
|
|
12253
|
+
"steering",
|
|
12254
|
+
);
|
|
12255
|
+
// Keep the display list aligned with the ACTUAL purge decisions:
|
|
12256
|
+
// preserved post-snapshot external steers stay visible and
|
|
12257
|
+
// purged internal steers disappear — a stale entry would let the
|
|
12258
|
+
// positional editing APIs remove a different preserved steer
|
|
12259
|
+
// (review thread P2).
|
|
12260
|
+
const steeringAfterPurge = new Set(this.agent.snapshotSteering());
|
|
12261
|
+
const keptSteeringDisplays: QueuedDisplayEntry[] = [];
|
|
12262
|
+
for (let displayIndex = 0; displayIndex < steeringBeforePurge.length; displayIndex++) {
|
|
12263
|
+
if (steeringAfterPurge.has(steeringBeforePurge[displayIndex]!)) {
|
|
12264
|
+
keptSteeringDisplays.push(this.#steeringMessages[displayIndex]!);
|
|
12265
|
+
}
|
|
12266
|
+
}
|
|
12267
|
+
this.#steeringMessages = keptSteeringDisplays;
|
|
12268
|
+
|
|
12269
|
+
// Remove ORDINARY follow-ups queued for the aborted turn: the
|
|
12270
|
+
// aborted loop exits without polling them, so a later unrelated
|
|
12271
|
+
// prompt could drain and execute the supposedly-terminated turn's
|
|
12272
|
+
// command. Only authorized owned-completion envelopes survive
|
|
12273
|
+
// (review thread P1).
|
|
12274
|
+
const followUpBeforePurge = this.agent.snapshotFollowUp();
|
|
12275
|
+
this.agent.removeQueuedMessages(message => {
|
|
12276
|
+
// An SDK/client follow-up independently requested the next
|
|
12277
|
+
// root turn; it is not a continuation caused by the
|
|
12278
|
+
// aborted turn.
|
|
12279
|
+
if (this.#externalFollowUps.has(message)) return false;
|
|
12280
|
+
// Preserve only envelopes that are CURRENTLY AUTHORIZED as
|
|
12281
|
+
// owned completions (fresh or drop). The public
|
|
12282
|
+
// sendMessage() accepts arbitrary details, so the mere
|
|
12283
|
+
// presence of the ownedCompletions field is not authority —
|
|
12284
|
+
// an envelope with no matching terminal scope classifies
|
|
12285
|
+
// ordinary and must NOT survive the abort (review thread P2).
|
|
12286
|
+
return ownedCompletionResumeAction(message as never) === "ordinary";
|
|
12287
|
+
}, "followUp");
|
|
12288
|
+
// Mirror the follow-up purge in the display list so stale entries
|
|
12289
|
+
// cannot misalign the positional editing APIs (review thread P2).
|
|
12290
|
+
const followUpAfterPurge = new Set(this.agent.snapshotFollowUp());
|
|
12291
|
+
const keptFollowUpDisplays: QueuedDisplayEntry[] = [];
|
|
12292
|
+
for (let displayIndex = 0; displayIndex < followUpBeforePurge.length; displayIndex++) {
|
|
12293
|
+
if (followUpAfterPurge.has(followUpBeforePurge[displayIndex]!)) {
|
|
12294
|
+
keptFollowUpDisplays.push(this.#followUpMessages[displayIndex]!);
|
|
12295
|
+
}
|
|
12296
|
+
}
|
|
12297
|
+
this.#followUpMessages = keptFollowUpDisplays;
|
|
12298
|
+
}
|
|
12299
|
+
// Do NOT advance the attempt epoch here: the terminal scope must stay
|
|
12300
|
+
// keyed to the aborted epoch so #isTurnContinuationBlocked (which
|
|
12301
|
+
// looks up by the CURRENT #promptGeneration) still finds the closed
|
|
12302
|
+
// fence for hidden-next-turn and other non-generation-guarded
|
|
12303
|
+
// continuation paths. Fresh-turn allocation is the per-admission
|
|
12304
|
+
// responsibility: every NEW ROOT TURN (#promptWithMessage with
|
|
12305
|
+
// resetRetryReplaySafety) advances the epoch before minting, so the
|
|
12306
|
+
// next turn after the abort gets a distinct (lineage, epoch) and is
|
|
12307
|
+
// never captured by this scope (review thread P2).
|
|
12308
|
+
this.#promptPreflightCancellationGeneration++;
|
|
12309
|
+
this.#promptPreflightAbortController.abort();
|
|
12310
|
+
this.#promptPreflightAbortController = new AbortController();
|
|
12311
|
+
}
|
|
10359
12312
|
const aborted = this.#runCancellationDomains.abort(handle);
|
|
10360
12313
|
if (!aborted.ok) {
|
|
10361
12314
|
if (aborted.reason === "quarantined") {
|
|
10362
|
-
return
|
|
12315
|
+
return {
|
|
12316
|
+
...(await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: 0 })),
|
|
12317
|
+
...(registeredScope ? { terminalScope: registeredScope } : {}),
|
|
12318
|
+
};
|
|
10363
12319
|
}
|
|
10364
|
-
return {
|
|
12320
|
+
return {
|
|
12321
|
+
status: "unfenced",
|
|
12322
|
+
reason: "unknown_run",
|
|
12323
|
+
pending: [],
|
|
12324
|
+
...(registeredScope ? { terminalScope: registeredScope } : {}),
|
|
12325
|
+
};
|
|
10365
12326
|
}
|
|
10366
12327
|
if (handle === this.agent.activeResourceRunId) this.agent.abort();
|
|
10367
12328
|
const proof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: options.graceMs });
|
|
10368
12329
|
if (proof.status === "unfenced") this.agent.resourceLedger.quarantine(handle);
|
|
10369
|
-
|
|
12330
|
+
// Rearm surviving owned-completion follow-ups once the abort has
|
|
12331
|
+
// settled: the aborted loop exits before polling the follow-up queue,
|
|
12332
|
+
// so preserving the envelopes alone leaves them stranded until an
|
|
12333
|
+
// unrelated prompt. Schedule a continue so they are consumed as a
|
|
12334
|
+
// fresh turn (review thread P1).
|
|
12335
|
+
if (options.terminal && registeredScope) {
|
|
12336
|
+
// Rearm ONLY envelopes the classifier marks fresh. A drop-classified
|
|
12337
|
+
// envelope (a denied owned completion under scope:"owned") must never
|
|
12338
|
+
// reach the model: purge it from the follow-up queue BEFORE the
|
|
12339
|
+
// rearm's continue is scheduled, because the agent loop dequeues
|
|
12340
|
+
// follow-ups DIRECTLY and consumes the whole queue at once — a
|
|
12341
|
+
// denied envelope left queued alongside the fresh completion would
|
|
12342
|
+
// otherwise be passed to #runLoop with it (review thread P1). The
|
|
12343
|
+
// onFollowUpConsumed filter at agent.ts continue() is the second
|
|
12344
|
+
// line of defense for any envelope queued in the window after this
|
|
12345
|
+
// purge and before the dequeue.
|
|
12346
|
+
const followUpSnapshot = this.agent.snapshotQueues();
|
|
12347
|
+
const droppedFollowUps: AgentMessage[] = [];
|
|
12348
|
+
const purgedFollowUps = followUpSnapshot.followUp.filter(message => {
|
|
12349
|
+
if (ownedCompletionResumeAction(message as never) === "drop") {
|
|
12350
|
+
droppedFollowUps.push(message);
|
|
12351
|
+
return false;
|
|
12352
|
+
}
|
|
12353
|
+
return true;
|
|
12354
|
+
});
|
|
12355
|
+
if (droppedFollowUps.length > 0) {
|
|
12356
|
+
this.agent.restoreQueues({ ...followUpSnapshot, followUp: purgedFollowUps });
|
|
12357
|
+
// The denied completion is terminal; settle its registration so
|
|
12358
|
+
// the tuple does not occupy global capacity while the fresh
|
|
12359
|
+
// completion resumes (review thread P2).
|
|
12360
|
+
this.#settleDeliveredOwnedRegistrations(droppedFollowUps);
|
|
12361
|
+
}
|
|
12362
|
+
const hasPreservedFollowUp = () =>
|
|
12363
|
+
this.agent
|
|
12364
|
+
.snapshotQueues()
|
|
12365
|
+
.followUp.some(
|
|
12366
|
+
message =>
|
|
12367
|
+
ownedCompletionResumeAction(message as never) === "fresh" || this.#externalFollowUps.has(message),
|
|
12368
|
+
);
|
|
12369
|
+
// A preserved post-snapshot client steer is also an independent
|
|
12370
|
+
// root-turn request: the abort exits the loop and never polls the
|
|
12371
|
+
// steering queue, so without a rearm the accepted steer stays
|
|
12372
|
+
// stranded until unrelated activity (review thread P1).
|
|
12373
|
+
const hasPreservedSteering = (): boolean =>
|
|
12374
|
+
this.agent
|
|
12375
|
+
.snapshotQueues()
|
|
12376
|
+
.steering.some(
|
|
12377
|
+
message =>
|
|
12378
|
+
this.#externalSteerMessages.has(message) &&
|
|
12379
|
+
(this.#externalSteerAdmissionSeq.get(message) ?? 0) > (abortSteeringSnapshot ?? -1),
|
|
12380
|
+
);
|
|
12381
|
+
if (hasPreservedFollowUp() || hasPreservedSteering()) {
|
|
12382
|
+
// ANY preserved follow-up or steer — an owned-completion resume,
|
|
12383
|
+
// an independently requested external follow-up, or a
|
|
12384
|
+
// post-snapshot client steer — needs a fresh lineage before its
|
|
12385
|
+
// continue: without it the scheduled continuation runs under the
|
|
12386
|
+
// aborted lineage+epoch and the terminal fence skips it as
|
|
12387
|
+
// terminal_turn, stranding the preserved user request until
|
|
12388
|
+
// unrelated activity. Minting the fresh lineage does not
|
|
12389
|
+
// reclassify the external message as agent-initiated owned work;
|
|
12390
|
+
// it only unbinds the new turn from the aborted attempt (review
|
|
12391
|
+
// thread P1).
|
|
12392
|
+
this.#resumeFromOwnedCompletion();
|
|
12393
|
+
this.#scheduleAgentContinue({
|
|
12394
|
+
delayMs: 1,
|
|
12395
|
+
generation: this.#promptGeneration,
|
|
12396
|
+
shouldContinue: () => hasPreservedFollowUp() || hasPreservedSteering(),
|
|
12397
|
+
// The rearmed continuation must consume the queued steer inside
|
|
12398
|
+
// the run acceptance (continueQueuedMessages) rather than
|
|
12399
|
+
// dequeue it later via getSteeringMessages: a terminal abort
|
|
12400
|
+
// leaves a non-assistant history tail (tool result), which
|
|
12401
|
+
// would otherwise select Agent.continue() and accept the run
|
|
12402
|
+
// with an empty consumedQueuedMessages payload, so
|
|
12403
|
+
// #fireQueuedPromotionHooks never fires and the submitting
|
|
12404
|
+
// connection is not recorded as an owner — its later terminal
|
|
12405
|
+
// abort is rejected as an owner mismatch (review thread P1).
|
|
12406
|
+
// The preserved messages are still re-verified by
|
|
12407
|
+
// shouldContinue at continuation time, so a steer removed
|
|
12408
|
+
// during the delay never fires a stale hook and a post-snapshot
|
|
12409
|
+
// steer admitted during the delay is not missed.
|
|
12410
|
+
continueQueuedOnly: true,
|
|
12411
|
+
rescheduleOnBusy: true,
|
|
12412
|
+
});
|
|
12413
|
+
}
|
|
12414
|
+
}
|
|
12415
|
+
return {
|
|
12416
|
+
...proof,
|
|
12417
|
+
...(registeredScope ? { terminalScope: registeredScope } : {}),
|
|
12418
|
+
};
|
|
10370
12419
|
}
|
|
10371
12420
|
|
|
10372
12421
|
/** Atomically interrupt the active run and make text the next prompt. */
|
|
@@ -10514,6 +12563,7 @@ export class AgentSession {
|
|
|
10514
12563
|
resetRetryReplaySafety: true,
|
|
10515
12564
|
onRunAccepted: () => {
|
|
10516
12565
|
runAccepted = true;
|
|
12566
|
+
if (selected) this.#fireQueuedPromotionHooks([message]);
|
|
10517
12567
|
if (selected) {
|
|
10518
12568
|
this.#steeringMessages = this.#steeringMessages.filter(entry => entry !== selected.display);
|
|
10519
12569
|
this.#followUpMessages = this.#followUpMessages.filter(entry => entry !== selected.display);
|
|
@@ -10596,7 +12646,7 @@ export class AgentSession {
|
|
|
10596
12646
|
}
|
|
10597
12647
|
}
|
|
10598
12648
|
|
|
10599
|
-
const manager = AsyncJobManager.instance();
|
|
12649
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
10600
12650
|
const ownerId = this.#agentId;
|
|
10601
12651
|
const lease = manager && ownerId ? manager.beginOwnerSubagentShutdown(ownerId) : undefined;
|
|
10602
12652
|
if (manager && ownerId && !lease) {
|
|
@@ -10622,11 +12672,18 @@ export class AgentSession {
|
|
|
10622
12672
|
this.#rebindProviderSessionState(new Map());
|
|
10623
12673
|
this.agent.reset();
|
|
10624
12674
|
if (!options?.drop) await this.sessionManager.flush();
|
|
12675
|
+
const noLeasePreviousSessionIdentity = this.sessionManager.getSessionId();
|
|
12676
|
+
const noLeasePreviousSessionFile = this.sessionManager.getSessionFile();
|
|
10625
12677
|
const prepared = await this.sessionManager.prepareNewSession(options);
|
|
10626
12678
|
try {
|
|
10627
12679
|
// Last fallible gate while public getters still show the predecessor (#3138).
|
|
10628
12680
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
12681
|
+
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
10629
12682
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
12683
|
+
// Endpoint identity committed to the successor: re-register the
|
|
12684
|
+
// manager so post-transition lineage bindings resolve and owned
|
|
12685
|
+
// aborts classify in the successor session (review thread P1).
|
|
12686
|
+
this.#rekeyJobManagerForSessionIdentity(noLeasePreviousSessionIdentity, noLeasePreviousSessionFile);
|
|
10630
12687
|
await this.#runToolSessionTransitionCleanups();
|
|
10631
12688
|
} catch (error) {
|
|
10632
12689
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
@@ -10654,6 +12711,7 @@ export class AgentSession {
|
|
|
10654
12711
|
if (!manager) throw new Error("Owner subagent shutdown manager became unavailable.");
|
|
10655
12712
|
if (!ownerId) throw new Error("Owner subagent shutdown owner became unavailable.");
|
|
10656
12713
|
const previousSessionIdentity = this.sessionManager.getSessionId();
|
|
12714
|
+
const previousSessionIdentityFile = this.sessionManager.getSessionFile();
|
|
10657
12715
|
try {
|
|
10658
12716
|
try {
|
|
10659
12717
|
manager.runOwnerProducerCleanupsStrict({ ownerId });
|
|
@@ -10696,7 +12754,12 @@ export class AgentSession {
|
|
|
10696
12754
|
try {
|
|
10697
12755
|
// Last fallible gate while public getters still show the predecessor (#3138).
|
|
10698
12756
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
12757
|
+
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
10699
12758
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
12759
|
+
// Endpoint identity committed to the successor: re-register the
|
|
12760
|
+
// manager so post-transition lineage bindings resolve and owned
|
|
12761
|
+
// aborts classify in the successor session (review thread P1).
|
|
12762
|
+
this.#rekeyJobManagerForSessionIdentity(previousSessionIdentity, previousSessionIdentityFile);
|
|
10700
12763
|
await this.#runToolSessionTransitionCleanups();
|
|
10701
12764
|
} catch (error) {
|
|
10702
12765
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
@@ -10871,6 +12934,7 @@ export class AgentSession {
|
|
|
10871
12934
|
try {
|
|
10872
12935
|
const previousSessionFile = this.sessionFile;
|
|
10873
12936
|
const previousWorkflowGateSessionId = this.sessionId;
|
|
12937
|
+
const previousSessionIdentity = this.sessionManager.getSessionId();
|
|
10874
12938
|
|
|
10875
12939
|
// Emit session_before_switch event with reason "fork" (can be cancelled)
|
|
10876
12940
|
if (this.#extensionRunner?.hasHandlers("session_before_switch")) {
|
|
@@ -10907,6 +12971,7 @@ export class AgentSession {
|
|
|
10907
12971
|
getSessionId: () => forkedManager.getSessionId(),
|
|
10908
12972
|
});
|
|
10909
12973
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
12974
|
+
this.#assertJobManagerEndpointAdmission(forkedManager.getSessionId(), forkedManager.getSessionFile());
|
|
10910
12975
|
} catch (error) {
|
|
10911
12976
|
const forkedFile = forkedManager.getSessionFile();
|
|
10912
12977
|
const cleanupErrors: unknown[] = [];
|
|
@@ -10937,6 +13002,7 @@ export class AgentSession {
|
|
|
10937
13002
|
throw error;
|
|
10938
13003
|
}
|
|
10939
13004
|
this.sessionManager = forkedManager;
|
|
13005
|
+
this.#rekeyJobManagerForSessionIdentity(previousSessionIdentity, previousSessionFile);
|
|
10940
13006
|
try {
|
|
10941
13007
|
await previousManager.close();
|
|
10942
13008
|
} catch (error) {
|
|
@@ -10953,7 +13019,11 @@ export class AgentSession {
|
|
|
10953
13019
|
try {
|
|
10954
13020
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
10955
13021
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
13022
|
+
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
10956
13023
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
13024
|
+
// Fork commits a successor endpoint identity; re-register the
|
|
13025
|
+
// manager under it (review thread P1).
|
|
13026
|
+
this.#rekeyJobManagerForSessionIdentity(previousSessionIdentity, previousSessionFile);
|
|
10957
13027
|
await this.#runToolSessionTransitionCleanups();
|
|
10958
13028
|
} catch (error) {
|
|
10959
13029
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
@@ -11054,6 +13124,27 @@ export class AgentSession {
|
|
|
11054
13124
|
return this.#activeModelProfile;
|
|
11055
13125
|
}
|
|
11056
13126
|
|
|
13127
|
+
/**
|
|
13128
|
+
* Re-apply vendor-separated delegation after a profile activation changed the
|
|
13129
|
+
* role layer. `eagerTasks` is resolved at prompt build time, but under
|
|
13130
|
+
* `tools.discoveryMode: all` the `task` tool is hidden until something
|
|
13131
|
+
* activates it, and the delegation directive stays behind its
|
|
13132
|
+
* `has tools "task"` guard. Activating a vendor-separated profile mid-session
|
|
13133
|
+
* must therefore reach the live tool set, not just the next session.
|
|
13134
|
+
*/
|
|
13135
|
+
async syncEagerDelegation(): Promise<void> {
|
|
13136
|
+
const { eagerTasks } = resolveEagerTaskDelegation({
|
|
13137
|
+
settings: this.settings,
|
|
13138
|
+
profile: this.#activeModelProfile
|
|
13139
|
+
? this.#modelRegistry.getModelProfile?.(this.#activeModelProfile)
|
|
13140
|
+
: undefined,
|
|
13141
|
+
});
|
|
13142
|
+
if (eagerTasks && this.#toolRegistry.has("task") && !this.getActiveToolNames().includes("task")) {
|
|
13143
|
+
await this.activateDiscoveredTools(["task"]);
|
|
13144
|
+
}
|
|
13145
|
+
await this.refreshBaseSystemPrompt();
|
|
13146
|
+
}
|
|
13147
|
+
|
|
11057
13148
|
/** Resolver intent only for assignments owned by the active profile. */
|
|
11058
13149
|
#persistedModelProfileAliasIntent(role: string): { aliasIntent: "preset-equivalent" } | undefined {
|
|
11059
13150
|
if (!this.#activeModelProfile) return undefined;
|
|
@@ -11201,11 +13292,10 @@ export class AgentSession {
|
|
|
11201
13292
|
onAfterActivation?: () => void;
|
|
11202
13293
|
},
|
|
11203
13294
|
): Promise<{ changed: boolean; id: string }> {
|
|
13295
|
+
// Do not hold selection admission while waiting for a scheduled continuation:
|
|
13296
|
+
// the continuation may need prompt admission to settle the current turn.
|
|
13297
|
+
await this.waitForIdle();
|
|
11204
13298
|
const canonicalName = await this.#withSessionAdmission("selection", async () => {
|
|
11205
|
-
// A model-picker action must not alter an in-flight generation: wait
|
|
11206
|
-
// for the current run to settle inside the admission before applying
|
|
11207
|
-
// the profile, matching `setDefaultModelSelection`.
|
|
11208
|
-
await this.waitForIdle();
|
|
11209
13299
|
const profiles = this.#modelRegistry.getModelProfiles();
|
|
11210
13300
|
let canonical: string;
|
|
11211
13301
|
try {
|
|
@@ -11292,10 +13382,10 @@ export class AgentSession {
|
|
|
11292
13382
|
* profile activation and default-model selection.
|
|
11293
13383
|
*/
|
|
11294
13384
|
async withSdkControlMutation<T>(body: () => Promise<T>): Promise<T> {
|
|
13385
|
+
// Waiting while selection owns admission deadlocks with a scheduled
|
|
13386
|
+
// continuation queued behind it. Wait before acquiring the mutation lease.
|
|
13387
|
+
await this.waitForIdle();
|
|
11295
13388
|
return this.#withSessionAdmission("selection", async () => {
|
|
11296
|
-
// A config mutation can change the active model-role assignment. Do not
|
|
11297
|
-
// alter an in-flight turn after its prompt admission lease has released.
|
|
11298
|
-
await this.waitForIdle();
|
|
11299
13389
|
return await body();
|
|
11300
13390
|
});
|
|
11301
13391
|
}
|
|
@@ -11647,95 +13737,148 @@ export class AgentSession {
|
|
|
11647
13737
|
onAfterMutation?: () => void;
|
|
11648
13738
|
},
|
|
11649
13739
|
): Promise<DefaultModelSelectionResult> {
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
11667
|
-
|
|
11668
|
-
|
|
11669
|
-
|
|
11670
|
-
|
|
11671
|
-
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
11675
|
-
|
|
11676
|
-
|
|
11677
|
-
|
|
11678
|
-
|
|
11679
|
-
|
|
11680
|
-
|
|
13740
|
+
// Reserve the causal selection fence synchronously, before credential
|
|
13741
|
+
// probing can yield. Later external prompts wait behind this operation,
|
|
13742
|
+
// while an inherited post-prompt continuation may bypass the fence so
|
|
13743
|
+
// waitForIdle never deadlocks waiting for prompt admission.
|
|
13744
|
+
const owner = this.#sessionAdmissionContext.getStore();
|
|
13745
|
+
if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
|
|
13746
|
+
if (this.#sessionAdmissionClosing || this.#sessionAdmissionClosed || this.#isDisposed) {
|
|
13747
|
+
throw this.#sessionAdmissionBusyError();
|
|
13748
|
+
}
|
|
13749
|
+
if (thinkingLevel === ThinkingLevel.Inherit) {
|
|
13750
|
+
throw new Error("Default model selection cannot inherit a thinking level");
|
|
13751
|
+
}
|
|
13752
|
+
const expectedSessionId = this.sessionId;
|
|
13753
|
+
const priorSelectionFence = this.#selectionFenceTail;
|
|
13754
|
+
const selectionFence = Promise.withResolvers<void>();
|
|
13755
|
+
this.#selectionFenceGeneration += 1;
|
|
13756
|
+
this.#pendingSelectionFences += 1;
|
|
13757
|
+
const selectionFenceGeneration = this.#selectionFenceGeneration;
|
|
13758
|
+
if (this.#oldestPendingSelectionFenceGeneration === 0) {
|
|
13759
|
+
this.#oldestPendingSelectionFenceGeneration = selectionFenceGeneration;
|
|
13760
|
+
}
|
|
13761
|
+
this.#selectionFenceTail = priorSelectionFence.then(() => selectionFence.promise);
|
|
13762
|
+
void this.#selectionFenceTail.catch(() => {});
|
|
13763
|
+
try {
|
|
13764
|
+
await priorSelectionFence;
|
|
13765
|
+
const { effectiveLevel } = await this.#withSessionAdmission(
|
|
13766
|
+
"selection",
|
|
13767
|
+
async () => {
|
|
13768
|
+
if (this.sessionId !== expectedSessionId) {
|
|
13769
|
+
throw new Error("Session changed while selecting model");
|
|
13770
|
+
}
|
|
13771
|
+
const apiKey = await this.#modelRegistry.getApiKey(model, this.credentialSessionId);
|
|
13772
|
+
if (this.sessionId !== expectedSessionId) {
|
|
13773
|
+
throw new Error("Session changed while selecting model");
|
|
13774
|
+
}
|
|
13775
|
+
if (!apiKey) {
|
|
13776
|
+
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
13777
|
+
}
|
|
13778
|
+
const resolvedLevel = resolveThinkingLevelForModel(model, thinkingLevel);
|
|
13779
|
+
return {
|
|
13780
|
+
effectiveLevel:
|
|
13781
|
+
resolvedLevel ??
|
|
13782
|
+
resolveThinkingLevelForModel(model, model.thinking?.defaultLevel ?? this.thinkingLevel) ??
|
|
13783
|
+
ThinkingLevel.Off,
|
|
13784
|
+
};
|
|
13785
|
+
},
|
|
13786
|
+
undefined,
|
|
13787
|
+
undefined,
|
|
13788
|
+
{ allowDuringClosing: true },
|
|
11681
13789
|
);
|
|
11682
|
-
|
|
11683
|
-
|
|
11684
|
-
|
|
11685
|
-
|
|
11686
|
-
|
|
11687
|
-
|
|
11688
|
-
|
|
11689
|
-
|
|
11690
|
-
await this.sessionManager.
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
|
|
11697
|
-
|
|
11698
|
-
|
|
11699
|
-
|
|
11700
|
-
|
|
11701
|
-
|
|
11702
|
-
|
|
11703
|
-
|
|
11704
|
-
);
|
|
11705
|
-
}
|
|
11706
|
-
const promotion = this.sessionManager.promoteDefaultModelSelection(stage);
|
|
11707
|
-
switch (promotion.kind) {
|
|
11708
|
-
case "promoted": {
|
|
11709
|
-
this.#publishDefaultModelSelection(model, effectiveLevel, preparedSystemPrompt);
|
|
11710
|
-
break;
|
|
11711
|
-
}
|
|
11712
|
-
case "not_promoted": {
|
|
11713
|
-
return this.#throwDefaultModelSelectionRecovery(
|
|
11714
|
-
promotion.error ?? new Error("Default model selection was superseded before session promotion"),
|
|
11715
|
-
stage,
|
|
11716
|
-
durableCommit,
|
|
13790
|
+
await this.waitForIdle(selectionFenceGeneration);
|
|
13791
|
+
return await this.#withSessionAdmission(
|
|
13792
|
+
"selection",
|
|
13793
|
+
async () => {
|
|
13794
|
+
options?.onBeforeMutation?.();
|
|
13795
|
+
if (this.sessionId !== expectedSessionId) {
|
|
13796
|
+
throw new Error("Session changed while selecting model");
|
|
13797
|
+
}
|
|
13798
|
+
await this.sessionManager.flush();
|
|
13799
|
+
await this.#waitForAdmittedBaseSystemPromptRebuilds();
|
|
13800
|
+
if (this.sessionId !== expectedSessionId) {
|
|
13801
|
+
throw new Error("Session changed while selecting model");
|
|
13802
|
+
}
|
|
13803
|
+
const expectedMutationRevision = this.#defaultModelSelectionMutationRevision;
|
|
13804
|
+
const preparedSystemPrompt = await this.#prepareDefaultModelSelectionPrompt(model);
|
|
13805
|
+
if (this.sessionId !== expectedSessionId) {
|
|
13806
|
+
throw new Error("Session changed while selecting model");
|
|
13807
|
+
}
|
|
13808
|
+
const stage = await this.sessionManager.stageDefaultModelSelection(
|
|
13809
|
+
`${model.provider}/${model.id}`,
|
|
13810
|
+
effectiveLevel,
|
|
13811
|
+
{ appendThinkingLevel: true },
|
|
11717
13812
|
);
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
|
|
11727
|
-
|
|
11728
|
-
|
|
13813
|
+
let durableCommit: CasReceipt;
|
|
13814
|
+
try {
|
|
13815
|
+
const selector = formatModelSelectorValue(`${model.provider}/${model.id}`, effectiveLevel);
|
|
13816
|
+
durableCommit = await this.settings.commitAtomicBatchWithCurrent(() => [
|
|
13817
|
+
{ path: "modelRoles.default" as SettingPath, op: "set", value: selector },
|
|
13818
|
+
]);
|
|
13819
|
+
} catch (error) {
|
|
13820
|
+
try {
|
|
13821
|
+
await this.sessionManager.discardDefaultModelSelectionStage(stage);
|
|
13822
|
+
} catch (cleanupError) {
|
|
13823
|
+
throw new AggregateError(
|
|
13824
|
+
[error, cleanupError],
|
|
13825
|
+
"Default model selection persistence and staged session cleanup both failed.",
|
|
13826
|
+
);
|
|
13827
|
+
}
|
|
13828
|
+
throw error;
|
|
13829
|
+
}
|
|
13830
|
+
if (this.#defaultModelSelectionMutationRevision !== expectedMutationRevision) {
|
|
13831
|
+
await this.#throwDefaultModelSelectionRecovery(
|
|
13832
|
+
new Error("Default model selection was superseded before session promotion"),
|
|
13833
|
+
stage,
|
|
13834
|
+
durableCommit,
|
|
13835
|
+
);
|
|
13836
|
+
}
|
|
13837
|
+
const promotion = this.sessionManager.promoteDefaultModelSelection(stage);
|
|
13838
|
+
switch (promotion.kind) {
|
|
13839
|
+
case "promoted": {
|
|
13840
|
+
this.#publishDefaultModelSelection(model, effectiveLevel, preparedSystemPrompt);
|
|
13841
|
+
break;
|
|
13842
|
+
}
|
|
13843
|
+
case "not_promoted": {
|
|
13844
|
+
return this.#throwDefaultModelSelectionRecovery(
|
|
13845
|
+
promotion.error ?? new Error("Default model selection was superseded before session promotion"),
|
|
13846
|
+
stage,
|
|
13847
|
+
durableCommit,
|
|
13848
|
+
);
|
|
13849
|
+
}
|
|
13850
|
+
case "unknown": {
|
|
13851
|
+
const message = "Session replacement outcome could not be determined.";
|
|
13852
|
+
throw new DefaultModelSelectionRecoveryError(message, {
|
|
13853
|
+
message,
|
|
13854
|
+
rollback: {
|
|
13855
|
+
disposition: "unknown",
|
|
13856
|
+
failures: [
|
|
13857
|
+
{
|
|
13858
|
+
stage: "session",
|
|
13859
|
+
message: "Session replacement outcome could not be determined.",
|
|
13860
|
+
},
|
|
13861
|
+
],
|
|
11729
13862
|
},
|
|
11730
|
-
|
|
11731
|
-
}
|
|
11732
|
-
}
|
|
11733
|
-
|
|
13863
|
+
});
|
|
13864
|
+
}
|
|
13865
|
+
}
|
|
13866
|
+
this.#clearActiveModelProfileForConcreteDefault("user-selection");
|
|
13867
|
+
options?.onAfterMutation?.();
|
|
13868
|
+
return { provider: model.provider, modelId: model.id, thinkingLevel: effectiveLevel };
|
|
13869
|
+
},
|
|
13870
|
+
undefined,
|
|
13871
|
+
undefined,
|
|
13872
|
+
{ allowDuringClosing: true },
|
|
13873
|
+
);
|
|
13874
|
+
} finally {
|
|
13875
|
+
selectionFence.resolve();
|
|
13876
|
+
this.#pendingSelectionFences -= 1;
|
|
13877
|
+
if (this.#oldestPendingSelectionFenceGeneration === selectionFenceGeneration) {
|
|
13878
|
+
this.#oldestPendingSelectionFenceGeneration = 0;
|
|
11734
13879
|
}
|
|
11735
|
-
this.#
|
|
11736
|
-
|
|
11737
|
-
return { provider: model.provider, modelId: model.id, thinkingLevel: effectiveLevel };
|
|
11738
|
-
});
|
|
13880
|
+
this.#resolveSessionSettlement();
|
|
13881
|
+
}
|
|
11739
13882
|
}
|
|
11740
13883
|
/**
|
|
11741
13884
|
* Cycle to next/previous model.
|
|
@@ -13177,9 +15320,13 @@ export class AgentSession {
|
|
|
13177
15320
|
// Last fallible action: verified local:// readiness from immutable staged options.
|
|
13178
15321
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
13179
15322
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
15323
|
+
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
13180
15324
|
|
|
13181
15325
|
// --- Commit boundary: synchronous adoption is the sole identity publication.
|
|
13182
15326
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
15327
|
+
// Handoff commits a successor endpoint identity; re-register the
|
|
15328
|
+
// manager under it (review thread P1).
|
|
15329
|
+
this.#rekeyJobManagerForSessionIdentity(rollbackSessionState.sessionId, rollbackSessionState.sessionFile);
|
|
13183
15330
|
committed = true;
|
|
13184
15331
|
await this.#runToolSessionTransitionCleanups();
|
|
13185
15332
|
this.agent.reset();
|
|
@@ -13690,6 +15837,7 @@ export class AgentSession {
|
|
|
13690
15837
|
sessionMemory.metadataResidentBytes,
|
|
13691
15838
|
tuiChatChildren: retainedMemory.tuiChatChildren ?? 0,
|
|
13692
15839
|
tuiCachedRenderBytes: retainedMemory.tuiCachedRenderBytes ?? 0,
|
|
15840
|
+
transcriptFileBytes: this.sessionManager.getTranscriptFileBytes(),
|
|
13693
15841
|
};
|
|
13694
15842
|
}
|
|
13695
15843
|
|
|
@@ -15393,7 +17541,9 @@ export class AgentSession {
|
|
|
15393
17541
|
classification === "transient" ||
|
|
15394
17542
|
classification === "unknown" ||
|
|
15395
17543
|
classification === "first_event_timeout" ||
|
|
15396
|
-
classification === "empty_response"
|
|
17544
|
+
classification === "empty_response" ||
|
|
17545
|
+
classification === "local_snapshot" ||
|
|
17546
|
+
classification === "local_buffer_overflow"
|
|
15397
17547
|
);
|
|
15398
17548
|
}
|
|
15399
17549
|
const trigger = classifyFallbackTrigger(transportFailure ?? { status: message.errorStatus });
|
|
@@ -15413,7 +17563,13 @@ export class AgentSession {
|
|
|
15413
17563
|
return true;
|
|
15414
17564
|
}
|
|
15415
17565
|
const classification = this.#classifyErrorForRetry(message);
|
|
15416
|
-
return
|
|
17566
|
+
return (
|
|
17567
|
+
classification === "transient" ||
|
|
17568
|
+
classification === "unknown" ||
|
|
17569
|
+
classification === "first_event_timeout" ||
|
|
17570
|
+
classification === "local_snapshot" ||
|
|
17571
|
+
classification === "local_buffer_overflow"
|
|
17572
|
+
);
|
|
15417
17573
|
}
|
|
15418
17574
|
|
|
15419
17575
|
#isTransientErrorMessage(errorMessage: string): boolean {
|
|
@@ -15503,10 +17659,20 @@ export class AgentSession {
|
|
|
15503
17659
|
#classifyErrorForRetry(message: AssistantMessage): RetryErrorClassification {
|
|
15504
17660
|
if (message.stopReason !== "error") return "none";
|
|
15505
17661
|
if (message.errorKind === "provider_safety_stop") return "terminal";
|
|
17662
|
+
if (message.errorKind === "local_snapshot_failure") return "local_snapshot";
|
|
17663
|
+
if (message.errorKind === "local_buffer_overflow") return "local_buffer_overflow";
|
|
15506
17664
|
if (this.#isTypedFirstEventTimeout(message)) return "first_event_timeout";
|
|
15507
17665
|
if (this.#isTypedEmptyResponse(message)) return "empty_response";
|
|
15508
17666
|
if (!message.errorMessage) return "none";
|
|
15509
17667
|
const err = message.errorMessage;
|
|
17668
|
+
// Managed-attempt local failures from restored sessions may lack the
|
|
17669
|
+
// typed error kind; match the stable message prefixes as a fallback.
|
|
17670
|
+
if (err.startsWith("Managed fallback attempt could not produce a serializable event snapshot")) {
|
|
17671
|
+
return "local_snapshot";
|
|
17672
|
+
}
|
|
17673
|
+
if (err.startsWith("Managed fallback attempt exceeded the provisional event buffer limit")) {
|
|
17674
|
+
return "local_buffer_overflow";
|
|
17675
|
+
}
|
|
15510
17676
|
// Provider safety refusals (e.g. Anthropic stop_reason "refusal" /
|
|
15511
17677
|
// "sensitive") are deterministic for the submitted context: replaying
|
|
15512
17678
|
// the identical conversation re-triggers the identical refusal, so an
|
|
@@ -15714,6 +17880,23 @@ export class AgentSession {
|
|
|
15714
17880
|
this.#defaultFallbackChain().resetAttemptBudget();
|
|
15715
17881
|
return { type: "terminal", terminal: { stopReason: outcome.reason } };
|
|
15716
17882
|
}
|
|
17883
|
+
if (outcome.type === "escaped_arguments_discarded") {
|
|
17884
|
+
// An escaped-non-ASCII wire defect is a sampling accident, not provider
|
|
17885
|
+
// evidence: never charge the attempt, advance the chain, or suppress the
|
|
17886
|
+
// selector. The loop already removed the defective turn from history and
|
|
17887
|
+
// bounded its own resample budget, so this decision just re-issues the
|
|
17888
|
+
// same request on the same model. Once the loop declines (budget spent),
|
|
17889
|
+
// it falls through to the terminal per-call rejection, so the retry here
|
|
17890
|
+
// is a continuation of the same logical run rather than a new prompt.
|
|
17891
|
+
this.#defaultFallbackChain().discardStartedAttempt();
|
|
17892
|
+
return {
|
|
17893
|
+
type: "retry",
|
|
17894
|
+
continuation: async ownership => {
|
|
17895
|
+
if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
|
|
17896
|
+
await this.agent.continue(this.#managedFallbackPromptOptions());
|
|
17897
|
+
},
|
|
17898
|
+
};
|
|
17899
|
+
}
|
|
15717
17900
|
if (outcome.type === "context_overflow_discarded") {
|
|
15718
17901
|
// The provider invocation happened, but overflow is context maintenance rather
|
|
15719
17902
|
// than a fallback-policy failure. Keep the logical run owner and do not charge,
|
|
@@ -16000,12 +18183,50 @@ export class AgentSession {
|
|
|
16000
18183
|
this.settings.has("retry.maxRetries") ||
|
|
16001
18184
|
this.settings.has("retry.baseDelayMs") ||
|
|
16002
18185
|
this.settings.has("retry.maxDelayMs");
|
|
16003
|
-
// retry.enabled=false always surfaces immediately, matching the explicit
|
|
16004
|
-
// user opt-out.
|
|
16005
|
-
if (!managedFallback && !retrySettings.enabled) return false;
|
|
16006
18186
|
const classification = this.#classifyErrorForRetry(message);
|
|
18187
|
+
const localSnapshot = classification === "local_snapshot";
|
|
18188
|
+
const localBufferOverflow = classification === "local_buffer_overflow";
|
|
18189
|
+
// A local machinery failure must never stay charged against the provider
|
|
18190
|
+
// fallback budget, no matter which local exit follows (disabled retry,
|
|
18191
|
+
// visible-content surface, bounded retry, exhaustion, or the immediate
|
|
18192
|
+
// buffer-overflow surface). Discard the started attempt's provisional
|
|
18193
|
+
// charge up front; the discard is a no-op when no attempt is currently
|
|
18194
|
+
// charged.
|
|
18195
|
+
if ((localSnapshot || localBufferOverflow) && managedFallback) controller.discardStartedAttempt();
|
|
18196
|
+
// Local snapshot/staging failures are deterministic for the retained
|
|
18197
|
+
// producer shape. Re-streaming the same request only duplicates the same
|
|
18198
|
+
// local defect (and, before #4578, amplified it three times). Surface the
|
|
18199
|
+
// one producer-boundary diagnostic immediately without provider-fallback
|
|
18200
|
+
// attribution or credential mutation.
|
|
18201
|
+
if (localSnapshot || localBufferOverflow) {
|
|
18202
|
+
return managedOutcome
|
|
18203
|
+
? {
|
|
18204
|
+
type: "terminal",
|
|
18205
|
+
terminal: { stopReason: "error", messages: [message] },
|
|
18206
|
+
}
|
|
18207
|
+
: false;
|
|
18208
|
+
}
|
|
18209
|
+
// retry.enabled=false always surfaces immediately, matching the explicit
|
|
18210
|
+
// user opt-out. The managed provider-fallback chain keeps its own
|
|
18211
|
+
// availability policy.
|
|
18212
|
+
if (!retrySettings.enabled && !managedFallback) {
|
|
18213
|
+
return managedOutcome
|
|
18214
|
+
? {
|
|
18215
|
+
type: "terminal",
|
|
18216
|
+
terminal: { stopReason: "error", messages: [message] },
|
|
18217
|
+
}
|
|
18218
|
+
: false;
|
|
18219
|
+
}
|
|
16007
18220
|
const firstEventTimeout = classification === "first_event_timeout";
|
|
16008
18221
|
const emptyResponse = classification === "empty_response";
|
|
18222
|
+
const reportedRetryMaxAttempts = transportFailure?.retryMaxAttempts;
|
|
18223
|
+
if (reportedRetryMaxAttempts !== undefined) {
|
|
18224
|
+
this.#providerRetryMaxAttempts = Math.min(
|
|
18225
|
+
this.#providerRetryMaxAttempts ?? Number.POSITIVE_INFINITY,
|
|
18226
|
+
reportedRetryMaxAttempts,
|
|
18227
|
+
);
|
|
18228
|
+
}
|
|
18229
|
+
const providerRetryMaxAttempts = this.#providerRetryMaxAttempts;
|
|
16009
18230
|
// Content-free message-only watchdog prose (wrapped canonical or bare
|
|
16010
18231
|
// per-provider variants) is admitted like the typed path: it is
|
|
16011
18232
|
// replay-safe, so a bare-default retry may re-issue the request even
|
|
@@ -16037,18 +18258,24 @@ export class AgentSession {
|
|
|
16037
18258
|
}
|
|
16038
18259
|
: false;
|
|
16039
18260
|
}
|
|
16040
|
-
const trigger
|
|
18261
|
+
const trigger:
|
|
18262
|
+
| { class: FallbackTriggerClass; retryAfterMs?: number; authDisposition?: AuthDisposition }
|
|
18263
|
+
| undefined = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
|
|
16041
18264
|
if (!trigger) {
|
|
16042
18265
|
return managedOutcome
|
|
16043
18266
|
? this.#managedFallbackExhaustionDecision(message, message.errorMessage || "Model fallback attempt failed")
|
|
16044
18267
|
: false;
|
|
16045
18268
|
}
|
|
18269
|
+
const attemptsUsed = managedFallback ? controller.attemptsUsed || 1 : this.#retryAttempt + 1;
|
|
18270
|
+
const providerRetryCeilingReached =
|
|
18271
|
+
providerRetryMaxAttempts !== undefined && attemptsUsed >= providerRetryMaxAttempts;
|
|
16046
18272
|
// Credential rotation: a content-free quota/rate-limit failure has no
|
|
16047
18273
|
// observable state to corrupt, so it is replay-safe regardless of
|
|
16048
18274
|
// extension lifecycle participation. Mark the failed credential and
|
|
16049
18275
|
// retry with the next stored credential of the same provider.
|
|
16050
18276
|
let credentialRotated =
|
|
16051
18277
|
!managedFallback &&
|
|
18278
|
+
!providerRetryCeilingReached &&
|
|
16052
18279
|
!assistantMessageHasVisibleOrToolContent(message) &&
|
|
16053
18280
|
(trigger.class === "quota" || trigger.class === "rate_limit") &&
|
|
16054
18281
|
(await this.#markFailedCredential(trigger));
|
|
@@ -16056,7 +18283,9 @@ export class AgentSession {
|
|
|
16056
18283
|
// output, no tool calls, no extension-observable streaming state was produced
|
|
16057
18284
|
// before the failure. This bypasses #hasCleanRetryReplaySafety because the
|
|
16058
18285
|
// content-free check is the replay-safety guarantee for credential rotation.
|
|
16059
|
-
|
|
18286
|
+
// A reached provider ceiling also continues through this gate only so the
|
|
18287
|
+
// bounded exhaustion path can surface its exact diagnostic; it never replays.
|
|
18288
|
+
const canReplayRotatedCredential = credentialRotated || providerRetryCeilingReached;
|
|
16060
18289
|
// Universal automatic replay-safety gate (#3791): once the failed attempt
|
|
16061
18290
|
// carries observable assistant text, thinking, or tool-call content,
|
|
16062
18291
|
// automatic session retry must not re-issue the request. This covers
|
|
@@ -16094,17 +18323,30 @@ export class AgentSession {
|
|
|
16094
18323
|
!managedFallback &&
|
|
16095
18324
|
classification === "transient" &&
|
|
16096
18325
|
!this.#isIdleStreamStallErrorMessage(message.errorMessage ?? "");
|
|
16097
|
-
|
|
18326
|
+
|
|
16098
18327
|
const failedSelector = managedFallback ? controller.currentSelector() : undefined;
|
|
16099
|
-
let outcome
|
|
16100
|
-
|
|
16101
|
-
|
|
16102
|
-
|
|
16103
|
-
: "exhausted";
|
|
18328
|
+
let outcome: "retry" | "advance" | "exhausted";
|
|
18329
|
+
if (managedFallback) {
|
|
18330
|
+
outcome = controller.onAttemptFailure(trigger.class, message.errorMessage || "Unknown error");
|
|
18331
|
+
if (providerRetryCeilingReached && outcome === "retry") {
|
|
18332
|
+
outcome = controller.advance() ? "advance" : "exhausted";
|
|
18333
|
+
}
|
|
18334
|
+
} else {
|
|
18335
|
+
outcome = providerRetryCeilingReached
|
|
18336
|
+
? "exhausted"
|
|
18337
|
+
: legacyUnbounded || attemptsUsed <= retrySettings.maxRetries
|
|
18338
|
+
? "retry"
|
|
18339
|
+
: "exhausted";
|
|
18340
|
+
}
|
|
16104
18341
|
// Credential rotation is unbounded: a fresh credential is a different
|
|
16105
18342
|
// retry dimension from transient-error backoff, so it overrides maxRetries
|
|
16106
18343
|
// exhaustion and forces an immediate same-model retry.
|
|
16107
|
-
if (
|
|
18344
|
+
if (
|
|
18345
|
+
managedFallback &&
|
|
18346
|
+
!providerRetryCeilingReached &&
|
|
18347
|
+
outcome === "advance" &&
|
|
18348
|
+
(trigger.class === "quota" || trigger.class === "rate_limit")
|
|
18349
|
+
) {
|
|
16108
18350
|
credentialRotated = await this.#markFailedCredential(trigger);
|
|
16109
18351
|
}
|
|
16110
18352
|
if (credentialRotated) {
|
|
@@ -16118,6 +18360,9 @@ export class AgentSession {
|
|
|
16118
18360
|
outcome = "retry";
|
|
16119
18361
|
}
|
|
16120
18362
|
}
|
|
18363
|
+
if (outcome === "advance") {
|
|
18364
|
+
this.#providerRetryMaxAttempts = undefined;
|
|
18365
|
+
}
|
|
16121
18366
|
if (outcome === "exhausted") {
|
|
16122
18367
|
if (managedFallback) {
|
|
16123
18368
|
const errorMessage = this.#fallbackExhaustionError(controller);
|
|
@@ -16126,9 +18371,13 @@ export class AgentSession {
|
|
|
16126
18371
|
controller.resetSticky();
|
|
16127
18372
|
return managedOutcome ? this.#managedFallbackExhaustionDecision(message, errorMessage) : false;
|
|
16128
18373
|
}
|
|
16129
|
-
if (classification === "first_event_timeout") {
|
|
18374
|
+
if (classification === "first_event_timeout" || providerRetryMaxAttempts !== undefined) {
|
|
16130
18375
|
const elapsedMs = Date.now() - (this.#firstEventTimeoutRetryStartedAt ?? Date.now());
|
|
16131
|
-
|
|
18376
|
+
const prefix =
|
|
18377
|
+
classification === "first_event_timeout"
|
|
18378
|
+
? "First-event stream timeout exhausted"
|
|
18379
|
+
: "First-event/grace retry ceiling exhausted";
|
|
18380
|
+
message.errorMessage = `${prefix} after ${attemptsUsed} attempts; waited ${elapsedMs}ms total: ${message.errorMessage ?? "Unknown error"}`;
|
|
16132
18381
|
}
|
|
16133
18382
|
return false;
|
|
16134
18383
|
}
|
|
@@ -16151,7 +18400,9 @@ export class AgentSession {
|
|
|
16151
18400
|
}
|
|
16152
18401
|
|
|
16153
18402
|
const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
|
|
16154
|
-
if (managedFallback && !credentialRotated
|
|
18403
|
+
if (managedFallback && !credentialRotated && !providerRetryCeilingReached) {
|
|
18404
|
+
await this.#markFailedCredential(trigger);
|
|
18405
|
+
}
|
|
16155
18406
|
let advanced = outcome !== "advance";
|
|
16156
18407
|
let resolutionError: unknown;
|
|
16157
18408
|
if (outcome === "advance") {
|
|
@@ -16194,11 +18445,17 @@ export class AgentSession {
|
|
|
16194
18445
|
await this.#emitSessionEvent({
|
|
16195
18446
|
type: "auto_retry_start",
|
|
16196
18447
|
attempt: this.#retryAttempt,
|
|
16197
|
-
maxAttempts:
|
|
16198
|
-
|
|
16199
|
-
|
|
16200
|
-
|
|
16201
|
-
|
|
18448
|
+
maxAttempts:
|
|
18449
|
+
managedFallback && !localSnapshot
|
|
18450
|
+
? firstEventTimeout || this.#providerRetryMaxAttempts !== undefined
|
|
18451
|
+
? Math.min(controller.maxAttempts, this.#providerRetryMaxAttempts ?? Number.POSITIVE_INFINITY)
|
|
18452
|
+
: controller.maxAttempts
|
|
18453
|
+
: firstEventTimeout || this.#providerRetryMaxAttempts !== undefined
|
|
18454
|
+
? Math.min(
|
|
18455
|
+
retrySettings.maxRetries + 1,
|
|
18456
|
+
this.#providerRetryMaxAttempts ?? Number.POSITIVE_INFINITY,
|
|
18457
|
+
)
|
|
18458
|
+
: retrySettings.maxRetries,
|
|
16202
18459
|
delayMs,
|
|
16203
18460
|
errorMessage,
|
|
16204
18461
|
unbounded: managedFallback ? false : legacyUnbounded,
|
|
@@ -16401,6 +18658,7 @@ export class AgentSession {
|
|
|
16401
18658
|
}
|
|
16402
18659
|
if (!preflightAccepted) {
|
|
16403
18660
|
await seam?.onPreflightAccepted?.();
|
|
18661
|
+
if (seam?.signal?.aborted) throw promptPreflightCancelledError();
|
|
16404
18662
|
preflightAccepted = true;
|
|
16405
18663
|
}
|
|
16406
18664
|
await this.agent.prompt(messages, options);
|
|
@@ -16506,6 +18764,7 @@ export class AgentSession {
|
|
|
16506
18764
|
if (lastMsg.role !== "assistant") {
|
|
16507
18765
|
if (!this.#isInterruptedRetryTail(lastMsg)) return false;
|
|
16508
18766
|
this.#retryAttempt = 0;
|
|
18767
|
+
this.#providerRetryMaxAttempts = undefined;
|
|
16509
18768
|
this.#scheduleAgentContinue({ delayMs: 1 });
|
|
16510
18769
|
return true;
|
|
16511
18770
|
}
|
|
@@ -16524,6 +18783,7 @@ export class AgentSession {
|
|
|
16524
18783
|
|
|
16525
18784
|
// Reset retry budget for a fresh attempt
|
|
16526
18785
|
this.#retryAttempt = 0;
|
|
18786
|
+
this.#providerRetryMaxAttempts = undefined;
|
|
16527
18787
|
|
|
16528
18788
|
// Re-attempt the turn
|
|
16529
18789
|
this.#scheduleAgentContinue({ delayMs: 1 });
|
|
@@ -17618,7 +19878,7 @@ export class AgentSession {
|
|
|
17618
19878
|
}
|
|
17619
19879
|
options?.onTransitionMutationStarted?.();
|
|
17620
19880
|
|
|
17621
|
-
const asyncManager = AsyncJobManager.instance();
|
|
19881
|
+
const asyncManager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
17622
19882
|
const ownerId = this.#agentId;
|
|
17623
19883
|
const lease =
|
|
17624
19884
|
switchingToDifferentSession && asyncManager && ownerId
|
|
@@ -17680,6 +19940,13 @@ export class AgentSession {
|
|
|
17680
19940
|
await this.sessionManager.setSessionFile(sessionPath, {
|
|
17681
19941
|
deferEphemeralArtifactRetirement: switchingToDifferentSession,
|
|
17682
19942
|
});
|
|
19943
|
+
// setSessionFile rotated the endpoint identity to the successor;
|
|
19944
|
+
// re-register the manager under it so post-transition lineage
|
|
19945
|
+
// bindings resolve and owned aborts classify in the successor
|
|
19946
|
+
// session (review thread P1).
|
|
19947
|
+
this.#rekeyJobManagerForSessionIdentity(previousSessionState.sessionId, previousSessionState.sessionFile, {
|
|
19948
|
+
retirePredecessorRegistrations: false,
|
|
19949
|
+
});
|
|
17683
19950
|
if (switchingToDifferentSession) this.sessionManager.stageAdoptedArtifactManagerForTransition();
|
|
17684
19951
|
// The successor identity is already rotated in the manager but not yet
|
|
17685
19952
|
// published; gate its local:// root before publication so the agent,
|
|
@@ -17826,6 +20093,10 @@ export class AgentSession {
|
|
|
17826
20093
|
}
|
|
17827
20094
|
|
|
17828
20095
|
if (switchingToDifferentSession) {
|
|
20096
|
+
const predecessorEndpointId = this.#asyncJobEndpointId(
|
|
20097
|
+
previousSessionState.sessionId,
|
|
20098
|
+
previousSessionState.sessionFile,
|
|
20099
|
+
);
|
|
17829
20100
|
let ownerShutdownSettled = true;
|
|
17830
20101
|
if (ownerShutdownManager && ownerShutdownLease && ownerId) {
|
|
17831
20102
|
try {
|
|
@@ -17840,7 +20111,12 @@ export class AgentSession {
|
|
|
17840
20111
|
} catch (error) {
|
|
17841
20112
|
ownerShutdownSettled = false;
|
|
17842
20113
|
ownerShutdownFinalizationDeferred = true;
|
|
17843
|
-
this.#scheduleDeferredOwnerShutdownFinalization(
|
|
20114
|
+
this.#scheduleDeferredOwnerShutdownFinalization(
|
|
20115
|
+
ownerShutdownManager,
|
|
20116
|
+
ownerShutdownLease,
|
|
20117
|
+
ownerId,
|
|
20118
|
+
predecessorEndpointId,
|
|
20119
|
+
);
|
|
17844
20120
|
this.#suppressOwnAsyncJobDeliveries();
|
|
17845
20121
|
this.emitNotice(
|
|
17846
20122
|
"error",
|
|
@@ -17853,6 +20129,10 @@ export class AgentSession {
|
|
|
17853
20129
|
// Different files may intentionally carry the same copied session id; pathname transition is the commit signal.
|
|
17854
20130
|
ownerShutdownTransitionCommitted = true;
|
|
17855
20131
|
if (ownerShutdownSettled) {
|
|
20132
|
+
// Every predecessor job/delivery has settled. Only now can its
|
|
20133
|
+
// tuple evidence be retired; doing this immediately after rekey
|
|
20134
|
+
// made rollback restore live jobs without their owned tuples.
|
|
20135
|
+
retireOwnedRegistrationsForEndpoint(predecessorEndpointId);
|
|
17856
20136
|
this.sessionManager.retireEphemeralArtifactsAfterTransition();
|
|
17857
20137
|
await this.#runToolSessionTransitionCleanups();
|
|
17858
20138
|
}
|
|
@@ -17878,6 +20158,35 @@ export class AgentSession {
|
|
|
17878
20158
|
return true;
|
|
17879
20159
|
} catch (error) {
|
|
17880
20160
|
if (transitionCleanupCommitted) throw error;
|
|
20161
|
+
// The switch never committed: rotate the manager's endpoint
|
|
20162
|
+
// registration back to the predecessor before restoring it
|
|
20163
|
+
// (review thread P1 — the map key must track the session id).
|
|
20164
|
+
const successorEndpointId = this.#asyncJobEndpointId(
|
|
20165
|
+
this.sessionManager.getSessionId(),
|
|
20166
|
+
this.sessionManager.getSessionFile(),
|
|
20167
|
+
);
|
|
20168
|
+
const predecessorEndpointId = this.#asyncJobEndpointId(
|
|
20169
|
+
previousSessionState.sessionId,
|
|
20170
|
+
previousSessionState.sessionFile,
|
|
20171
|
+
);
|
|
20172
|
+
const rekeyed = AsyncJobManager.rekeyForEndpoint(
|
|
20173
|
+
successorEndpointId,
|
|
20174
|
+
predecessorEndpointId,
|
|
20175
|
+
AsyncJobManager.forEndpoint(successorEndpointId),
|
|
20176
|
+
);
|
|
20177
|
+
if (!rekeyed) {
|
|
20178
|
+
// Another top-level session claimed the freed predecessor endpoint
|
|
20179
|
+
// while the switch's cleanup was in flight: restoring the old
|
|
20180
|
+
// identity would leave this manager registered under the successor
|
|
20181
|
+
// while tool lookups for the restored identity resolve the foreign
|
|
20182
|
+
// manager, so same-ID jobs could be queried or cancelled across
|
|
20183
|
+
// sessions and owned aborts lose their causal set (review thread
|
|
20184
|
+
// P1). Fail the rollback instead of restoring into misattributed
|
|
20185
|
+
// ownership; the caller surfaces the switch failure.
|
|
20186
|
+
throw new Error(
|
|
20187
|
+
`Session switch rollback aborted: predecessor endpoint "${previousSessionState.sessionId}" is no longer owned by this session.`,
|
|
20188
|
+
);
|
|
20189
|
+
}
|
|
17881
20190
|
await this.sessionManager.restoreRollbackState(previousSessionState);
|
|
17882
20191
|
this.#defaultFallbackController = undefined;
|
|
17883
20192
|
this.#syncAgentSessionId(previousSessionState.sessionId);
|
|
@@ -17969,6 +20278,7 @@ export class AgentSession {
|
|
|
17969
20278
|
try {
|
|
17970
20279
|
const previousSessionFile = this.sessionFile;
|
|
17971
20280
|
const previousWorkflowGateSessionId = this.sessionId;
|
|
20281
|
+
const previousSessionIdentity = this.sessionManager.getSessionId();
|
|
17972
20282
|
const selectedEntry = this.sessionManager.getEntryForFidelity(entryId);
|
|
17973
20283
|
|
|
17974
20284
|
if (selectedEntry?.type !== "message" || selectedEntry.message.role !== "user") {
|
|
@@ -18000,7 +20310,11 @@ export class AgentSession {
|
|
|
18000
20310
|
try {
|
|
18001
20311
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
18002
20312
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
20313
|
+
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
18003
20314
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
20315
|
+
// Branch commits a successor endpoint identity; re-register the
|
|
20316
|
+
// manager under it (review thread P1).
|
|
20317
|
+
this.#rekeyJobManagerForSessionIdentity(previousSessionIdentity, previousSessionFile);
|
|
18004
20318
|
await this.#runToolSessionTransitionCleanups();
|
|
18005
20319
|
} catch (error) {
|
|
18006
20320
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|