@gajae-code/coding-agent 0.13.3 → 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 +184 -2
- 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 +9 -1
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +70 -6
- 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 +1 -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/gajae-pet-widget.d.ts +8 -30
- 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 +11 -25
- 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/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 +40 -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 +9 -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 +20 -1
- package/src/config/model-profiles.ts +29 -19
- package/src/config/model-registry.ts +132 -21
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +76 -6
- 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/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 +40 -32
- package/src/main.ts +32 -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/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/gajae-pet-widget.ts +539 -213
- package/src/modes/components/irc-sidebar.ts +11 -0
- package/src/modes/components/iterm-pet-transport.ts +585 -0
- package/src/modes/components/oauth-selector.ts +277 -86
- package/src/modes/components/pet-capability.ts +39 -40
- package/src/modes/components/settings-selector.ts +9 -0
- package/src/modes/components/status-line/gh.ts +45 -1
- package/src/modes/components/status-line/segments.ts +16 -1
- 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 +177 -72
- 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/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 +2857 -582
- 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 +173 -12
- package/src/session/messages.ts +1 -1
- package/src/session/secure-token-file.ts +80 -0
- package/src/session/session-manager.ts +730 -58
- 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 +98 -16
- 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 +27 -16
- 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/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,17 +1040,12 @@ 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 =
|
|
956
1048
|
/^(?:[A-Za-z][A-Za-z0-9-]*(?: [A-Za-z][A-Za-z0-9-]*){0,3} )stream (?:timed out while waiting for the first event|stalled while waiting for the next event)$/;
|
|
957
|
-
/**
|
|
958
|
-
* Consecutive same-model retries allowed for `escaped_arguments_discarded`
|
|
959
|
-
* managed outcomes before the run terminates. Mirrors the agent loop's
|
|
960
|
-
* escaped-non-ASCII resample budget on dev; enforced session-side here because
|
|
961
|
-
* a managed retry re-enters the loop as a fresh run.
|
|
962
|
-
*/
|
|
963
|
-
const MAX_ESCAPED_ARGUMENT_RETRIES = 2;
|
|
964
1049
|
const PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = "Provider stream timed out while waiting for the first event";
|
|
965
1050
|
const WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = `Error: ${PROVIDER_FIRST_EVENT_TIMEOUT_ERROR}`;
|
|
966
1051
|
const PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR = "Provider stream timed out while waiting for first event";
|
|
@@ -1047,7 +1132,12 @@ function assistantMessageHasVisibleOrToolContent(message: AssistantMessage): boo
|
|
|
1047
1132
|
|
|
1048
1133
|
function isLocalModelEndpoint(model: Model | undefined): boolean {
|
|
1049
1134
|
if (!model) return false;
|
|
1050
|
-
if (
|
|
1135
|
+
if (
|
|
1136
|
+
model.provider === "ollama" ||
|
|
1137
|
+
model.provider === "lm-studio" ||
|
|
1138
|
+
model.provider === "llama.cpp" ||
|
|
1139
|
+
model.provider === "omlx"
|
|
1140
|
+
) {
|
|
1051
1141
|
return true;
|
|
1052
1142
|
}
|
|
1053
1143
|
try {
|
|
@@ -1511,6 +1601,7 @@ function extractPermissionLocations(
|
|
|
1511
1601
|
* stable edit id while the display arrays preserve delivery order. */
|
|
1512
1602
|
type QueuedDisplayEntry = { text: string; tag?: string; sequence: number };
|
|
1513
1603
|
type IrcRosterClaim = { token: symbol; signature: string; epoch: number; message: CustomMessage };
|
|
1604
|
+
type QueuedFollowUpOwner = { cancel(): boolean };
|
|
1514
1605
|
export type QueuedMessageEditMode = "steer" | "followUp";
|
|
1515
1606
|
|
|
1516
1607
|
export interface QueuedMessageEditEntry {
|
|
@@ -1801,12 +1892,19 @@ type SessionAdmissionEntry = {
|
|
|
1801
1892
|
ready: PromiseWithResolvers<void>;
|
|
1802
1893
|
settled: PromiseWithResolvers<void>;
|
|
1803
1894
|
released: boolean;
|
|
1895
|
+
selectionFenceGeneration: number;
|
|
1896
|
+
continuationCapability?: symbol;
|
|
1804
1897
|
};
|
|
1805
1898
|
|
|
1806
1899
|
type SessionAdmissionLease = {
|
|
1807
1900
|
release(): void;
|
|
1808
1901
|
};
|
|
1809
1902
|
|
|
1903
|
+
type ScheduledContinuationAdmission = {
|
|
1904
|
+
entry: SessionAdmissionEntry;
|
|
1905
|
+
capability: symbol;
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1810
1908
|
export interface DefaultFallbackRuntimeState {
|
|
1811
1909
|
chain: ConfiguredFallbackChain;
|
|
1812
1910
|
controller: FallbackChainRuntimeState;
|
|
@@ -1838,10 +1936,37 @@ function deobfuscateSessionContext(context: SessionContext, obfuscator: SecretOb
|
|
|
1838
1936
|
return { ...context, messages };
|
|
1839
1937
|
}
|
|
1840
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
|
+
|
|
1841
1955
|
export class AgentSession {
|
|
1956
|
+
#provisionalStreamingToolCallIds = new Set<string>();
|
|
1842
1957
|
readonly agent: Agent;
|
|
1843
1958
|
sessionManager: SessionManager;
|
|
1844
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
|
+
}
|
|
1845
1970
|
readonly memoryBackend: LazyService<MemoryBackend>;
|
|
1846
1971
|
readonly notificationSessionController: NotificationSessionController | undefined;
|
|
1847
1972
|
readonly taskDepth: number;
|
|
@@ -1866,8 +1991,20 @@ export class AgentSession {
|
|
|
1866
1991
|
#preProfileModel: Model | undefined;
|
|
1867
1992
|
#sessionAdmissionQueue: SessionAdmissionEntry[] = [];
|
|
1868
1993
|
#activeSessionAdmission: SessionAdmissionEntry | undefined;
|
|
1994
|
+
#sessionAdmissionClosing = false;
|
|
1869
1995
|
#sessionAdmissionClosed = false;
|
|
1870
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;
|
|
1871
2008
|
#defaultModelSelectionMutationRevision = 0;
|
|
1872
2009
|
#thinkingLevelMutationRevision = 0;
|
|
1873
2010
|
#thinkingVisibilityMutationRevision = 0;
|
|
@@ -1937,9 +2074,62 @@ export class AgentSession {
|
|
|
1937
2074
|
/** Tracks pending follow-up messages for UI display. Removed when delivered.
|
|
1938
2075
|
* See `#steeringMessages` for entry shape. */
|
|
1939
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;
|
|
1940
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
|
+
}
|
|
1941
2131
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
1942
|
-
#pendingNextTurnMessages: CustomMessage
|
|
2132
|
+
#pendingNextTurnMessages: Array<{ message: CustomMessage; origin: "turn" | "external" }> = [];
|
|
1943
2133
|
#scheduledHiddenNextTurnGeneration: number | undefined = undefined;
|
|
1944
2134
|
#planModeState: PlanModeState | undefined;
|
|
1945
2135
|
#goalModeState: GoalModeState | undefined;
|
|
@@ -1996,6 +2186,7 @@ export class AgentSession {
|
|
|
1996
2186
|
this.#retryReplayEpoch++;
|
|
1997
2187
|
this.#retryReplayUnsafeEpoch = undefined;
|
|
1998
2188
|
this.#firstEventTimeoutRetryStartedAt = Date.now();
|
|
2189
|
+
this.#providerRetryMaxAttempts = undefined;
|
|
1999
2190
|
}
|
|
2000
2191
|
|
|
2001
2192
|
#markRetryReplayUnsafe(): void {
|
|
@@ -2033,11 +2224,11 @@ export class AgentSession {
|
|
|
2033
2224
|
#retryAbortController: AbortController | undefined = undefined;
|
|
2034
2225
|
#retryNowRequested = false;
|
|
2035
2226
|
#firstEventTimeoutRetryStartedAt: number | undefined;
|
|
2227
|
+
#providerRetryMaxAttempts: number | undefined;
|
|
2036
2228
|
#retryAttempt = 0;
|
|
2037
2229
|
#retryPromise: Promise<void> | undefined = undefined;
|
|
2038
2230
|
#retryResolve: (() => void) | undefined = undefined;
|
|
2039
2231
|
#defaultFallbackController: FallbackChainController | undefined;
|
|
2040
|
-
#consecutiveEscapedArgumentRetries = 0;
|
|
2041
2232
|
#overflowMaintenanceAttempts = 0;
|
|
2042
2233
|
#defaultFallbackExhaustedLastTurn = false;
|
|
2043
2234
|
#fallbackInvocationId = 0;
|
|
@@ -2077,6 +2268,7 @@ export class AgentSession {
|
|
|
2077
2268
|
* this undefined and **MUST NOT** dispose the global instance on teardown.
|
|
2078
2269
|
*/
|
|
2079
2270
|
readonly #ownedAsyncJobManager: AsyncJobManager | undefined;
|
|
2271
|
+
readonly #disposeAsyncJobManager: boolean;
|
|
2080
2272
|
readonly #ownedMcpManager: MCPManager | undefined;
|
|
2081
2273
|
#startupTurnBarrier: Promise<void> | undefined;
|
|
2082
2274
|
#pendingPythonMessages: Array<{
|
|
@@ -2099,7 +2291,9 @@ export class AgentSession {
|
|
|
2099
2291
|
#ircRosterClaim: IrcRosterClaim | null = null;
|
|
2100
2292
|
#providerSessionId: string | undefined;
|
|
2101
2293
|
#credentialSessionId: string | undefined;
|
|
2294
|
+
#credentialStoreIdentity: string | undefined;
|
|
2102
2295
|
#providerCacheSessionId: string | undefined;
|
|
2296
|
+
readonly #asyncJobProviderSessionId: string | undefined;
|
|
2103
2297
|
#isDisposed = false;
|
|
2104
2298
|
#disposePromise: Promise<void> | undefined;
|
|
2105
2299
|
readonly #toolSessionCleanups = new Set<() => Promise<void> | void>();
|
|
@@ -2108,6 +2302,9 @@ export class AgentSession {
|
|
|
2108
2302
|
#newSessionTransition: Promise<boolean> | undefined;
|
|
2109
2303
|
// Extension system
|
|
2110
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>();
|
|
2111
2308
|
#attemptAuthority!: AttemptScopeAuthority;
|
|
2112
2309
|
#attemptRecordStore!: AttemptRecordStore;
|
|
2113
2310
|
#activeLogicalRunId: AttemptRunHandle["logicalRunId"] | undefined;
|
|
@@ -2145,6 +2342,17 @@ export class AgentSession {
|
|
|
2145
2342
|
|
|
2146
2343
|
// Tool registry and prompt builder for extensions
|
|
2147
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>;
|
|
2148
2356
|
#workflowGateToolSession: ToolSession | undefined;
|
|
2149
2357
|
#transformContext: (
|
|
2150
2358
|
messages: AgentMessage[],
|
|
@@ -2242,6 +2450,7 @@ export class AgentSession {
|
|
|
2242
2450
|
/** Test-only abort outcome override for cancel-and-submit rollback coverage. */
|
|
2243
2451
|
#cancelAndSubmitAbortOutcomeProviderForTests: (() => Promise<AbortOutcome>) | undefined = undefined;
|
|
2244
2452
|
#postPromptTasks = new Set<Promise<void>>();
|
|
2453
|
+
#postPromptTaskSelectionFenceGenerations = new Map<Promise<void>, number>();
|
|
2245
2454
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
2246
2455
|
#postPromptTasksResolve: (() => void) | undefined = undefined;
|
|
2247
2456
|
#postPromptTasksAbortController = new AbortController();
|
|
@@ -2305,8 +2514,18 @@ export class AgentSession {
|
|
|
2305
2514
|
|
|
2306
2515
|
#pendingRewindReport: string | undefined = undefined;
|
|
2307
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();
|
|
2308
2526
|
#promptGeneration = 0;
|
|
2309
2527
|
#promptPreflightAbortController = new AbortController();
|
|
2528
|
+
#promptPreflightCancellationGeneration = 0;
|
|
2310
2529
|
|
|
2311
2530
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
2312
2531
|
#temporaryProviderSessionScopes: TemporaryProviderSessionScopeRecord[] = [];
|
|
@@ -2372,6 +2591,12 @@ export class AgentSession {
|
|
|
2372
2591
|
});
|
|
2373
2592
|
}
|
|
2374
2593
|
|
|
2594
|
+
#assertSessionAdmissionOpen(): void {
|
|
2595
|
+
if (this.#sessionAdmissionClosing || this.#sessionAdmissionClosed || this.#isDisposed) {
|
|
2596
|
+
throw this.#sessionAdmissionBusyError();
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2375
2600
|
/**
|
|
2376
2601
|
* Reject a turn start while a handoff transition owns the session. Handoff never
|
|
2377
2602
|
* routes its own generation/injection through these turn-start chokepoints, and
|
|
@@ -2420,6 +2645,18 @@ export class AgentSession {
|
|
|
2420
2645
|
next.ready.resolve();
|
|
2421
2646
|
}
|
|
2422
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
|
+
|
|
2423
2660
|
async #awaitStartupTurnBarrier(): Promise<void> {
|
|
2424
2661
|
const barrier = this.#startupTurnBarrier;
|
|
2425
2662
|
if (!barrier) return;
|
|
@@ -2435,11 +2672,43 @@ export class AgentSession {
|
|
|
2435
2672
|
async #withSessionAdmission<T>(
|
|
2436
2673
|
kind: SessionAdmissionKind,
|
|
2437
2674
|
body: (lease: SessionAdmissionLease) => Promise<T>,
|
|
2675
|
+
signal?: AbortSignal,
|
|
2676
|
+
continuationAdmission?: ScheduledContinuationAdmission,
|
|
2677
|
+
options?: {
|
|
2678
|
+
allowDuringClosing?: boolean;
|
|
2679
|
+
bypassSelectionFenceGeneration?: number;
|
|
2680
|
+
allowPromptContinuationReentry?: boolean;
|
|
2681
|
+
},
|
|
2438
2682
|
): Promise<T> {
|
|
2439
|
-
if (kind === "prompt") await this.#awaitStartupTurnBarrier();
|
|
2440
2683
|
const owner = this.#sessionAdmissionContext.getStore();
|
|
2441
|
-
if (owner && !owner.released)
|
|
2442
|
-
|
|
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();
|
|
2443
2712
|
// Reject new external turns for the whole handoff transition. The handoff
|
|
2444
2713
|
// itself never acquires prompt admission (it generates via generateHandoff and
|
|
2445
2714
|
// injects via appendCustomMessageEntry), so this fences external entrants —
|
|
@@ -2456,11 +2725,30 @@ export class AgentSession {
|
|
|
2456
2725
|
ready: Promise.withResolvers<void>(),
|
|
2457
2726
|
settled: Promise.withResolvers<void>(),
|
|
2458
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();
|
|
2459
2739
|
};
|
|
2460
2740
|
this.#sessionAdmissionQueue.push(entry);
|
|
2461
2741
|
this.#activateNextSessionAdmission();
|
|
2462
|
-
|
|
2463
|
-
|
|
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
|
+
) {
|
|
2464
2752
|
entry.released = true;
|
|
2465
2753
|
entry.settled.resolve();
|
|
2466
2754
|
if (this.#activeSessionAdmission === entry) this.#activeSessionAdmission = undefined;
|
|
@@ -2480,11 +2768,7 @@ export class AgentSession {
|
|
|
2480
2768
|
}
|
|
2481
2769
|
|
|
2482
2770
|
const release = () => {
|
|
2483
|
-
|
|
2484
|
-
entry.released = true;
|
|
2485
|
-
entry.settled.resolve();
|
|
2486
|
-
if (this.#activeSessionAdmission === entry) this.#activeSessionAdmission = undefined;
|
|
2487
|
-
this.#activateNextSessionAdmission();
|
|
2771
|
+
releaseEntry();
|
|
2488
2772
|
};
|
|
2489
2773
|
try {
|
|
2490
2774
|
return await this.#sessionAdmissionContext.run(entry, () => body({ release }));
|
|
@@ -2494,19 +2778,22 @@ export class AgentSession {
|
|
|
2494
2778
|
}
|
|
2495
2779
|
|
|
2496
2780
|
async #closeSessionAdmission(): Promise<void> {
|
|
2497
|
-
this.#
|
|
2498
|
-
const queued = this.#sessionAdmissionQueue.splice(0);
|
|
2499
|
-
for (const entry of queued) {
|
|
2500
|
-
entry.released = true;
|
|
2501
|
-
entry.ready.reject(this.#sessionAdmissionBusyError());
|
|
2502
|
-
entry.settled.resolve();
|
|
2503
|
-
}
|
|
2781
|
+
this.#sessionAdmissionClosing = true;
|
|
2504
2782
|
const active = this.#activeSessionAdmission;
|
|
2505
2783
|
if (active?.kind === "prompt") {
|
|
2506
2784
|
this.#promptGeneration++;
|
|
2785
|
+
this.#promptPreflightCancellationGeneration++;
|
|
2507
2786
|
this.#promptPreflightAbortController.abort();
|
|
2508
2787
|
}
|
|
2509
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
|
+
}
|
|
2510
2797
|
}
|
|
2511
2798
|
|
|
2512
2799
|
#beginInFlight(): void {
|
|
@@ -2517,6 +2804,50 @@ export class AgentSession {
|
|
|
2517
2804
|
// in #refreshTeamWorkerHeartbeat(), including internally dispatched turns.
|
|
2518
2805
|
}
|
|
2519
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
|
+
}
|
|
2520
2851
|
|
|
2521
2852
|
#isPromptPreflightCancelled(generation: number, signal: AbortSignal): boolean {
|
|
2522
2853
|
return signal.aborted || this.#promptGeneration !== generation;
|
|
@@ -2550,6 +2881,17 @@ export class AgentSession {
|
|
|
2550
2881
|
return hold;
|
|
2551
2882
|
}
|
|
2552
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
|
+
|
|
2553
2895
|
#claimDeferredAgentEndForContinuation(hold: symbol | undefined): AgentSessionEvent | undefined {
|
|
2554
2896
|
if (!hold) return undefined;
|
|
2555
2897
|
const pending = this.#pendingAgentEndContinuationHolds.get(hold);
|
|
@@ -2607,33 +2949,159 @@ export class AgentSession {
|
|
|
2607
2949
|
}
|
|
2608
2950
|
lease.closeDiscovery();
|
|
2609
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
|
+
}
|
|
2610
3025
|
|
|
2611
|
-
|
|
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
|
+
}
|
|
3044
|
+
|
|
3045
|
+
#isSessionSettlementPending(ignoreSelectionFenceGeneration?: number): boolean {
|
|
2612
3046
|
return (
|
|
2613
3047
|
this.#promptInFlightCount > 0 ||
|
|
2614
3048
|
this.#agentEventHandlersInFlight > 0 ||
|
|
2615
3049
|
this.#agentEndPublicationInFlight > 0 ||
|
|
2616
3050
|
this.#pendingAgentEndContinuationHolds.size > 0 ||
|
|
3051
|
+
this.#pendingSelectionFenceDeferredContinuations(ignoreSelectionFenceGeneration) > 0 ||
|
|
2617
3052
|
this.#pendingAgentEndEmit !== undefined
|
|
2618
3053
|
);
|
|
2619
3054
|
}
|
|
2620
3055
|
|
|
2621
3056
|
#resolveSessionSettlement(): void {
|
|
2622
|
-
if (this.#isSessionSettlementPending() || !this.#sessionSettlementResolve)
|
|
3057
|
+
if (this.#isSessionSettlementPending() || !this.#sessionSettlementResolve) {
|
|
3058
|
+
this.#wakeScopedSettlementWaiters();
|
|
3059
|
+
return;
|
|
3060
|
+
}
|
|
2623
3061
|
const resolve = this.#sessionSettlementResolve;
|
|
2624
3062
|
this.#sessionSettlementResolve = undefined;
|
|
2625
3063
|
this.#sessionSettlementPromise = undefined;
|
|
2626
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();
|
|
2627
3070
|
}
|
|
2628
3071
|
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
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);
|
|
2635
3104
|
}
|
|
2636
|
-
await this.#sessionSettlementPromise;
|
|
2637
3105
|
}
|
|
2638
3106
|
}
|
|
2639
3107
|
|
|
@@ -2670,7 +3138,12 @@ export class AgentSession {
|
|
|
2670
3138
|
// A post-prompt continuation runs before its predecessor prompt returns. Its
|
|
2671
3139
|
// publication must settle, but session-wide settlement can still be owned by
|
|
2672
3140
|
// that predecessor; waiting here would make each prompt wait on the other.
|
|
2673
|
-
|
|
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
|
+
}
|
|
2674
3147
|
if (flushError) throw flushError;
|
|
2675
3148
|
}
|
|
2676
3149
|
|
|
@@ -2746,7 +3219,14 @@ export class AgentSession {
|
|
|
2746
3219
|
// Persist before notifying synchronous subscribers: a subscriber may start a
|
|
2747
3220
|
// successor prompt from agent_end, whose running state must serialize after
|
|
2748
3221
|
// this terminal boundary rather than be overwritten by it.
|
|
2749
|
-
|
|
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);
|
|
2750
3230
|
this.#emit(pending);
|
|
2751
3231
|
extensionDelivery = this.#queueExtensionEvent(
|
|
2752
3232
|
pending,
|
|
@@ -2768,9 +3248,31 @@ export class AgentSession {
|
|
|
2768
3248
|
|
|
2769
3249
|
constructor(config: AgentSessionConfig) {
|
|
2770
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
|
+
});
|
|
2771
3272
|
this.agent.bindRunCancellationDomainBridge(this.#runCancellationDomains, this.#agentSessionClaimKey);
|
|
2772
3273
|
this.sessionManager = config.sessionManager;
|
|
2773
3274
|
this.settings = config.settings;
|
|
3275
|
+
this.#requestedAgentDir = config.agentDir ? path.resolve(config.agentDir) : undefined;
|
|
2774
3276
|
this.sessionManager.setSessionMemoryMode(this.settings.get("sessionMemory.mode"));
|
|
2775
3277
|
this.#unregisterSessionMemorySettings = this.settings.onChanged(settingPath => {
|
|
2776
3278
|
if (settingPath === "sessionMemory.mode") {
|
|
@@ -2819,6 +3321,7 @@ export class AgentSession {
|
|
|
2819
3321
|
this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
2820
3322
|
this.#recoveryHydrationContext = config.recoveryHydrationContext;
|
|
2821
3323
|
this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
|
|
3324
|
+
this.#disposeAsyncJobManager = config.disposeAsyncJobManager ?? true;
|
|
2822
3325
|
this.#retainedMemorySampler = config.retainedMemorySampler;
|
|
2823
3326
|
this.#ownedMcpManager = config.ownedMcpManager;
|
|
2824
3327
|
this.#startupTurnBarrier = config.startupTurnBarrier;
|
|
@@ -2842,6 +3345,7 @@ export class AgentSession {
|
|
|
2842
3345
|
this.#providerSessionState = config.providerSessionState;
|
|
2843
3346
|
}
|
|
2844
3347
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
3348
|
+
this.#builtinToolIdentities = new WeakSet(config.builtinToolIdentities ?? []);
|
|
2845
3349
|
this.#workflowGateToolSession = config.workflowGateToolSession;
|
|
2846
3350
|
this.#requestedToolNames = config.requestedToolNames;
|
|
2847
3351
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
@@ -2878,16 +3382,80 @@ export class AgentSession {
|
|
|
2878
3382
|
this.#bindWorkflowGateEmitter();
|
|
2879
3383
|
this.yieldQueue = new YieldQueue({
|
|
2880
3384
|
isStreaming: () => this.isStreaming || this.#handoffTransitionActive,
|
|
2881
|
-
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
|
+
},
|
|
2882
3415
|
injectIdle: async messages => {
|
|
2883
|
-
|
|
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];
|
|
2884
3428
|
if (!first) return;
|
|
2885
3429
|
await this.#awaitStartupTurnBarrier();
|
|
2886
3430
|
if (this.#isDisposed) return;
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
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);
|
|
2891
3459
|
}
|
|
2892
3460
|
},
|
|
2893
3461
|
scheduleIdleFlush: run => {
|
|
@@ -2980,9 +3548,83 @@ export class AgentSession {
|
|
|
2980
3548
|
this.#agentRegistry = config.agentRegistry;
|
|
2981
3549
|
this.#providerSessionId = config.providerSessionId;
|
|
2982
3550
|
this.#credentialSessionId = config.credentialSessionId;
|
|
3551
|
+
this.#credentialStoreIdentity = config.credentialStoreIdentity;
|
|
2983
3552
|
this.#providerCacheSessionId = config.providerCacheSessionId;
|
|
3553
|
+
this.#asyncJobProviderSessionId = config.asyncJobProviderSessionId;
|
|
2984
3554
|
// Per-tool TTSR reminders are folded into the matched tool's result via this hook.
|
|
2985
|
-
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
|
+
};
|
|
2986
3628
|
this.agent.providerSessionState = this.#providerSessionState;
|
|
2987
3629
|
this.#syncAgentSessionId();
|
|
2988
3630
|
this.#removeEphemeralCustomMessages();
|
|
@@ -3639,7 +4281,12 @@ export class AgentSession {
|
|
|
3639
4281
|
}
|
|
3640
4282
|
|
|
3641
4283
|
getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
|
|
3642
|
-
|
|
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();
|
|
3643
4290
|
if (!manager) return null;
|
|
3644
4291
|
const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
|
|
3645
4292
|
const running = manager.getRunningJobs(ownerFilter).map(job => ({
|
|
@@ -3672,7 +4319,12 @@ export class AgentSession {
|
|
|
3672
4319
|
*/
|
|
3673
4320
|
#cancelOwnAsyncJobs(): void {
|
|
3674
4321
|
if (!this.#agentId) return;
|
|
3675
|
-
|
|
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();
|
|
3676
4328
|
if (!manager) return;
|
|
3677
4329
|
// Run owner cleanups first so cron timers (and any other owner-scoped
|
|
3678
4330
|
// resource cleanup) cannot register fresh jobs while we tear down the
|
|
@@ -3683,7 +4335,7 @@ export class AgentSession {
|
|
|
3683
4335
|
|
|
3684
4336
|
async #settleOwnAsyncJobsBeforeArtifactRetirement(): Promise<void> {
|
|
3685
4337
|
const ownerId = this.#agentId;
|
|
3686
|
-
const manager = AsyncJobManager.instance();
|
|
4338
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
3687
4339
|
if (!ownerId || !manager) return;
|
|
3688
4340
|
const lease = manager.beginOwnerSubagentShutdown(ownerId);
|
|
3689
4341
|
if (!lease) throw new Error("Owned async cleanup is already in progress before artifact retirement.");
|
|
@@ -3707,6 +4359,7 @@ export class AgentSession {
|
|
|
3707
4359
|
manager: AsyncJobManager,
|
|
3708
4360
|
lease: OwnerSubagentShutdownLease,
|
|
3709
4361
|
ownerId: string,
|
|
4362
|
+
predecessorEndpointId: string,
|
|
3710
4363
|
): void {
|
|
3711
4364
|
const finalization = (async () => {
|
|
3712
4365
|
while (!this.#isDisposed) {
|
|
@@ -3717,7 +4370,11 @@ export class AgentSession {
|
|
|
3717
4370
|
if (!(await manager.waitForOwnerInFlightDeliveries(ownerId)))
|
|
3718
4371
|
throw new Error("owner_delivery_settlement_timeout");
|
|
3719
4372
|
if (!(await manager.cancelAndSettleOwnerJobs(ownerId))) throw new Error("owner_job_settlement_timeout");
|
|
3720
|
-
if (this.#isDisposed)
|
|
4373
|
+
if (this.#isDisposed) {
|
|
4374
|
+
retireOwnedRegistrationsForEndpoint(predecessorEndpointId);
|
|
4375
|
+
break;
|
|
4376
|
+
}
|
|
4377
|
+
retireOwnedRegistrationsForEndpoint(predecessorEndpointId);
|
|
3721
4378
|
this.sessionManager.retireEphemeralArtifactsAfterTransition();
|
|
3722
4379
|
await this.#runToolSessionTransitionCleanups();
|
|
3723
4380
|
manager.finishOwnerSubagentShutdown(lease, "commit");
|
|
@@ -3738,7 +4395,14 @@ export class AgentSession {
|
|
|
3738
4395
|
|
|
3739
4396
|
#suppressOwnAsyncJobDeliveries(): void {
|
|
3740
4397
|
if (!this.#agentId) return;
|
|
3741
|
-
|
|
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();
|
|
3742
4406
|
if (!manager) return;
|
|
3743
4407
|
const pendingJobIds = manager.getDeliveryState({ ownerId: this.#agentId }).pendingJobIds;
|
|
3744
4408
|
if (pendingJobIds.length > 0) {
|
|
@@ -3808,7 +4472,78 @@ export class AgentSession {
|
|
|
3808
4472
|
return queued;
|
|
3809
4473
|
}
|
|
3810
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
|
+
|
|
3811
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);
|
|
3812
4547
|
const terminalOwner = event.type === "agent_end" ? getAgentTerminalOwnerContext(event) : undefined;
|
|
3813
4548
|
const maintenanceCheckpoint =
|
|
3814
4549
|
event.type === "agent_end" && event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted";
|
|
@@ -3862,14 +4597,15 @@ export class AgentSession {
|
|
|
3862
4597
|
this.#postPromptLeases.set(eventLease.resourceRunId, eventLease);
|
|
3863
4598
|
if (eventLease) {
|
|
3864
4599
|
await this.#runResourceLeaseContext.run(eventLease, () =>
|
|
3865
|
-
this.#handleAgentEvent(event, activePromptHandle),
|
|
4600
|
+
this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission),
|
|
3866
4601
|
);
|
|
3867
4602
|
} else {
|
|
3868
|
-
await this.#handleAgentEvent(event, activePromptHandle);
|
|
4603
|
+
await this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission);
|
|
3869
4604
|
}
|
|
3870
4605
|
} catch (error) {
|
|
3871
4606
|
logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
|
|
3872
4607
|
} finally {
|
|
4608
|
+
canonicalAdmission?.release();
|
|
3873
4609
|
if (eventLease) {
|
|
3874
4610
|
const pendingAgentEnd =
|
|
3875
4611
|
event.type === "agent_end" && !maintenanceCheckpoint && this.#pendingAgentEndEmit === event
|
|
@@ -3892,13 +4628,43 @@ export class AgentSession {
|
|
|
3892
4628
|
return handler;
|
|
3893
4629
|
};
|
|
3894
4630
|
|
|
3895
|
-
|
|
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> {
|
|
3896
4658
|
try {
|
|
3897
|
-
await persistCoordinatorRuntimeStateFromEvent(
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
4659
|
+
await persistCoordinatorRuntimeStateFromEvent(
|
|
4660
|
+
event,
|
|
4661
|
+
{
|
|
4662
|
+
sessionId: this.sessionId,
|
|
4663
|
+
cwd: this.sessionManager.getCwd(),
|
|
4664
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
4665
|
+
},
|
|
4666
|
+
observation,
|
|
4667
|
+
);
|
|
3902
4668
|
} catch {
|
|
3903
4669
|
logger.warn("Failed to persist coordinator runtime state", { event: event.type });
|
|
3904
4670
|
}
|
|
@@ -3946,7 +4712,6 @@ export class AgentSession {
|
|
|
3946
4712
|
if (event.type === "agent_end" && event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted")
|
|
3947
4713
|
return;
|
|
3948
4714
|
|
|
3949
|
-
const persistRuntimeState = () => this.#persistRuntimeStateInBackground(event);
|
|
3950
4715
|
// Hold agent_end until the prompt's finally and all earlier async event work
|
|
3951
4716
|
// have unwound. Subscribers treat this event as the ready signal; flushing it
|
|
3952
4717
|
// from abort while either barrier is active permits a successor to race the
|
|
@@ -3959,7 +4724,7 @@ export class AgentSession {
|
|
|
3959
4724
|
if (event.type === "agent_end") {
|
|
3960
4725
|
// Start the durable terminal write before synchronous subscribers can
|
|
3961
4726
|
// re-enter prompt(), so a successor's running transition serializes after it.
|
|
3962
|
-
await
|
|
4727
|
+
await this.#queueCoordinatorRuntimeStatePersist(event);
|
|
3963
4728
|
this.#emit(event);
|
|
3964
4729
|
await this.#emitExtensionEvent(event);
|
|
3965
4730
|
return;
|
|
@@ -3969,13 +4734,22 @@ export class AgentSession {
|
|
|
3969
4734
|
// auto-continuation gates, goal reminders, and tests all observe these events
|
|
3970
4735
|
// synchronously. Coordinator sidecar writes and extension hooks are secondary
|
|
3971
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);
|
|
3972
4743
|
this.#emit(event);
|
|
3973
|
-
void persistRuntimeState();
|
|
3974
4744
|
await this.#emitExtensionEvent(event);
|
|
3975
4745
|
}
|
|
3976
4746
|
|
|
3977
4747
|
// Track last assistant message for auto-compaction check
|
|
3978
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>();
|
|
3979
4753
|
// Provider context construction must wait for this chain. Agent event listeners
|
|
3980
4754
|
// are synchronous dispatch only; their async work cannot otherwise gate the
|
|
3981
4755
|
// next tool-result provider request.
|
|
@@ -4043,7 +4817,11 @@ export class AgentSession {
|
|
|
4043
4817
|
}
|
|
4044
4818
|
|
|
4045
4819
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
4046
|
-
#handleAgentEvent = async (
|
|
4820
|
+
#handleAgentEvent = async (
|
|
4821
|
+
event: AgentEvent,
|
|
4822
|
+
activePromptHandle?: string,
|
|
4823
|
+
canonicalAdmission?: CanonicalMessageAdmission,
|
|
4824
|
+
): Promise<void> => {
|
|
4047
4825
|
const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
|
|
4048
4826
|
|
|
4049
4827
|
if (
|
|
@@ -4190,10 +4968,31 @@ export class AgentSession {
|
|
|
4190
4968
|
this.#silentAbortPending = false;
|
|
4191
4969
|
}
|
|
4192
4970
|
|
|
4193
|
-
// Canonical persistence
|
|
4194
|
-
//
|
|
4195
|
-
//
|
|
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
|
+
|
|
4196
4992
|
if (event.type === "message_end") {
|
|
4993
|
+
if (canonicalAdmission && !canonicalAdmission.predecessor.released) {
|
|
4994
|
+
await canonicalAdmission.predecessor.promise;
|
|
4995
|
+
}
|
|
4197
4996
|
if (
|
|
4198
4997
|
(event.message.role === "hookMessage" || event.message.role === "custom") &&
|
|
4199
4998
|
!(event.message.role === "custom" && event.message.customType === "hindsight-recall")
|
|
@@ -4224,6 +5023,47 @@ export class AgentSession {
|
|
|
4224
5023
|
try {
|
|
4225
5024
|
this.sessionManager.appendMessage(event.message);
|
|
4226
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
|
+
}
|
|
4227
5067
|
if (
|
|
4228
5068
|
event.message.role !== "toolResult" ||
|
|
4229
5069
|
event.message.toolName !== "todo_write" ||
|
|
@@ -4289,6 +5129,7 @@ export class AgentSession {
|
|
|
4289
5129
|
}
|
|
4290
5130
|
}
|
|
4291
5131
|
}
|
|
5132
|
+
canonicalAdmission?.release();
|
|
4292
5133
|
}
|
|
4293
5134
|
|
|
4294
5135
|
// Deobfuscate assistant message content for display emission — the LLM echoes back
|
|
@@ -4486,6 +5327,7 @@ export class AgentSession {
|
|
|
4486
5327
|
|
|
4487
5328
|
if (
|
|
4488
5329
|
event.type === "message_update" &&
|
|
5330
|
+
!this.#isProvisionalStreamingToolEvent(event) &&
|
|
4489
5331
|
(event.assistantMessageEvent.type === "toolcall_start" ||
|
|
4490
5332
|
event.assistantMessageEvent.type === "toolcall_delta" ||
|
|
4491
5333
|
event.assistantMessageEvent.type === "toolcall_end")
|
|
@@ -4495,6 +5337,7 @@ export class AgentSession {
|
|
|
4495
5337
|
|
|
4496
5338
|
if (
|
|
4497
5339
|
event.type === "message_update" &&
|
|
5340
|
+
!this.#isProvisionalStreamingToolEvent(event) &&
|
|
4498
5341
|
(event.assistantMessageEvent.type === "toolcall_end" || event.assistantMessageEvent.type === "toolcall_delta")
|
|
4499
5342
|
) {
|
|
4500
5343
|
this.#maybeAbortStreamingEdit(event, this.#promptGeneration);
|
|
@@ -4506,9 +5349,9 @@ export class AgentSession {
|
|
|
4506
5349
|
this.#markTtsrInjected(this.#extractTtsrRuleNames(event.message.details));
|
|
4507
5350
|
}
|
|
4508
5351
|
|
|
4509
|
-
//
|
|
5352
|
+
// (#lastAssistantMessage is captured synchronously before the admission
|
|
5353
|
+
// wait above; the block below handles assistant side effects only.)
|
|
4510
5354
|
if (event.message.role === "assistant") {
|
|
4511
|
-
this.#lastAssistantMessage = event.message;
|
|
4512
5355
|
const assistantMsg = event.message as AssistantMessage;
|
|
4513
5356
|
const currentGrantsAnthropicPriority =
|
|
4514
5357
|
this.serviceTier === "priority" || this.serviceTier === "claude-only";
|
|
@@ -4558,6 +5401,9 @@ export class AgentSession {
|
|
|
4558
5401
|
// #resolveRetry() is idempotent, so the later tail call is a no-op.
|
|
4559
5402
|
this.#resolveRetry();
|
|
4560
5403
|
}
|
|
5404
|
+
if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "aborted") {
|
|
5405
|
+
this.#providerRetryMaxAttempts = undefined;
|
|
5406
|
+
}
|
|
4561
5407
|
}
|
|
4562
5408
|
|
|
4563
5409
|
if (event.message.role === "toolResult") {
|
|
@@ -4631,6 +5477,13 @@ export class AgentSession {
|
|
|
4631
5477
|
}
|
|
4632
5478
|
}
|
|
4633
5479
|
|
|
5480
|
+
if (
|
|
5481
|
+
event.type === "agent_end" &&
|
|
5482
|
+
!(event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted")
|
|
5483
|
+
) {
|
|
5484
|
+
this.#releaseDeferredSdkFollowUps();
|
|
5485
|
+
}
|
|
5486
|
+
|
|
4634
5487
|
// Check auto-retry and auto-compaction after agent completes
|
|
4635
5488
|
if (event.type === "agent_end") {
|
|
4636
5489
|
// Cooperative mid-run maintenance interruption (issue #2035). The loop
|
|
@@ -4685,6 +5538,16 @@ export class AgentSession {
|
|
|
4685
5538
|
.find((message): message is AssistantMessage => message.role === "assistant");
|
|
4686
5539
|
const msg = this.#lastAssistantMessage ?? fallbackAssistant;
|
|
4687
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
|
+
}
|
|
4688
5551
|
if (!msg) {
|
|
4689
5552
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
4690
5553
|
this.#resolveRetry();
|
|
@@ -4813,8 +5676,14 @@ export class AgentSession {
|
|
|
4813
5676
|
this.#postPromptTasksPromise = undefined;
|
|
4814
5677
|
}
|
|
4815
5678
|
|
|
4816
|
-
#trackPostPromptTask(
|
|
5679
|
+
#trackPostPromptTask(
|
|
5680
|
+
task: Promise<void>,
|
|
5681
|
+
selectionFenceGeneration: number,
|
|
5682
|
+
lease?: RunResourceProducerLease,
|
|
5683
|
+
leaseTask: Promise<void> = task,
|
|
5684
|
+
): void {
|
|
4817
5685
|
this.#postPromptTasks.add(task);
|
|
5686
|
+
this.#postPromptTaskSelectionFenceGenerations.set(task, selectionFenceGeneration);
|
|
4818
5687
|
this.#ensurePostPromptTasksPromise();
|
|
4819
5688
|
if (lease) {
|
|
4820
5689
|
lease.track("post_prompt", "agent-session", leaseTask);
|
|
@@ -4824,6 +5693,7 @@ export class AgentSession {
|
|
|
4824
5693
|
.catch(() => {})
|
|
4825
5694
|
.finally(() => {
|
|
4826
5695
|
this.#postPromptTasks.delete(task);
|
|
5696
|
+
this.#postPromptTaskSelectionFenceGenerations.delete(task);
|
|
4827
5697
|
if (this.#postPromptTasks.size === 0) this.#resolvePostPromptTasks();
|
|
4828
5698
|
});
|
|
4829
5699
|
}
|
|
@@ -4836,8 +5706,14 @@ export class AgentSession {
|
|
|
4836
5706
|
onSkip?: () => void;
|
|
4837
5707
|
resourceRunId?: string;
|
|
4838
5708
|
leaseTask?: Promise<void>;
|
|
5709
|
+
selectionFenceGeneration?: number;
|
|
4839
5710
|
},
|
|
4840
5711
|
): Promise<void> {
|
|
5712
|
+
const selectionFenceGeneration =
|
|
5713
|
+
options?.selectionFenceGeneration ??
|
|
5714
|
+
this.#selectionFenceGenerationContext.getStore() ??
|
|
5715
|
+
this.#sessionAdmissionContext.getStore()?.selectionFenceGeneration ??
|
|
5716
|
+
this.#selectionFenceGeneration;
|
|
4841
5717
|
const delayMs = options?.delayMs ?? 0;
|
|
4842
5718
|
const resourceRunId = options?.resourceRunId;
|
|
4843
5719
|
const contextualLease = this.#runResourceLeaseContext.getStore();
|
|
@@ -4875,12 +5751,17 @@ export class AgentSession {
|
|
|
4875
5751
|
options.onSkip?.();
|
|
4876
5752
|
return;
|
|
4877
5753
|
}
|
|
4878
|
-
await task(signal);
|
|
5754
|
+
await this.#selectionFenceGenerationContext.run(selectionFenceGeneration, () => task(signal));
|
|
4879
5755
|
};
|
|
4880
5756
|
const scheduled = reservation?.ok
|
|
4881
5757
|
? this.#runResourceLeaseContext.run(reservation.lease, runScheduled)
|
|
4882
5758
|
: runScheduled();
|
|
4883
|
-
this.#trackPostPromptTask(
|
|
5759
|
+
this.#trackPostPromptTask(
|
|
5760
|
+
scheduled,
|
|
5761
|
+
selectionFenceGeneration,
|
|
5762
|
+
reservation?.ok ? reservation.lease : undefined,
|
|
5763
|
+
options?.leaseTask,
|
|
5764
|
+
);
|
|
4884
5765
|
return scheduled;
|
|
4885
5766
|
}
|
|
4886
5767
|
|
|
@@ -4894,9 +5775,10 @@ export class AgentSession {
|
|
|
4894
5775
|
skipCompactionCheck?: boolean;
|
|
4895
5776
|
suppressPredecessorAgentEnd?: boolean;
|
|
4896
5777
|
shouldContinue?: () => boolean;
|
|
4897
|
-
onSkip?: (
|
|
5778
|
+
onSkip?: (
|
|
5779
|
+
reason: "generation_changed" | "aborted_signal" | "queue_drained" | "handoff_in_progress" | "terminal_turn",
|
|
5780
|
+
) => void;
|
|
4898
5781
|
allowDuringCancelAndSubmit?: boolean;
|
|
4899
|
-
rescheduleOnBusy?: boolean;
|
|
4900
5782
|
onError?: (error: unknown) => void;
|
|
4901
5783
|
resourceRunId?: string;
|
|
4902
5784
|
maintenanceContinuation?: boolean;
|
|
@@ -4905,14 +5787,63 @@ export class AgentSession {
|
|
|
4905
5787
|
// queued turn without replaying the non-assistant tail. Every other
|
|
4906
5788
|
// scheduled continuation keeps continue() semantics.
|
|
4907
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;
|
|
4908
5800
|
}): Promise<void> {
|
|
4909
|
-
const
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
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;
|
|
4916
5847
|
this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
4917
5848
|
options?.onSkip?.(reason);
|
|
4918
5849
|
};
|
|
@@ -4932,126 +5863,161 @@ export class AgentSession {
|
|
|
4932
5863
|
const settleLease = () => leaseSettlement.resolve();
|
|
4933
5864
|
return this.#schedulePostPromptTask(
|
|
4934
5865
|
async scheduledSignal => {
|
|
4935
|
-
const canContinue = (): boolean => {
|
|
4936
|
-
if (scheduledSignal.aborted || this.#isDisposed) {
|
|
4937
|
-
skip("aborted_signal");
|
|
4938
|
-
return false;
|
|
4939
|
-
}
|
|
4940
|
-
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
4941
|
-
skip("generation_changed");
|
|
4942
|
-
return false;
|
|
4943
|
-
}
|
|
4944
|
-
if (this.#cancelAndSubmitInProgress && !options?.allowDuringCancelAndSubmit) {
|
|
4945
|
-
skip("queue_drained");
|
|
4946
|
-
return false;
|
|
4947
|
-
}
|
|
4948
|
-
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
4949
|
-
skip("queue_drained");
|
|
4950
|
-
return false;
|
|
4951
|
-
}
|
|
4952
|
-
return true;
|
|
4953
|
-
};
|
|
4954
|
-
if (!canContinue()) {
|
|
4955
|
-
settleLease();
|
|
4956
|
-
return;
|
|
4957
|
-
}
|
|
4958
|
-
await this.#awaitStartupTurnBarrier();
|
|
4959
|
-
if (!canContinue()) {
|
|
4960
|
-
settleLease();
|
|
4961
|
-
return;
|
|
4962
|
-
}
|
|
4963
5866
|
try {
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
}
|
|
4972
|
-
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
4973
|
-
skip("generation_changed");
|
|
4974
|
-
return;
|
|
4975
|
-
}
|
|
4976
|
-
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
4977
|
-
skip("queue_drained");
|
|
4978
|
-
return;
|
|
4979
|
-
}
|
|
4980
|
-
// A continuation scheduled before a handoff engaged must not start a
|
|
4981
|
-
// turn against the session being handed off (or the restored
|
|
4982
|
-
// predecessor). rearmIdle / normal delivery resumes after the fence.
|
|
4983
|
-
if (this.#handoffTransitionActive) {
|
|
4984
|
-
skip("handoff_in_progress");
|
|
4985
|
-
return;
|
|
4986
|
-
}
|
|
4987
|
-
const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
|
|
4988
|
-
let predecessorAccepted = false;
|
|
4989
|
-
const releasePredecessor = () => {
|
|
4990
|
-
if (predecessorAccepted) return;
|
|
4991
|
-
predecessorAccepted = true;
|
|
4992
|
-
this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
|
|
4993
|
-
};
|
|
4994
|
-
const hasQueuedMessages = this.agent.hasQueuedMessages();
|
|
4995
|
-
const startsQueuedSuccessor =
|
|
4996
|
-
hasQueuedMessages &&
|
|
4997
|
-
(options?.continueQueuedOnly === true || this.agent.state.messages.at(-1)?.role === "assistant");
|
|
4998
|
-
const continueQueued = options?.continueQueuedOnly
|
|
4999
|
-
? this.agent.continueQueuedMessages.bind(this.agent)
|
|
5000
|
-
: this.agent.continue.bind(this.agent);
|
|
5001
|
-
try {
|
|
5002
|
-
await continueQueued({
|
|
5003
|
-
...this.#managedFallbackPromptOptions(),
|
|
5004
|
-
maintenanceContinuation: options?.maintenanceContinuation,
|
|
5005
|
-
// Reset only after continue() has claimed the queued turn. Skipped or stale
|
|
5006
|
-
// continuations retain predecessor accounting, and resetAttemptBudget keeps
|
|
5007
|
-
// the sticky fallback cursor unchanged.
|
|
5008
|
-
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
5009
|
-
this.#acceptRunHandle(handle);
|
|
5010
|
-
settleLease();
|
|
5011
|
-
releasePredecessor();
|
|
5012
|
-
if (startsQueuedSuccessor) {
|
|
5013
|
-
this.#defaultFallbackChain().resetAttemptBudget();
|
|
5014
|
-
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;
|
|
5015
5874
|
}
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
predecessorAgentEnd,
|
|
5033
|
-
);
|
|
5034
|
-
logger.debug("agent.continue busy after scheduling; rescheduling", {
|
|
5035
|
-
error: error.message,
|
|
5036
|
-
attempt: busyReschedules,
|
|
5037
|
-
delayMs: nextDelayMs,
|
|
5038
|
-
});
|
|
5039
|
-
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();
|
|
5040
5891
|
return;
|
|
5041
5892
|
}
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
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
|
+
);
|
|
5051
6018
|
} catch (error) {
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
settleLease();
|
|
6019
|
+
if (scheduledSignal.aborted) skip("aborted_signal");
|
|
6020
|
+
else fail(error);
|
|
5055
6021
|
}
|
|
5056
6022
|
},
|
|
5057
6023
|
{
|
|
@@ -5062,12 +6028,12 @@ export class AgentSession {
|
|
|
5062
6028
|
},
|
|
5063
6029
|
resourceRunId: options?.resourceRunId,
|
|
5064
6030
|
leaseTask: leaseSettlement.promise,
|
|
6031
|
+
selectionFenceGeneration,
|
|
5065
6032
|
},
|
|
5066
6033
|
);
|
|
5067
6034
|
};
|
|
5068
6035
|
return scheduleAttempt();
|
|
5069
6036
|
}
|
|
5070
|
-
|
|
5071
6037
|
#logCompactionContinuationSkipped(
|
|
5072
6038
|
source: "auto_continue_prompt" | "queued_continue" | "overflow_retry",
|
|
5073
6039
|
reason: string,
|
|
@@ -5128,7 +6094,6 @@ export class AgentSession {
|
|
|
5128
6094
|
generation,
|
|
5129
6095
|
suppressPredecessorAgentEnd: true,
|
|
5130
6096
|
resourceRunId,
|
|
5131
|
-
rescheduleOnBusy: true,
|
|
5132
6097
|
onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
|
|
5133
6098
|
onError: error => this.#logCompactionContinuationError("overflow_retry", error),
|
|
5134
6099
|
});
|
|
@@ -5145,8 +6110,13 @@ export class AgentSession {
|
|
|
5145
6110
|
return false;
|
|
5146
6111
|
}
|
|
5147
6112
|
|
|
5148
|
-
#scheduleAutoContinuePrompt(
|
|
5149
|
-
|
|
6113
|
+
#scheduleAutoContinuePrompt(
|
|
6114
|
+
generation: number,
|
|
6115
|
+
requireUnfinishedWork = true,
|
|
6116
|
+
resourceRunId?: string,
|
|
6117
|
+
deferredSelectionFenceGeneration?: number,
|
|
6118
|
+
deferredPredecessorAgentEnd?: AgentSessionEvent,
|
|
6119
|
+
): void {
|
|
5150
6120
|
const scheduledGeneration = generation;
|
|
5151
6121
|
const continuationAuthorized = async (
|
|
5152
6122
|
signal: AbortSignal,
|
|
@@ -5164,6 +6134,13 @@ export class AgentSession {
|
|
|
5164
6134
|
);
|
|
5165
6135
|
return false;
|
|
5166
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
|
+
}
|
|
5167
6144
|
const authorized = requireUnfinishedWork
|
|
5168
6145
|
? this.#hasUnfinishedWork(snapshot) || hasPendingNextTurnMessages
|
|
5169
6146
|
: snapshot.queuedMessages ||
|
|
@@ -5173,81 +6150,95 @@ export class AgentSession {
|
|
|
5173
6150
|
if (!authorized) this.emitNotice("info", "Auto-continue skipped: no unfinished work detected");
|
|
5174
6151
|
return authorized;
|
|
5175
6152
|
};
|
|
5176
|
-
|
|
5177
|
-
const
|
|
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
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
if (
|
|
5215
|
-
|
|
5216
|
-
const nextDelayMs = agentContinueBusyRescheduleDelayMs(busyReschedules);
|
|
5217
|
-
logger.debug("Auto-compaction continuation busy; rescheduling", {
|
|
5218
|
-
source: "auto_continue_prompt",
|
|
5219
|
-
reason: "queue_drained",
|
|
5220
|
-
error: error.message,
|
|
5221
|
-
attempt: busyReschedules,
|
|
5222
|
-
delayMs: nextDelayMs,
|
|
5223
|
-
});
|
|
5224
|
-
rescheduled = true;
|
|
5225
|
-
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");
|
|
5226
6193
|
return;
|
|
5227
6194
|
}
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
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) {
|
|
5244
6223
|
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
5245
|
-
|
|
5246
|
-
}
|
|
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);
|
|
5247
6239
|
},
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
scheduleAttempt();
|
|
6240
|
+
},
|
|
6241
|
+
);
|
|
5251
6242
|
}
|
|
5252
6243
|
|
|
5253
6244
|
async #cancelPostPromptTasks(): Promise<void> {
|
|
@@ -5273,6 +6264,7 @@ export class AgentSession {
|
|
|
5273
6264
|
this.#postPromptTasksAbortController.abort();
|
|
5274
6265
|
this.#postPromptTasksAbortController = new AbortController();
|
|
5275
6266
|
this.#postPromptTasks.clear();
|
|
6267
|
+
this.#postPromptTaskSelectionFenceGenerations.clear();
|
|
5276
6268
|
this.#releaseDeferredAgentEndContinuations();
|
|
5277
6269
|
this.#resolveTtsrResume();
|
|
5278
6270
|
this.#resolvePostPromptTasks();
|
|
@@ -5309,6 +6301,19 @@ export class AgentSession {
|
|
|
5309
6301
|
}
|
|
5310
6302
|
}
|
|
5311
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
|
+
|
|
5312
6317
|
/** Get TTSR injection payload and clear pending injections. */
|
|
5313
6318
|
#getTtsrInjectionContent(): { content: string; rules: Rule[] } | undefined {
|
|
5314
6319
|
if (this.#pendingTtsrInjections.length === 0) return undefined;
|
|
@@ -5667,6 +6672,13 @@ export class AgentSession {
|
|
|
5667
6672
|
}
|
|
5668
6673
|
}
|
|
5669
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
|
+
|
|
5670
6682
|
#streamingEditPrecachePending = new Set<string>();
|
|
5671
6683
|
async #preCacheFileAsync(resolvedPath: string): Promise<void> {
|
|
5672
6684
|
if (this.#streamingEditFileCache.has(resolvedPath)) return;
|
|
@@ -6013,7 +7025,16 @@ export class AgentSession {
|
|
|
6013
7025
|
try {
|
|
6014
7026
|
if (event.type === "agent_start") {
|
|
6015
7027
|
this.#turnIndex = 0;
|
|
6016
|
-
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
|
+
);
|
|
6017
7038
|
} else if (event.type === "agent_end") {
|
|
6018
7039
|
await this.#extensionRunner.emit(
|
|
6019
7040
|
{
|
|
@@ -6251,6 +7272,81 @@ export class AgentSession {
|
|
|
6251
7272
|
);
|
|
6252
7273
|
}
|
|
6253
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
|
+
|
|
6254
7350
|
#rekeyHindsightMemoryForCurrentSessionId(): void {
|
|
6255
7351
|
if (resolveMemoryBackendId(this.settings) !== "hindsight") return;
|
|
6256
7352
|
|
|
@@ -6322,7 +7418,13 @@ export class AgentSession {
|
|
|
6322
7418
|
await admissionClosed;
|
|
6323
7419
|
await this.#agentEndPublicationPromise;
|
|
6324
7420
|
await this.#queuedExtensionEvents;
|
|
6325
|
-
|
|
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;
|
|
6326
7428
|
this.#pendingBackgroundExchanges = [];
|
|
6327
7429
|
this.yieldQueue.clear();
|
|
6328
7430
|
|
|
@@ -6334,6 +7436,7 @@ export class AgentSession {
|
|
|
6334
7436
|
} catch (error) {
|
|
6335
7437
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
6336
7438
|
}
|
|
7439
|
+
this.#workflowGateEmitter?.fence?.();
|
|
6337
7440
|
this.#workflowGateEmitter = undefined;
|
|
6338
7441
|
this.#notifyWorkflowGateEmitterChanged(this.sessionId, undefined);
|
|
6339
7442
|
await this.#flushWorkerIntegrationAttempt();
|
|
@@ -6345,7 +7448,7 @@ export class AgentSession {
|
|
|
6345
7448
|
this.#cancelOwnAsyncJobs();
|
|
6346
7449
|
await Promise.allSettled(this.#deferredOwnerShutdownFinalizations);
|
|
6347
7450
|
const ownedAsyncManager = this.#ownedAsyncJobManager;
|
|
6348
|
-
if (ownedAsyncManager) {
|
|
7451
|
+
if (ownedAsyncManager && this.#disposeAsyncJobManager) {
|
|
6349
7452
|
const drained = await ownedAsyncManager.dispose({ timeoutMs: 3_000 });
|
|
6350
7453
|
const deliveryState = ownedAsyncManager.getDeliveryState();
|
|
6351
7454
|
if (drained === false && deliveryState) {
|
|
@@ -6400,6 +7503,7 @@ export class AgentSession {
|
|
|
6400
7503
|
await this.memoryBackend.dispose();
|
|
6401
7504
|
if (this.#workspaceTreeService) await this.#workspaceTreeService.dispose();
|
|
6402
7505
|
if (this.#networkPrewarmService) await this.#networkPrewarmService.dispose();
|
|
7506
|
+
this.#modelRegistry.authStorage?.releaseCredentialScope(this.credentialSessionId);
|
|
6403
7507
|
await this.sessionManager.close();
|
|
6404
7508
|
this.#closeAllProviderSessions("dispose");
|
|
6405
7509
|
const hindsightState = this.getHindsightSessionState();
|
|
@@ -6514,24 +7618,58 @@ export class AgentSession {
|
|
|
6514
7618
|
}
|
|
6515
7619
|
|
|
6516
7620
|
/** Wait until streaming and session settlement work are fully settled. */
|
|
6517
|
-
|
|
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> {
|
|
6518
7631
|
while (true) {
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
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);
|
|
6522
7639
|
if (
|
|
6523
7640
|
!this.agent.state.isStreaming &&
|
|
6524
7641
|
!this.#retryPromise &&
|
|
6525
7642
|
!this.#ttsrResumePromise &&
|
|
6526
7643
|
!this.#postPromptTasksPromise &&
|
|
6527
|
-
!this.#isSessionSettlementPending()
|
|
7644
|
+
!this.#isSessionSettlementPending(ignoreSelectionFenceGeneration)
|
|
6528
7645
|
)
|
|
6529
7646
|
return;
|
|
6530
7647
|
}
|
|
6531
7648
|
}
|
|
6532
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
|
+
|
|
6533
7671
|
async drainAsyncJobDeliveriesForAcp(options?: { timeoutMs?: number }): Promise<boolean> {
|
|
6534
|
-
const manager = AsyncJobManager.instance();
|
|
7672
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
6535
7673
|
if (!manager) return false;
|
|
6536
7674
|
const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
|
|
6537
7675
|
const before = manager.getDeliveryState(ownerFilter);
|
|
@@ -6555,7 +7693,7 @@ export class AgentSession {
|
|
|
6555
7693
|
* any remaining queued/delivering work is observable and can block mutation.
|
|
6556
7694
|
*/
|
|
6557
7695
|
getAsyncDeliveryStateForAcp(): { queued: number; delivering: boolean } {
|
|
6558
|
-
const manager = AsyncJobManager.instance();
|
|
7696
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
6559
7697
|
if (!manager) return { queued: 0, delivering: false };
|
|
6560
7698
|
const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
|
|
6561
7699
|
return manager.getDeliveryState(ownerFilter);
|
|
@@ -7113,9 +8251,15 @@ export class AgentSession {
|
|
|
7113
8251
|
activeSkillState: this.getActiveSkillState(),
|
|
7114
8252
|
sessionId: this.sessionManager.getSessionId(),
|
|
7115
8253
|
}),
|
|
8254
|
+
() => this.getSessionAgentDir(),
|
|
7116
8255
|
),
|
|
7117
8256
|
),
|
|
7118
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);
|
|
7119
8263
|
if (!wrappersByVersion) {
|
|
7120
8264
|
wrappersByVersion = new Map();
|
|
7121
8265
|
this.#guardedToolWrapperCache.set(tool, wrappersByVersion);
|
|
@@ -7241,6 +8385,9 @@ export class AgentSession {
|
|
|
7241
8385
|
const refreshedTool = await this.#reloadSshTool();
|
|
7242
8386
|
if (refreshedTool) {
|
|
7243
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);
|
|
7244
8391
|
} else {
|
|
7245
8392
|
this.#toolRegistry.delete("ssh");
|
|
7246
8393
|
this.#selectedDiscoveredToolNames.delete("ssh");
|
|
@@ -7842,6 +8989,36 @@ export class AgentSession {
|
|
|
7842
8989
|
return this.#credentialSessionId ?? this.sessionId;
|
|
7843
8990
|
}
|
|
7844
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
|
+
|
|
7845
9022
|
/** Current session display name, if set */
|
|
7846
9023
|
get sessionName(): string | undefined {
|
|
7847
9024
|
return this.sessionManager.getSessionName();
|
|
@@ -7895,8 +9072,12 @@ export class AgentSession {
|
|
|
7895
9072
|
async invokeSkill(
|
|
7896
9073
|
name: string,
|
|
7897
9074
|
args = "",
|
|
7898
|
-
options?: Pick<
|
|
9075
|
+
options?: Pick<
|
|
9076
|
+
PromptOptions,
|
|
9077
|
+
"onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared" | "preflightSignal"
|
|
9078
|
+
>,
|
|
7899
9079
|
): Promise<{ name: string; path: string; args?: string; lineCount?: number }> {
|
|
9080
|
+
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
7900
9081
|
const skillName = name.trim();
|
|
7901
9082
|
if (!skillName) throw Object.assign(new Error("skill.invoke requires a skill name."), { code: "invalid_input" });
|
|
7902
9083
|
if (typeof args !== "string")
|
|
@@ -7908,18 +9089,24 @@ export class AgentSession {
|
|
|
7908
9089
|
throw Object.assign(new Error(`Skill ${skillName} was not found.${availableHint}`), { code: "invalid_input" });
|
|
7909
9090
|
}
|
|
7910
9091
|
const deepInterviewUserIntentEpoch = this.#claimDeepInterviewUserIntent();
|
|
7911
|
-
const activation = await
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
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
|
+
);
|
|
7923
9110
|
const skillPromptMessage = {
|
|
7924
9111
|
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
7925
9112
|
content: built.message,
|
|
@@ -7928,6 +9115,7 @@ export class AgentSession {
|
|
|
7928
9115
|
attribution: "user" as const,
|
|
7929
9116
|
};
|
|
7930
9117
|
this.#deepInterviewPreclaimedCustomInputEpochs.set(skillPromptMessage, deepInterviewUserIntentEpoch);
|
|
9118
|
+
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
7931
9119
|
options?.onSkillPrepared?.({
|
|
7932
9120
|
name: skill.name,
|
|
7933
9121
|
path: skill.filePath,
|
|
@@ -8161,6 +9349,11 @@ export class AgentSession {
|
|
|
8161
9349
|
const wrappedTool = wrapToolWithMetaNotice(createdAskTool as unknown as AgentTool);
|
|
8162
9350
|
askTool = this.#extensionRunner ? new ExtensionToolWrapper(wrappedTool, this.#extensionRunner) : wrappedTool;
|
|
8163
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);
|
|
8164
9357
|
}
|
|
8165
9358
|
|
|
8166
9359
|
try {
|
|
@@ -8609,6 +9802,8 @@ export class AgentSession {
|
|
|
8609
9802
|
*/
|
|
8610
9803
|
async prompt(text: string, options?: PromptOptions): Promise<void> {
|
|
8611
9804
|
this.#assertRecoveryHydrationPromoted();
|
|
9805
|
+
const owner = this.#sessionAdmissionContext.getStore();
|
|
9806
|
+
if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
|
|
8612
9807
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
8613
9808
|
|
|
8614
9809
|
if (expandPromptTemplates && text.startsWith("/skill:") && !options?.images?.length) {
|
|
@@ -8620,12 +9815,13 @@ export class AgentSession {
|
|
|
8620
9815
|
await this.invokeSkill(
|
|
8621
9816
|
invocation.skill.name,
|
|
8622
9817
|
invocation.args,
|
|
8623
|
-
options?.onPreflightAccepted || options?.onPreflightAcceptCommit
|
|
9818
|
+
options?.onPreflightAccepted || options?.onPreflightAcceptCommit || options?.preflightSignal
|
|
8624
9819
|
? {
|
|
8625
9820
|
...(options.onPreflightAccepted ? { onPreflightAccepted: options.onPreflightAccepted } : {}),
|
|
8626
9821
|
...(options.onPreflightAcceptCommit
|
|
8627
9822
|
? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
|
|
8628
9823
|
: {}),
|
|
9824
|
+
...(options.preflightSignal ? { preflightSignal: options.preflightSignal } : {}),
|
|
8629
9825
|
}
|
|
8630
9826
|
: undefined,
|
|
8631
9827
|
);
|
|
@@ -8662,6 +9858,13 @@ export class AgentSession {
|
|
|
8662
9858
|
assertImagePlaceholdersHavePayload(expandedText, options?.images);
|
|
8663
9859
|
const workflowIntentDiff = options?.synthetic ? null : buildWorkflowIntentDiff(expandedText);
|
|
8664
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
|
+
}
|
|
8665
9868
|
const deepInterviewUserIntentEpoch =
|
|
8666
9869
|
claimsGenuineUserIntent && !this.isStreaming ? this.#claimDeepInterviewUserIntent() : undefined;
|
|
8667
9870
|
|
|
@@ -8686,64 +9889,67 @@ export class AgentSession {
|
|
|
8686
9889
|
return;
|
|
8687
9890
|
}
|
|
8688
9891
|
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
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
|
+
}
|
|
8696
9899
|
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
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;
|
|
8701
9904
|
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
const promptAttribution = options?.attribution ?? (options?.synthetic ? "agent" : "user");
|
|
8708
|
-
const message = options?.synthetic
|
|
8709
|
-
? {
|
|
8710
|
-
role: "developer" as const,
|
|
8711
|
-
content: userContent,
|
|
8712
|
-
attribution: promptAttribution,
|
|
8713
|
-
timestamp: Date.now(),
|
|
8714
|
-
}
|
|
8715
|
-
: { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
|
|
8716
|
-
if (deepInterviewUserIntentEpoch !== undefined)
|
|
8717
|
-
this.#deepInterviewGenuineUserMessageEpochs.set(message, deepInterviewUserIntentEpoch);
|
|
8718
|
-
await this.refreshGjcSubskillTools();
|
|
9905
|
+
const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
|
|
9906
|
+
if (options?.images) {
|
|
9907
|
+
userContent.push(...options.images);
|
|
9908
|
+
}
|
|
8719
9909
|
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
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
|
+
}
|
|
8725
9928
|
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
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
|
+
);
|
|
8742
9947
|
}
|
|
8743
9948
|
|
|
8744
9949
|
async #syncSkillPromptActiveState(
|
|
8745
9950
|
message: Pick<CustomMessage<unknown>, "customType" | "details">,
|
|
8746
9951
|
active: boolean,
|
|
9952
|
+
persistActiveState = true,
|
|
8747
9953
|
): Promise<void> {
|
|
8748
9954
|
if (message.customType !== SKILL_PROMPT_MESSAGE_TYPE) return;
|
|
8749
9955
|
const details = message.details;
|
|
@@ -8767,8 +9973,12 @@ export class AgentSession {
|
|
|
8767
9973
|
// initial mode-state + active row only when the skill is not already
|
|
8768
9974
|
// active, so the mutation guard and Stop hook engage immediately instead
|
|
8769
9975
|
// of relying on the skill prompt to run its own state-init steps.
|
|
8770
|
-
if (active) {
|
|
8771
|
-
await ensureWorkflowSkillActivationState({
|
|
9976
|
+
if (active && persistActiveState) {
|
|
9977
|
+
await ensureWorkflowSkillActivationState({
|
|
9978
|
+
cwd: this.sessionManager.getCwd(),
|
|
9979
|
+
skill,
|
|
9980
|
+
sessionId,
|
|
9981
|
+
});
|
|
8772
9982
|
const subskillDetails = details as {
|
|
8773
9983
|
subskillActivation?: LoadedSubskillActivation;
|
|
8774
9984
|
subskillActivationSet?: LoadedSubskillActivation[];
|
|
@@ -8802,9 +10012,10 @@ export class AgentSession {
|
|
|
8802
10012
|
async #syncSkillPromptActiveStateSafely(
|
|
8803
10013
|
message: Pick<CustomMessage<unknown>, "customType" | "details">,
|
|
8804
10014
|
active: boolean,
|
|
10015
|
+
persistActiveState = true,
|
|
8805
10016
|
): Promise<void> {
|
|
8806
10017
|
try {
|
|
8807
|
-
await this.#syncSkillPromptActiveState(message, active);
|
|
10018
|
+
await this.#syncSkillPromptActiveState(message, active, persistActiveState);
|
|
8808
10019
|
} catch {
|
|
8809
10020
|
// Skill HUD state is observational; a filesystem write failure must not
|
|
8810
10021
|
// interrupt the prompt turn it is visualizing. The native Stop hook still
|
|
@@ -8812,13 +10023,48 @@ export class AgentSession {
|
|
|
8812
10023
|
}
|
|
8813
10024
|
}
|
|
8814
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
|
+
|
|
8815
10055
|
async promptCustomMessage<T = unknown>(
|
|
8816
10056
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
8817
10057
|
options?: Pick<
|
|
8818
10058
|
PromptOptions,
|
|
8819
|
-
|
|
10059
|
+
| "streamingBehavior"
|
|
10060
|
+
| "toolChoice"
|
|
10061
|
+
| "followUpQueuePolicy"
|
|
10062
|
+
| "onPreflightAccepted"
|
|
10063
|
+
| "onPreflightAcceptCommit"
|
|
10064
|
+
| "preflightSignal"
|
|
8820
10065
|
>,
|
|
8821
10066
|
): Promise<void> {
|
|
10067
|
+
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
8822
10068
|
const textContent =
|
|
8823
10069
|
typeof message.content === "string"
|
|
8824
10070
|
? message.content
|
|
@@ -8849,31 +10095,61 @@ export class AgentSession {
|
|
|
8849
10095
|
|
|
8850
10096
|
const admissionGeneration = this.#promptGeneration;
|
|
8851
10097
|
const admissionSignal = this.#promptPreflightAbortController.signal;
|
|
8852
|
-
await this.#withSessionAdmission(
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
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
|
+
);
|
|
8877
10153
|
}
|
|
8878
10154
|
|
|
8879
10155
|
async #promptWithMessage(
|
|
@@ -8881,7 +10157,12 @@ export class AgentSession {
|
|
|
8881
10157
|
expandedText: string,
|
|
8882
10158
|
options?: Pick<
|
|
8883
10159
|
PromptOptions,
|
|
8884
|
-
|
|
10160
|
+
| "toolChoice"
|
|
10161
|
+
| "images"
|
|
10162
|
+
| "skipCompactionCheck"
|
|
10163
|
+
| "onPreflightAccepted"
|
|
10164
|
+
| "onPreflightAcceptCommit"
|
|
10165
|
+
| "preflightSignal"
|
|
8885
10166
|
> & {
|
|
8886
10167
|
prependMessages?: AgentMessage[];
|
|
8887
10168
|
skipPostPromptRecoveryWait?: boolean;
|
|
@@ -8893,16 +10174,59 @@ export class AgentSession {
|
|
|
8893
10174
|
},
|
|
8894
10175
|
): Promise<void> {
|
|
8895
10176
|
this.#assertNoHandoffTransition();
|
|
8896
|
-
|
|
10177
|
+
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
10178
|
+
await awaitPromptInvocationPreflight(this.#agentEndPublicationPromise, options?.preflightSignal);
|
|
8897
10179
|
// Re-check after the publication await: a handoff can engage during that
|
|
8898
10180
|
// window, and #beginInFlight below would otherwise start a turn against the
|
|
8899
10181
|
// session being handed off.
|
|
8900
10182
|
this.#assertNoHandoffTransition();
|
|
8901
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
|
+
}
|
|
8902
10213
|
const predecessorAgentEndHold =
|
|
8903
10214
|
options?.predecessorAgentEndHold ?? this.#reserveDeferredAgentEndForContinuation();
|
|
8904
10215
|
const generation = this.#promptGeneration;
|
|
8905
|
-
|
|
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;
|
|
8906
10230
|
const rosterClaim = this.#claimIrcRosterCandidate();
|
|
8907
10231
|
let hasPendingNextTurnMessages = false;
|
|
8908
10232
|
let pendingNextTurnMessageCount = 0;
|
|
@@ -8949,7 +10273,7 @@ export class AgentSession {
|
|
|
8949
10273
|
await this.#checkEstimatedContextBeforePrompt([
|
|
8950
10274
|
...(options?.prependMessages ?? []),
|
|
8951
10275
|
message,
|
|
8952
|
-
...this.#pendingNextTurnMessages,
|
|
10276
|
+
...this.#pendingNextTurnMessages.map(entry => entry.message),
|
|
8953
10277
|
]);
|
|
8954
10278
|
}
|
|
8955
10279
|
|
|
@@ -9108,8 +10432,14 @@ export class AgentSession {
|
|
|
9108
10432
|
messages.push(message);
|
|
9109
10433
|
|
|
9110
10434
|
// Re-present captured pending next-turn messages (never drained here).
|
|
9111
|
-
|
|
9112
|
-
|
|
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);
|
|
9113
10443
|
}
|
|
9114
10444
|
messages.push(...fileMentionMessages);
|
|
9115
10445
|
if (hindsightRecall) {
|
|
@@ -9153,6 +10483,15 @@ export class AgentSession {
|
|
|
9153
10483
|
attemptMessages = undefined;
|
|
9154
10484
|
},
|
|
9155
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
|
+
}
|
|
9156
10495
|
|
|
9157
10496
|
const agentPromptOptions = {
|
|
9158
10497
|
...(options?.toolChoice ? { toolChoice: options.toolChoice } : undefined),
|
|
@@ -9168,11 +10507,20 @@ export class AgentSession {
|
|
|
9168
10507
|
this.getHindsightSessionState()?.markRecallSnippetInjected(hindsightRecall);
|
|
9169
10508
|
}
|
|
9170
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));
|
|
9171
10518
|
this.#pendingNextTurnMessages.splice(0, pendingNextTurnMessageCount);
|
|
9172
10519
|
}
|
|
9173
10520
|
if (this.#cancelAndSubmitInProgress) this.#cancelAndSubmitPendingNextTurnDrained = true;
|
|
9174
10521
|
},
|
|
9175
10522
|
};
|
|
10523
|
+
|
|
9176
10524
|
await this.#promptAgentWithIdleRetry(preSubmit, agentPromptOptions, predecessorAgentEndHold, {
|
|
9177
10525
|
signal: preflightSignal,
|
|
9178
10526
|
resourceRunId: this.#runResourceLeaseContext.getStore()?.resourceRunId,
|
|
@@ -9457,15 +10805,21 @@ export class AgentSession {
|
|
|
9457
10805
|
async #queueSteer(
|
|
9458
10806
|
text: string,
|
|
9459
10807
|
images?: ImageContent[],
|
|
9460
|
-
options?: { claimsGenuineUserIntent?: boolean },
|
|
10808
|
+
options?: { claimsGenuineUserIntent?: boolean; onPromoted?: () => void; external?: boolean },
|
|
9461
10809
|
): Promise<void> {
|
|
9462
10810
|
this.#assertNoHandoffTransition();
|
|
9463
10811
|
assertImagePlaceholdersHavePayload(text, images);
|
|
9464
10812
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
9465
|
-
this.#
|
|
10813
|
+
const displayEntry = this.#createQueuedDisplayEntry(displayText);
|
|
10814
|
+
this.#steeringMessages.push(displayEntry);
|
|
9466
10815
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
9467
10816
|
if (images && images.length > 0) content.push(...images);
|
|
9468
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
|
+
}
|
|
9469
10823
|
if (options?.claimsGenuineUserIntent) {
|
|
9470
10824
|
const epoch = this.#claimDeepInterviewUserIntent();
|
|
9471
10825
|
this.#deepInterviewGenuineUserMessageEpochs.set(message, epoch);
|
|
@@ -9482,6 +10836,7 @@ export class AgentSession {
|
|
|
9482
10836
|
if (!this.#cancelAndSubmitInProgress && this.#canAutoContinueForSteer()) {
|
|
9483
10837
|
this.#scheduleAgentContinue({
|
|
9484
10838
|
shouldContinue: () => this.#canAutoContinueForSteer() && this.agent.hasQueuedSteering(),
|
|
10839
|
+
rescheduleOnBusy: true,
|
|
9485
10840
|
continueQueuedOnly: true,
|
|
9486
10841
|
});
|
|
9487
10842
|
}
|
|
@@ -9493,29 +10848,95 @@ export class AgentSession {
|
|
|
9493
10848
|
async #queueFollowUp(
|
|
9494
10849
|
text: string,
|
|
9495
10850
|
images?: ImageContent[],
|
|
9496
|
-
options?: {
|
|
9497
|
-
|
|
10851
|
+
options?: {
|
|
10852
|
+
forceOneAtATime?: boolean;
|
|
10853
|
+
claimsGenuineUserIntent?: boolean;
|
|
10854
|
+
onPromoted?: () => void;
|
|
10855
|
+
sdkRunToken?: string;
|
|
10856
|
+
},
|
|
10857
|
+
): Promise<QueuedFollowUpOwner> {
|
|
9498
10858
|
this.#assertNoHandoffTransition();
|
|
9499
10859
|
assertImagePlaceholdersHavePayload(text, images);
|
|
9500
10860
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
9501
|
-
this
|
|
10861
|
+
const queueWasEmpty = !this.agent.hasQueuedMessages();
|
|
10862
|
+
const displayEntry = this.#createQueuedDisplayEntry(displayText);
|
|
10863
|
+
this.#followUpMessages.push(displayEntry);
|
|
9502
10864
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
9503
10865
|
if (images && images.length > 0) content.push(...images);
|
|
9504
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);
|
|
9505
10869
|
if (options?.claimsGenuineUserIntent) {
|
|
9506
10870
|
const epoch = this.#claimDeepInterviewUserIntent();
|
|
9507
10871
|
this.#deepInterviewGenuineUserMessageEpochs.set(message, epoch);
|
|
9508
10872
|
}
|
|
9509
|
-
this.
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
//
|
|
9516
|
-
//
|
|
9517
|
-
//
|
|
9518
|
-
|
|
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";
|
|
9519
10940
|
}
|
|
9520
10941
|
|
|
9521
10942
|
/**
|
|
@@ -9531,10 +10952,17 @@ export class AgentSession {
|
|
|
9531
10952
|
const last = messages[messages.length - 1];
|
|
9532
10953
|
return last?.role === "assistant" || last?.role === "bashExecution" || last?.role === "pythonExecution";
|
|
9533
10954
|
}
|
|
9534
|
-
#scheduleQueuedFollowUpContinuation(): void {
|
|
9535
|
-
if (
|
|
10955
|
+
#scheduleQueuedFollowUpContinuation(ownsQueuedMessage: (() => boolean) | undefined = undefined): void {
|
|
10956
|
+
if (
|
|
10957
|
+
!this.#cancelAndSubmitInProgress &&
|
|
10958
|
+
this.#canAutoContinueForFollowUp() &&
|
|
10959
|
+
this.agent.hasQueuedMessages() &&
|
|
10960
|
+
(ownsQueuedMessage?.() ?? true)
|
|
10961
|
+
) {
|
|
9536
10962
|
this.#scheduleAgentContinue({
|
|
9537
|
-
shouldContinue: () =>
|
|
10963
|
+
shouldContinue: () =>
|
|
10964
|
+
this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages() && (ownsQueuedMessage?.() ?? true),
|
|
10965
|
+
rescheduleOnBusy: true,
|
|
9538
10966
|
continueQueuedOnly: true,
|
|
9539
10967
|
});
|
|
9540
10968
|
}
|
|
@@ -9556,16 +10984,16 @@ export class AgentSession {
|
|
|
9556
10984
|
}
|
|
9557
10985
|
|
|
9558
10986
|
queueDeferredMessage(message: CustomMessage): void {
|
|
9559
|
-
this.#queueHiddenNextTurnMessage(message, true);
|
|
10987
|
+
this.#queueHiddenNextTurnMessage(message, true, "turn");
|
|
9560
10988
|
}
|
|
9561
10989
|
|
|
9562
10990
|
queueDeferredMessageForTests(message: CustomMessage, triggerTurn = true): void {
|
|
9563
|
-
this.#queueHiddenNextTurnMessage(message, triggerTurn);
|
|
10991
|
+
this.#queueHiddenNextTurnMessage(message, triggerTurn, "turn");
|
|
9564
10992
|
}
|
|
9565
10993
|
|
|
9566
10994
|
/** Read-only test seam for the hidden next-turn context queue. */
|
|
9567
10995
|
getPendingNextTurnMessagesForTests(): readonly CustomMessage[] {
|
|
9568
|
-
return this.#pendingNextTurnMessages.
|
|
10996
|
+
return this.#pendingNextTurnMessages.map(entry => entry.message);
|
|
9569
10997
|
}
|
|
9570
10998
|
|
|
9571
10999
|
/** Test-only abort outcome override; undefined retains the production abort race. */
|
|
@@ -9573,12 +11001,16 @@ export class AgentSession {
|
|
|
9573
11001
|
this.#cancelAndSubmitAbortOutcomeProviderForTests = provider;
|
|
9574
11002
|
}
|
|
9575
11003
|
|
|
9576
|
-
#queueHiddenNextTurnMessage(
|
|
11004
|
+
#queueHiddenNextTurnMessage(
|
|
11005
|
+
message: CustomMessage,
|
|
11006
|
+
triggerTurn: boolean,
|
|
11007
|
+
origin: "turn" | "external" = "external",
|
|
11008
|
+
): void {
|
|
9577
11009
|
// A hidden next-turn message queued during a handoff transition would be
|
|
9578
11010
|
// dropped when the successor clears predecessor queues; reject it as busy so
|
|
9579
11011
|
// the caller can retry against the settled session.
|
|
9580
11012
|
this.#assertNoHandoffTransition();
|
|
9581
|
-
this.#pendingNextTurnMessages.push(message);
|
|
11013
|
+
this.#pendingNextTurnMessages.push({ message, origin });
|
|
9582
11014
|
if (!triggerTurn) return;
|
|
9583
11015
|
const generation = this.#promptGeneration;
|
|
9584
11016
|
if (this.#scheduledHiddenNextTurnGeneration === generation) {
|
|
@@ -9593,6 +11025,27 @@ export class AgentSession {
|
|
|
9593
11025
|
if (this.#pendingNextTurnMessages.length === 0) {
|
|
9594
11026
|
return;
|
|
9595
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
|
+
}
|
|
9596
11049
|
try {
|
|
9597
11050
|
await this.#promptQueuedHiddenNextTurnMessages();
|
|
9598
11051
|
} catch {
|
|
@@ -9617,12 +11070,47 @@ export class AgentSession {
|
|
|
9617
11070
|
|
|
9618
11071
|
const queuedMessages = [...this.#pendingNextTurnMessages];
|
|
9619
11072
|
this.#pendingNextTurnMessages = [];
|
|
9620
|
-
|
|
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;
|
|
9621
11109
|
if (!message) {
|
|
9622
11110
|
return;
|
|
9623
11111
|
}
|
|
9624
11112
|
|
|
9625
|
-
const prependMessages =
|
|
11113
|
+
const prependMessages = reclassified.slice(0, -1).map(entry => entry.message);
|
|
9626
11114
|
const textContent = this.#getCustomMessageTextContent(message);
|
|
9627
11115
|
await this.#syncSkillPromptActiveStateSafely(message, true);
|
|
9628
11116
|
try {
|
|
@@ -9631,13 +11119,47 @@ export class AgentSession {
|
|
|
9631
11119
|
skipPostPromptRecoveryWait: true,
|
|
9632
11120
|
});
|
|
9633
11121
|
} catch (error) {
|
|
9634
|
-
|
|
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];
|
|
9635
11130
|
throw error;
|
|
9636
11131
|
} finally {
|
|
9637
11132
|
await this.#syncSkillPromptActiveStateSafely(message, false);
|
|
9638
11133
|
}
|
|
9639
11134
|
}
|
|
9640
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
|
+
|
|
9641
11163
|
#getCustomMessageTextContent(message: Pick<CustomMessage, "content">): string {
|
|
9642
11164
|
if (typeof message.content === "string") {
|
|
9643
11165
|
return message.content;
|
|
@@ -9679,6 +11201,13 @@ export class AgentSession {
|
|
|
9679
11201
|
triggerTurn?: boolean;
|
|
9680
11202
|
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
9681
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";
|
|
9682
11211
|
},
|
|
9683
11212
|
): Promise<void> {
|
|
9684
11213
|
this.#assertRecoveryHydrationPromoted();
|
|
@@ -9703,7 +11232,7 @@ export class AgentSession {
|
|
|
9703
11232
|
// while an (auto-)handoff is unwinding it.
|
|
9704
11233
|
this.#assertNoHandoffTransition();
|
|
9705
11234
|
if (options?.deliverAs === "nextTurn") {
|
|
9706
|
-
this.#queueHiddenNextTurnMessage(appMessage, options?.triggerTurn ?? false);
|
|
11235
|
+
this.#queueHiddenNextTurnMessage(appMessage, options?.triggerTurn ?? false, options?.origin ?? "turn");
|
|
9707
11236
|
return;
|
|
9708
11237
|
}
|
|
9709
11238
|
|
|
@@ -9721,9 +11250,13 @@ export class AgentSession {
|
|
|
9721
11250
|
if (options?.deliverAs === "nextTurn") {
|
|
9722
11251
|
if (options?.triggerTurn) {
|
|
9723
11252
|
if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
|
|
9724
|
-
this.#queueHiddenNextTurnMessage(appMessage, false);
|
|
11253
|
+
this.#queueHiddenNextTurnMessage(appMessage, false, options?.origin ?? "turn");
|
|
9725
11254
|
return;
|
|
9726
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();
|
|
9727
11260
|
await this.#syncSkillPromptActiveStateSafely(appMessage, true);
|
|
9728
11261
|
try {
|
|
9729
11262
|
await this.#promptWithMessage(appMessage, this.#getCustomMessageTextContent(appMessage), {
|
|
@@ -9731,6 +11264,11 @@ export class AgentSession {
|
|
|
9731
11264
|
});
|
|
9732
11265
|
} finally {
|
|
9733
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]);
|
|
9734
11272
|
}
|
|
9735
11273
|
return;
|
|
9736
11274
|
}
|
|
@@ -9752,6 +11290,27 @@ export class AgentSession {
|
|
|
9752
11290
|
this.#queueHiddenNextTurnMessage(appMessage, false);
|
|
9753
11291
|
return;
|
|
9754
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();
|
|
9755
11314
|
await this.#syncSkillPromptActiveStateSafely(appMessage, true);
|
|
9756
11315
|
try {
|
|
9757
11316
|
await this.#promptWithMessage(appMessage, this.#getCustomMessageTextContent(appMessage), {
|
|
@@ -9759,6 +11318,13 @@ export class AgentSession {
|
|
|
9759
11318
|
});
|
|
9760
11319
|
} finally {
|
|
9761
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]);
|
|
9762
11328
|
}
|
|
9763
11329
|
return;
|
|
9764
11330
|
}
|
|
@@ -9786,13 +11352,13 @@ export class AgentSession {
|
|
|
9786
11352
|
}
|
|
9787
11353
|
const agentRemoved = this.agent.removeQueuedMessages(isMatch);
|
|
9788
11354
|
const beforeNext = this.#pendingNextTurnMessages.length;
|
|
9789
|
-
for (const
|
|
9790
|
-
if (predicate(
|
|
9791
|
-
const tag = readPendingDisplayTag(
|
|
11355
|
+
for (const entry of this.#pendingNextTurnMessages) {
|
|
11356
|
+
if (predicate(entry.message)) {
|
|
11357
|
+
const tag = readPendingDisplayTag(entry.message.details);
|
|
9792
11358
|
if (tag) removedTags.add(tag);
|
|
9793
11359
|
}
|
|
9794
11360
|
}
|
|
9795
|
-
this.#pendingNextTurnMessages = this.#pendingNextTurnMessages.filter(
|
|
11361
|
+
this.#pendingNextTurnMessages = this.#pendingNextTurnMessages.filter(entry => !predicate(entry.message));
|
|
9796
11362
|
const pendingNextTurn = beforeNext - this.#pendingNextTurnMessages.length;
|
|
9797
11363
|
let displaySteering = 0;
|
|
9798
11364
|
let displayFollowUp = 0;
|
|
@@ -9825,11 +11391,26 @@ export class AgentSession {
|
|
|
9825
11391
|
content: string | (TextContent | ImageContent)[],
|
|
9826
11392
|
options?: {
|
|
9827
11393
|
deliverAs?: "steer" | "followUp";
|
|
11394
|
+
/** Preserve a busy SDK dispatch as queued work across an admission fence. */
|
|
11395
|
+
queuedAtDispatch?: boolean;
|
|
9828
11396
|
onPreflightAccepted?: () => void;
|
|
9829
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;
|
|
9830
11402
|
},
|
|
9831
11403
|
): Promise<void> {
|
|
9832
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
|
+
}
|
|
9833
11414
|
// Normalize content to text string + optional images
|
|
9834
11415
|
let text: string;
|
|
9835
11416
|
let images: ImageContent[] | undefined;
|
|
@@ -9850,38 +11431,145 @@ export class AgentSession {
|
|
|
9850
11431
|
if (images.length === 0) images = undefined;
|
|
9851
11432
|
}
|
|
9852
11433
|
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
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
|
+
}
|
|
9865
11527
|
|
|
9866
|
-
|
|
9867
|
-
|
|
9868
|
-
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
|
|
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
|
+
}
|
|
9877
11544
|
|
|
9878
|
-
|
|
9879
|
-
|
|
9880
|
-
|
|
9881
|
-
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
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
|
+
}
|
|
9885
11573
|
}
|
|
9886
11574
|
|
|
9887
11575
|
/**
|
|
@@ -9893,6 +11581,7 @@ export class AgentSession {
|
|
|
9893
11581
|
const followUp = this.#followUpMessages.map(e => e.text);
|
|
9894
11582
|
this.#steeringMessages = [];
|
|
9895
11583
|
this.#followUpMessages = [];
|
|
11584
|
+
this.#deferredSdkFollowUps = [];
|
|
9896
11585
|
this.agent.clearAllQueues();
|
|
9897
11586
|
return { steering, followUp };
|
|
9898
11587
|
}
|
|
@@ -10259,6 +11948,7 @@ export class AgentSession {
|
|
|
10259
11948
|
this.#markRetryReplayUnsafe();
|
|
10260
11949
|
this.abortRetry();
|
|
10261
11950
|
this.#promptGeneration++;
|
|
11951
|
+
this.#promptPreflightCancellationGeneration++;
|
|
10262
11952
|
this.#promptPreflightAbortController.abort();
|
|
10263
11953
|
this.#promptPreflightAbortController = new AbortController();
|
|
10264
11954
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
@@ -10331,6 +12021,7 @@ export class AgentSession {
|
|
|
10331
12021
|
delayMs: 1,
|
|
10332
12022
|
generation: this.#promptGeneration,
|
|
10333
12023
|
shouldContinue: () => this.agent.hasQueuedSteering(),
|
|
12024
|
+
rescheduleOnBusy: true,
|
|
10334
12025
|
continueQueuedOnly: true,
|
|
10335
12026
|
});
|
|
10336
12027
|
}
|
|
@@ -10361,20 +12052,370 @@ export class AgentSession {
|
|
|
10361
12052
|
if (outcome.kind === "error") throw outcome.cause;
|
|
10362
12053
|
}
|
|
10363
12054
|
/**
|
|
10364
|
-
*
|
|
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.
|
|
10365
12153
|
*/
|
|
10366
|
-
|
|
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
|
+
}
|
|
10367
12312
|
const aborted = this.#runCancellationDomains.abort(handle);
|
|
10368
12313
|
if (!aborted.ok) {
|
|
10369
12314
|
if (aborted.reason === "quarantined") {
|
|
10370
|
-
return
|
|
12315
|
+
return {
|
|
12316
|
+
...(await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: 0 })),
|
|
12317
|
+
...(registeredScope ? { terminalScope: registeredScope } : {}),
|
|
12318
|
+
};
|
|
10371
12319
|
}
|
|
10372
|
-
return {
|
|
12320
|
+
return {
|
|
12321
|
+
status: "unfenced",
|
|
12322
|
+
reason: "unknown_run",
|
|
12323
|
+
pending: [],
|
|
12324
|
+
...(registeredScope ? { terminalScope: registeredScope } : {}),
|
|
12325
|
+
};
|
|
10373
12326
|
}
|
|
10374
12327
|
if (handle === this.agent.activeResourceRunId) this.agent.abort();
|
|
10375
12328
|
const proof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: options.graceMs });
|
|
10376
12329
|
if (proof.status === "unfenced") this.agent.resourceLedger.quarantine(handle);
|
|
10377
|
-
|
|
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
|
+
};
|
|
10378
12419
|
}
|
|
10379
12420
|
|
|
10380
12421
|
/** Atomically interrupt the active run and make text the next prompt. */
|
|
@@ -10522,6 +12563,7 @@ export class AgentSession {
|
|
|
10522
12563
|
resetRetryReplaySafety: true,
|
|
10523
12564
|
onRunAccepted: () => {
|
|
10524
12565
|
runAccepted = true;
|
|
12566
|
+
if (selected) this.#fireQueuedPromotionHooks([message]);
|
|
10525
12567
|
if (selected) {
|
|
10526
12568
|
this.#steeringMessages = this.#steeringMessages.filter(entry => entry !== selected.display);
|
|
10527
12569
|
this.#followUpMessages = this.#followUpMessages.filter(entry => entry !== selected.display);
|
|
@@ -10604,7 +12646,7 @@ export class AgentSession {
|
|
|
10604
12646
|
}
|
|
10605
12647
|
}
|
|
10606
12648
|
|
|
10607
|
-
const manager = AsyncJobManager.instance();
|
|
12649
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
10608
12650
|
const ownerId = this.#agentId;
|
|
10609
12651
|
const lease = manager && ownerId ? manager.beginOwnerSubagentShutdown(ownerId) : undefined;
|
|
10610
12652
|
if (manager && ownerId && !lease) {
|
|
@@ -10630,11 +12672,18 @@ export class AgentSession {
|
|
|
10630
12672
|
this.#rebindProviderSessionState(new Map());
|
|
10631
12673
|
this.agent.reset();
|
|
10632
12674
|
if (!options?.drop) await this.sessionManager.flush();
|
|
12675
|
+
const noLeasePreviousSessionIdentity = this.sessionManager.getSessionId();
|
|
12676
|
+
const noLeasePreviousSessionFile = this.sessionManager.getSessionFile();
|
|
10633
12677
|
const prepared = await this.sessionManager.prepareNewSession(options);
|
|
10634
12678
|
try {
|
|
10635
12679
|
// Last fallible gate while public getters still show the predecessor (#3138).
|
|
10636
12680
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
12681
|
+
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
10637
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);
|
|
10638
12687
|
await this.#runToolSessionTransitionCleanups();
|
|
10639
12688
|
} catch (error) {
|
|
10640
12689
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
@@ -10662,6 +12711,7 @@ export class AgentSession {
|
|
|
10662
12711
|
if (!manager) throw new Error("Owner subagent shutdown manager became unavailable.");
|
|
10663
12712
|
if (!ownerId) throw new Error("Owner subagent shutdown owner became unavailable.");
|
|
10664
12713
|
const previousSessionIdentity = this.sessionManager.getSessionId();
|
|
12714
|
+
const previousSessionIdentityFile = this.sessionManager.getSessionFile();
|
|
10665
12715
|
try {
|
|
10666
12716
|
try {
|
|
10667
12717
|
manager.runOwnerProducerCleanupsStrict({ ownerId });
|
|
@@ -10704,7 +12754,12 @@ export class AgentSession {
|
|
|
10704
12754
|
try {
|
|
10705
12755
|
// Last fallible gate while public getters still show the predecessor (#3138).
|
|
10706
12756
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
12757
|
+
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
10707
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);
|
|
10708
12763
|
await this.#runToolSessionTransitionCleanups();
|
|
10709
12764
|
} catch (error) {
|
|
10710
12765
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
@@ -10879,6 +12934,7 @@ export class AgentSession {
|
|
|
10879
12934
|
try {
|
|
10880
12935
|
const previousSessionFile = this.sessionFile;
|
|
10881
12936
|
const previousWorkflowGateSessionId = this.sessionId;
|
|
12937
|
+
const previousSessionIdentity = this.sessionManager.getSessionId();
|
|
10882
12938
|
|
|
10883
12939
|
// Emit session_before_switch event with reason "fork" (can be cancelled)
|
|
10884
12940
|
if (this.#extensionRunner?.hasHandlers("session_before_switch")) {
|
|
@@ -10915,6 +12971,7 @@ export class AgentSession {
|
|
|
10915
12971
|
getSessionId: () => forkedManager.getSessionId(),
|
|
10916
12972
|
});
|
|
10917
12973
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
12974
|
+
this.#assertJobManagerEndpointAdmission(forkedManager.getSessionId(), forkedManager.getSessionFile());
|
|
10918
12975
|
} catch (error) {
|
|
10919
12976
|
const forkedFile = forkedManager.getSessionFile();
|
|
10920
12977
|
const cleanupErrors: unknown[] = [];
|
|
@@ -10945,6 +13002,7 @@ export class AgentSession {
|
|
|
10945
13002
|
throw error;
|
|
10946
13003
|
}
|
|
10947
13004
|
this.sessionManager = forkedManager;
|
|
13005
|
+
this.#rekeyJobManagerForSessionIdentity(previousSessionIdentity, previousSessionFile);
|
|
10948
13006
|
try {
|
|
10949
13007
|
await previousManager.close();
|
|
10950
13008
|
} catch (error) {
|
|
@@ -10961,7 +13019,11 @@ export class AgentSession {
|
|
|
10961
13019
|
try {
|
|
10962
13020
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
10963
13021
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
13022
|
+
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
10964
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);
|
|
10965
13027
|
await this.#runToolSessionTransitionCleanups();
|
|
10966
13028
|
} catch (error) {
|
|
10967
13029
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
@@ -11062,6 +13124,27 @@ export class AgentSession {
|
|
|
11062
13124
|
return this.#activeModelProfile;
|
|
11063
13125
|
}
|
|
11064
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
|
+
|
|
11065
13148
|
/** Resolver intent only for assignments owned by the active profile. */
|
|
11066
13149
|
#persistedModelProfileAliasIntent(role: string): { aliasIntent: "preset-equivalent" } | undefined {
|
|
11067
13150
|
if (!this.#activeModelProfile) return undefined;
|
|
@@ -11209,11 +13292,10 @@ export class AgentSession {
|
|
|
11209
13292
|
onAfterActivation?: () => void;
|
|
11210
13293
|
},
|
|
11211
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();
|
|
11212
13298
|
const canonicalName = await this.#withSessionAdmission("selection", async () => {
|
|
11213
|
-
// A model-picker action must not alter an in-flight generation: wait
|
|
11214
|
-
// for the current run to settle inside the admission before applying
|
|
11215
|
-
// the profile, matching `setDefaultModelSelection`.
|
|
11216
|
-
await this.waitForIdle();
|
|
11217
13299
|
const profiles = this.#modelRegistry.getModelProfiles();
|
|
11218
13300
|
let canonical: string;
|
|
11219
13301
|
try {
|
|
@@ -11300,10 +13382,10 @@ export class AgentSession {
|
|
|
11300
13382
|
* profile activation and default-model selection.
|
|
11301
13383
|
*/
|
|
11302
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();
|
|
11303
13388
|
return this.#withSessionAdmission("selection", async () => {
|
|
11304
|
-
// A config mutation can change the active model-role assignment. Do not
|
|
11305
|
-
// alter an in-flight turn after its prompt admission lease has released.
|
|
11306
|
-
await this.waitForIdle();
|
|
11307
13389
|
return await body();
|
|
11308
13390
|
});
|
|
11309
13391
|
}
|
|
@@ -11655,95 +13737,148 @@ export class AgentSession {
|
|
|
11655
13737
|
onAfterMutation?: () => void;
|
|
11656
13738
|
},
|
|
11657
13739
|
): Promise<DefaultModelSelectionResult> {
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
11667
|
-
|
|
11668
|
-
|
|
11669
|
-
|
|
11670
|
-
|
|
11671
|
-
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
11675
|
-
|
|
11676
|
-
|
|
11677
|
-
|
|
11678
|
-
|
|
11679
|
-
|
|
11680
|
-
|
|
11681
|
-
|
|
11682
|
-
|
|
11683
|
-
|
|
11684
|
-
|
|
11685
|
-
|
|
11686
|
-
|
|
11687
|
-
|
|
11688
|
-
|
|
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 },
|
|
11689
13789
|
);
|
|
11690
|
-
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
|
|
11697
|
-
|
|
11698
|
-
await this.sessionManager.
|
|
11699
|
-
|
|
11700
|
-
|
|
11701
|
-
|
|
11702
|
-
|
|
11703
|
-
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
11707
|
-
|
|
11708
|
-
|
|
11709
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
);
|
|
11713
|
-
}
|
|
11714
|
-
const promotion = this.sessionManager.promoteDefaultModelSelection(stage);
|
|
11715
|
-
switch (promotion.kind) {
|
|
11716
|
-
case "promoted": {
|
|
11717
|
-
this.#publishDefaultModelSelection(model, effectiveLevel, preparedSystemPrompt);
|
|
11718
|
-
break;
|
|
11719
|
-
}
|
|
11720
|
-
case "not_promoted": {
|
|
11721
|
-
return this.#throwDefaultModelSelectionRecovery(
|
|
11722
|
-
promotion.error ?? new Error("Default model selection was superseded before session promotion"),
|
|
11723
|
-
stage,
|
|
11724
|
-
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 },
|
|
11725
13812
|
);
|
|
11726
|
-
|
|
11727
|
-
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
|
|
11733
|
-
|
|
11734
|
-
|
|
11735
|
-
|
|
11736
|
-
|
|
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
|
+
],
|
|
11737
13862
|
},
|
|
11738
|
-
|
|
11739
|
-
}
|
|
11740
|
-
}
|
|
11741
|
-
|
|
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;
|
|
11742
13879
|
}
|
|
11743
|
-
this.#
|
|
11744
|
-
|
|
11745
|
-
return { provider: model.provider, modelId: model.id, thinkingLevel: effectiveLevel };
|
|
11746
|
-
});
|
|
13880
|
+
this.#resolveSessionSettlement();
|
|
13881
|
+
}
|
|
11747
13882
|
}
|
|
11748
13883
|
/**
|
|
11749
13884
|
* Cycle to next/previous model.
|
|
@@ -13185,9 +15320,13 @@ export class AgentSession {
|
|
|
13185
15320
|
// Last fallible action: verified local:// readiness from immutable staged options.
|
|
13186
15321
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
13187
15322
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
15323
|
+
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
13188
15324
|
|
|
13189
15325
|
// --- Commit boundary: synchronous adoption is the sole identity publication.
|
|
13190
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);
|
|
13191
15330
|
committed = true;
|
|
13192
15331
|
await this.#runToolSessionTransitionCleanups();
|
|
13193
15332
|
this.agent.reset();
|
|
@@ -15402,7 +17541,9 @@ export class AgentSession {
|
|
|
15402
17541
|
classification === "transient" ||
|
|
15403
17542
|
classification === "unknown" ||
|
|
15404
17543
|
classification === "first_event_timeout" ||
|
|
15405
|
-
classification === "empty_response"
|
|
17544
|
+
classification === "empty_response" ||
|
|
17545
|
+
classification === "local_snapshot" ||
|
|
17546
|
+
classification === "local_buffer_overflow"
|
|
15406
17547
|
);
|
|
15407
17548
|
}
|
|
15408
17549
|
const trigger = classifyFallbackTrigger(transportFailure ?? { status: message.errorStatus });
|
|
@@ -15422,7 +17563,13 @@ export class AgentSession {
|
|
|
15422
17563
|
return true;
|
|
15423
17564
|
}
|
|
15424
17565
|
const classification = this.#classifyErrorForRetry(message);
|
|
15425
|
-
return
|
|
17566
|
+
return (
|
|
17567
|
+
classification === "transient" ||
|
|
17568
|
+
classification === "unknown" ||
|
|
17569
|
+
classification === "first_event_timeout" ||
|
|
17570
|
+
classification === "local_snapshot" ||
|
|
17571
|
+
classification === "local_buffer_overflow"
|
|
17572
|
+
);
|
|
15426
17573
|
}
|
|
15427
17574
|
|
|
15428
17575
|
#isTransientErrorMessage(errorMessage: string): boolean {
|
|
@@ -15512,10 +17659,20 @@ export class AgentSession {
|
|
|
15512
17659
|
#classifyErrorForRetry(message: AssistantMessage): RetryErrorClassification {
|
|
15513
17660
|
if (message.stopReason !== "error") return "none";
|
|
15514
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";
|
|
15515
17664
|
if (this.#isTypedFirstEventTimeout(message)) return "first_event_timeout";
|
|
15516
17665
|
if (this.#isTypedEmptyResponse(message)) return "empty_response";
|
|
15517
17666
|
if (!message.errorMessage) return "none";
|
|
15518
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
|
+
}
|
|
15519
17676
|
// Provider safety refusals (e.g. Anthropic stop_reason "refusal" /
|
|
15520
17677
|
// "sensitive") are deterministic for the submitted context: replaying
|
|
15521
17678
|
// the identical conversation re-triggers the identical refusal, so an
|
|
@@ -15719,7 +17876,6 @@ export class AgentSession {
|
|
|
15719
17876
|
}
|
|
15720
17877
|
|
|
15721
17878
|
async #handleManagedAttemptOutcome(outcome: ManagedAttemptOutcome): Promise<ManagedAttemptDecision> {
|
|
15722
|
-
if (outcome.type !== "escaped_arguments_discarded") this.#consecutiveEscapedArgumentRetries = 0;
|
|
15723
17879
|
if (outcome.type === "run_terminal") {
|
|
15724
17880
|
this.#defaultFallbackChain().resetAttemptBudget();
|
|
15725
17881
|
return { type: "terminal", terminal: { stopReason: outcome.reason } };
|
|
@@ -15727,24 +17883,12 @@ export class AgentSession {
|
|
|
15727
17883
|
if (outcome.type === "escaped_arguments_discarded") {
|
|
15728
17884
|
// An escaped-non-ASCII wire defect is a sampling accident, not provider
|
|
15729
17885
|
// evidence: never charge the attempt, advance the chain, or suppress the
|
|
15730
|
-
// selector. The loop already removed the defective turn from history
|
|
15731
|
-
// this decision just re-issues the
|
|
15732
|
-
//
|
|
15733
|
-
//
|
|
15734
|
-
//
|
|
15735
|
-
// MAX_ESCAPED_ARGUMENT_RETRIES consecutive escaped-argument discards the
|
|
15736
|
-
// run terminates with an error instead of ping-ponging forever against a
|
|
15737
|
-
// model that deterministically emits escaped arguments. The counter
|
|
15738
|
-
// resets on any other outcome.
|
|
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.
|
|
15739
17891
|
this.#defaultFallbackChain().discardStartedAttempt();
|
|
15740
|
-
this.#consecutiveEscapedArgumentRetries++;
|
|
15741
|
-
if (this.#consecutiveEscapedArgumentRetries > MAX_ESCAPED_ARGUMENT_RETRIES) {
|
|
15742
|
-
this.#consecutiveEscapedArgumentRetries = 0;
|
|
15743
|
-
return {
|
|
15744
|
-
type: "terminal",
|
|
15745
|
-
terminal: { stopReason: "error", messages: [outcome.message] },
|
|
15746
|
-
};
|
|
15747
|
-
}
|
|
15748
17892
|
return {
|
|
15749
17893
|
type: "retry",
|
|
15750
17894
|
continuation: async ownership => {
|
|
@@ -16039,12 +18183,50 @@ export class AgentSession {
|
|
|
16039
18183
|
this.settings.has("retry.maxRetries") ||
|
|
16040
18184
|
this.settings.has("retry.baseDelayMs") ||
|
|
16041
18185
|
this.settings.has("retry.maxDelayMs");
|
|
16042
|
-
// retry.enabled=false always surfaces immediately, matching the explicit
|
|
16043
|
-
// user opt-out.
|
|
16044
|
-
if (!managedFallback && !retrySettings.enabled) return false;
|
|
16045
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
|
+
}
|
|
16046
18220
|
const firstEventTimeout = classification === "first_event_timeout";
|
|
16047
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;
|
|
16048
18230
|
// Content-free message-only watchdog prose (wrapped canonical or bare
|
|
16049
18231
|
// per-provider variants) is admitted like the typed path: it is
|
|
16050
18232
|
// replay-safe, so a bare-default retry may re-issue the request even
|
|
@@ -16076,18 +18258,24 @@ export class AgentSession {
|
|
|
16076
18258
|
}
|
|
16077
18259
|
: false;
|
|
16078
18260
|
}
|
|
16079
|
-
const trigger
|
|
18261
|
+
const trigger:
|
|
18262
|
+
| { class: FallbackTriggerClass; retryAfterMs?: number; authDisposition?: AuthDisposition }
|
|
18263
|
+
| undefined = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
|
|
16080
18264
|
if (!trigger) {
|
|
16081
18265
|
return managedOutcome
|
|
16082
18266
|
? this.#managedFallbackExhaustionDecision(message, message.errorMessage || "Model fallback attempt failed")
|
|
16083
18267
|
: false;
|
|
16084
18268
|
}
|
|
18269
|
+
const attemptsUsed = managedFallback ? controller.attemptsUsed || 1 : this.#retryAttempt + 1;
|
|
18270
|
+
const providerRetryCeilingReached =
|
|
18271
|
+
providerRetryMaxAttempts !== undefined && attemptsUsed >= providerRetryMaxAttempts;
|
|
16085
18272
|
// Credential rotation: a content-free quota/rate-limit failure has no
|
|
16086
18273
|
// observable state to corrupt, so it is replay-safe regardless of
|
|
16087
18274
|
// extension lifecycle participation. Mark the failed credential and
|
|
16088
18275
|
// retry with the next stored credential of the same provider.
|
|
16089
18276
|
let credentialRotated =
|
|
16090
18277
|
!managedFallback &&
|
|
18278
|
+
!providerRetryCeilingReached &&
|
|
16091
18279
|
!assistantMessageHasVisibleOrToolContent(message) &&
|
|
16092
18280
|
(trigger.class === "quota" || trigger.class === "rate_limit") &&
|
|
16093
18281
|
(await this.#markFailedCredential(trigger));
|
|
@@ -16095,7 +18283,9 @@ export class AgentSession {
|
|
|
16095
18283
|
// output, no tool calls, no extension-observable streaming state was produced
|
|
16096
18284
|
// before the failure. This bypasses #hasCleanRetryReplaySafety because the
|
|
16097
18285
|
// content-free check is the replay-safety guarantee for credential rotation.
|
|
16098
|
-
|
|
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;
|
|
16099
18289
|
// Universal automatic replay-safety gate (#3791): once the failed attempt
|
|
16100
18290
|
// carries observable assistant text, thinking, or tool-call content,
|
|
16101
18291
|
// automatic session retry must not re-issue the request. This covers
|
|
@@ -16133,17 +18323,30 @@ export class AgentSession {
|
|
|
16133
18323
|
!managedFallback &&
|
|
16134
18324
|
classification === "transient" &&
|
|
16135
18325
|
!this.#isIdleStreamStallErrorMessage(message.errorMessage ?? "");
|
|
16136
|
-
|
|
18326
|
+
|
|
16137
18327
|
const failedSelector = managedFallback ? controller.currentSelector() : undefined;
|
|
16138
|
-
let outcome
|
|
16139
|
-
|
|
16140
|
-
|
|
16141
|
-
|
|
16142
|
-
: "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
|
+
}
|
|
16143
18341
|
// Credential rotation is unbounded: a fresh credential is a different
|
|
16144
18342
|
// retry dimension from transient-error backoff, so it overrides maxRetries
|
|
16145
18343
|
// exhaustion and forces an immediate same-model retry.
|
|
16146
|
-
if (
|
|
18344
|
+
if (
|
|
18345
|
+
managedFallback &&
|
|
18346
|
+
!providerRetryCeilingReached &&
|
|
18347
|
+
outcome === "advance" &&
|
|
18348
|
+
(trigger.class === "quota" || trigger.class === "rate_limit")
|
|
18349
|
+
) {
|
|
16147
18350
|
credentialRotated = await this.#markFailedCredential(trigger);
|
|
16148
18351
|
}
|
|
16149
18352
|
if (credentialRotated) {
|
|
@@ -16157,6 +18360,9 @@ export class AgentSession {
|
|
|
16157
18360
|
outcome = "retry";
|
|
16158
18361
|
}
|
|
16159
18362
|
}
|
|
18363
|
+
if (outcome === "advance") {
|
|
18364
|
+
this.#providerRetryMaxAttempts = undefined;
|
|
18365
|
+
}
|
|
16160
18366
|
if (outcome === "exhausted") {
|
|
16161
18367
|
if (managedFallback) {
|
|
16162
18368
|
const errorMessage = this.#fallbackExhaustionError(controller);
|
|
@@ -16165,9 +18371,13 @@ export class AgentSession {
|
|
|
16165
18371
|
controller.resetSticky();
|
|
16166
18372
|
return managedOutcome ? this.#managedFallbackExhaustionDecision(message, errorMessage) : false;
|
|
16167
18373
|
}
|
|
16168
|
-
if (classification === "first_event_timeout") {
|
|
18374
|
+
if (classification === "first_event_timeout" || providerRetryMaxAttempts !== undefined) {
|
|
16169
18375
|
const elapsedMs = Date.now() - (this.#firstEventTimeoutRetryStartedAt ?? Date.now());
|
|
16170
|
-
|
|
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"}`;
|
|
16171
18381
|
}
|
|
16172
18382
|
return false;
|
|
16173
18383
|
}
|
|
@@ -16190,7 +18400,9 @@ export class AgentSession {
|
|
|
16190
18400
|
}
|
|
16191
18401
|
|
|
16192
18402
|
const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
|
|
16193
|
-
if (managedFallback && !credentialRotated
|
|
18403
|
+
if (managedFallback && !credentialRotated && !providerRetryCeilingReached) {
|
|
18404
|
+
await this.#markFailedCredential(trigger);
|
|
18405
|
+
}
|
|
16194
18406
|
let advanced = outcome !== "advance";
|
|
16195
18407
|
let resolutionError: unknown;
|
|
16196
18408
|
if (outcome === "advance") {
|
|
@@ -16233,11 +18445,17 @@ export class AgentSession {
|
|
|
16233
18445
|
await this.#emitSessionEvent({
|
|
16234
18446
|
type: "auto_retry_start",
|
|
16235
18447
|
attempt: this.#retryAttempt,
|
|
16236
|
-
maxAttempts:
|
|
16237
|
-
|
|
16238
|
-
|
|
16239
|
-
|
|
16240
|
-
|
|
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,
|
|
16241
18459
|
delayMs,
|
|
16242
18460
|
errorMessage,
|
|
16243
18461
|
unbounded: managedFallback ? false : legacyUnbounded,
|
|
@@ -16440,6 +18658,7 @@ export class AgentSession {
|
|
|
16440
18658
|
}
|
|
16441
18659
|
if (!preflightAccepted) {
|
|
16442
18660
|
await seam?.onPreflightAccepted?.();
|
|
18661
|
+
if (seam?.signal?.aborted) throw promptPreflightCancelledError();
|
|
16443
18662
|
preflightAccepted = true;
|
|
16444
18663
|
}
|
|
16445
18664
|
await this.agent.prompt(messages, options);
|
|
@@ -16545,6 +18764,7 @@ export class AgentSession {
|
|
|
16545
18764
|
if (lastMsg.role !== "assistant") {
|
|
16546
18765
|
if (!this.#isInterruptedRetryTail(lastMsg)) return false;
|
|
16547
18766
|
this.#retryAttempt = 0;
|
|
18767
|
+
this.#providerRetryMaxAttempts = undefined;
|
|
16548
18768
|
this.#scheduleAgentContinue({ delayMs: 1 });
|
|
16549
18769
|
return true;
|
|
16550
18770
|
}
|
|
@@ -16563,6 +18783,7 @@ export class AgentSession {
|
|
|
16563
18783
|
|
|
16564
18784
|
// Reset retry budget for a fresh attempt
|
|
16565
18785
|
this.#retryAttempt = 0;
|
|
18786
|
+
this.#providerRetryMaxAttempts = undefined;
|
|
16566
18787
|
|
|
16567
18788
|
// Re-attempt the turn
|
|
16568
18789
|
this.#scheduleAgentContinue({ delayMs: 1 });
|
|
@@ -17657,7 +19878,7 @@ export class AgentSession {
|
|
|
17657
19878
|
}
|
|
17658
19879
|
options?.onTransitionMutationStarted?.();
|
|
17659
19880
|
|
|
17660
|
-
const asyncManager = AsyncJobManager.instance();
|
|
19881
|
+
const asyncManager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
17661
19882
|
const ownerId = this.#agentId;
|
|
17662
19883
|
const lease =
|
|
17663
19884
|
switchingToDifferentSession && asyncManager && ownerId
|
|
@@ -17719,6 +19940,13 @@ export class AgentSession {
|
|
|
17719
19940
|
await this.sessionManager.setSessionFile(sessionPath, {
|
|
17720
19941
|
deferEphemeralArtifactRetirement: switchingToDifferentSession,
|
|
17721
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
|
+
});
|
|
17722
19950
|
if (switchingToDifferentSession) this.sessionManager.stageAdoptedArtifactManagerForTransition();
|
|
17723
19951
|
// The successor identity is already rotated in the manager but not yet
|
|
17724
19952
|
// published; gate its local:// root before publication so the agent,
|
|
@@ -17865,6 +20093,10 @@ export class AgentSession {
|
|
|
17865
20093
|
}
|
|
17866
20094
|
|
|
17867
20095
|
if (switchingToDifferentSession) {
|
|
20096
|
+
const predecessorEndpointId = this.#asyncJobEndpointId(
|
|
20097
|
+
previousSessionState.sessionId,
|
|
20098
|
+
previousSessionState.sessionFile,
|
|
20099
|
+
);
|
|
17868
20100
|
let ownerShutdownSettled = true;
|
|
17869
20101
|
if (ownerShutdownManager && ownerShutdownLease && ownerId) {
|
|
17870
20102
|
try {
|
|
@@ -17879,7 +20111,12 @@ export class AgentSession {
|
|
|
17879
20111
|
} catch (error) {
|
|
17880
20112
|
ownerShutdownSettled = false;
|
|
17881
20113
|
ownerShutdownFinalizationDeferred = true;
|
|
17882
|
-
this.#scheduleDeferredOwnerShutdownFinalization(
|
|
20114
|
+
this.#scheduleDeferredOwnerShutdownFinalization(
|
|
20115
|
+
ownerShutdownManager,
|
|
20116
|
+
ownerShutdownLease,
|
|
20117
|
+
ownerId,
|
|
20118
|
+
predecessorEndpointId,
|
|
20119
|
+
);
|
|
17883
20120
|
this.#suppressOwnAsyncJobDeliveries();
|
|
17884
20121
|
this.emitNotice(
|
|
17885
20122
|
"error",
|
|
@@ -17892,6 +20129,10 @@ export class AgentSession {
|
|
|
17892
20129
|
// Different files may intentionally carry the same copied session id; pathname transition is the commit signal.
|
|
17893
20130
|
ownerShutdownTransitionCommitted = true;
|
|
17894
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);
|
|
17895
20136
|
this.sessionManager.retireEphemeralArtifactsAfterTransition();
|
|
17896
20137
|
await this.#runToolSessionTransitionCleanups();
|
|
17897
20138
|
}
|
|
@@ -17917,6 +20158,35 @@ export class AgentSession {
|
|
|
17917
20158
|
return true;
|
|
17918
20159
|
} catch (error) {
|
|
17919
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
|
+
}
|
|
17920
20190
|
await this.sessionManager.restoreRollbackState(previousSessionState);
|
|
17921
20191
|
this.#defaultFallbackController = undefined;
|
|
17922
20192
|
this.#syncAgentSessionId(previousSessionState.sessionId);
|
|
@@ -18008,6 +20278,7 @@ export class AgentSession {
|
|
|
18008
20278
|
try {
|
|
18009
20279
|
const previousSessionFile = this.sessionFile;
|
|
18010
20280
|
const previousWorkflowGateSessionId = this.sessionId;
|
|
20281
|
+
const previousSessionIdentity = this.sessionManager.getSessionId();
|
|
18011
20282
|
const selectedEntry = this.sessionManager.getEntryForFidelity(entryId);
|
|
18012
20283
|
|
|
18013
20284
|
if (selectedEntry?.type !== "message" || selectedEntry.message.role !== "user") {
|
|
@@ -18039,7 +20310,11 @@ export class AgentSession {
|
|
|
18039
20310
|
try {
|
|
18040
20311
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
18041
20312
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
20313
|
+
this.#assertJobManagerEndpointAdmission(prepared.sessionId, prepared.sessionFile);
|
|
18042
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);
|
|
18043
20318
|
await this.#runToolSessionTransitionCleanups();
|
|
18044
20319
|
} catch (error) {
|
|
18045
20320
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|