@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { execFile } from "node:child_process";
|
|
2
3
|
import * as crypto from "node:crypto";
|
|
3
4
|
import * as fs from "node:fs/promises";
|
|
@@ -6,16 +7,23 @@ import { promisify } from "node:util";
|
|
|
6
7
|
import { ThinkingLevel } from "@gajae-code/agent-core";
|
|
7
8
|
import type { Api, Model } from "@gajae-code/ai/core";
|
|
8
9
|
import { logger } from "@gajae-code/utils";
|
|
10
|
+
import { AsyncJobManager } from "../../async";
|
|
9
11
|
import { isModelProfileProviderAvailable, projectModelProfileCatalog } from "../../config/model-profile-contract";
|
|
10
12
|
import { type ModelProfileDefinition, resolveProfileBindings } from "../../config/model-profiles";
|
|
11
13
|
import { resolveModelChainWithAuth, splitSelectorThinkingSuffix } from "../../config/model-resolver";
|
|
12
14
|
import { type ModelSelectorValue, normalizeModelSelectorValue } from "../../config/model-selector-value";
|
|
13
15
|
import { type Settings, validateSettingPatch } from "../../config/settings";
|
|
14
16
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
|
|
17
|
+
import {
|
|
18
|
+
boundTerminalRetentionState,
|
|
19
|
+
findOwnedRegistrationsForTurn,
|
|
20
|
+
isOwnedAttemptRegistrationIncomplete,
|
|
21
|
+
MAX_DURABLE_TERMINAL_RESERVATIONS,
|
|
22
|
+
settleOwnedWork,
|
|
23
|
+
} from "../../session/terminal-abort";
|
|
15
24
|
import { parseThinkingLevel } from "../../thinking";
|
|
16
25
|
import { ensureBroker } from "../broker/ensure";
|
|
17
26
|
import { SessionIndex } from "../broker/session-index";
|
|
18
|
-
import { elevationAuthorityPath, verifyElevationCapability } from "../elevation/capability";
|
|
19
27
|
import {
|
|
20
28
|
collectAuthenticatedProfileProviders,
|
|
21
29
|
parseSyntheticModelId,
|
|
@@ -25,11 +33,19 @@ import {
|
|
|
25
33
|
syntheticNamespaceCollision,
|
|
26
34
|
} from "../model-profile-model";
|
|
27
35
|
import { projectQ10Models } from "../models.js";
|
|
36
|
+
import { PromptDeadlineManager } from "../prompt-deadline-manager";
|
|
28
37
|
import { formatPromptFailureForLocalLog, sanitizePromptFailure } from "../prompt-failure";
|
|
29
38
|
import { OPERATIONS } from "../protocol/operation-registry";
|
|
39
|
+
import {
|
|
40
|
+
createKindAwareReconciliation,
|
|
41
|
+
createReconciliationStore,
|
|
42
|
+
type KindAwareReconciliation,
|
|
43
|
+
resolveReconciliationSessionFile,
|
|
44
|
+
} from "../reconciliation-extensions";
|
|
30
45
|
import { type ControlSurface, controlRequestFromFrame, dispatchControl } from "./control";
|
|
46
|
+
import { BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD } from "./control/runtime-gate";
|
|
31
47
|
import { SessionSdkHost, type SessionSdkHostOptions } from "./host";
|
|
32
|
-
import { CursorRegistry, QueryHandlers, RevisionStore, type
|
|
48
|
+
import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "./query";
|
|
33
49
|
import {
|
|
34
50
|
createSdkCapabilities,
|
|
35
51
|
createSdkSurfacePolicyForContext,
|
|
@@ -41,6 +57,35 @@ import {
|
|
|
41
57
|
import type { BrokerIndexWriter, SdkFrame } from "./types";
|
|
42
58
|
|
|
43
59
|
const execFileAsync = promisify(execFile);
|
|
60
|
+
const sdkControlRequesterContext = new AsyncLocalStorage<string>();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Thrown from a serialized durable terminal-scope transaction when the
|
|
64
|
+
* idempotency key is already owned by a DIFFERENT input (scope). After the
|
|
65
|
+
* dispatch cache evicts an in-flight entry, two concurrent requests can both
|
|
66
|
+
* pass the earlier snapshot check; the atomic recheck inside the transaction
|
|
67
|
+
* must reject the second instead of appending a duplicate-key row (review
|
|
68
|
+
* thread P2).
|
|
69
|
+
*/
|
|
70
|
+
class SdkOnlyIdempotencyConflictError extends Error {
|
|
71
|
+
constructor() {
|
|
72
|
+
super("Idempotency key was reused with different input.");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Bounded wait for the correlated agent_end lifecycle publication after a
|
|
77
|
+
* terminal abort settles, before the durable row may claim
|
|
78
|
+
* `terminalPublished` (review thread P2). The bus runtime needs no such wait:
|
|
79
|
+
* it publishes the correlated event inline during terminalization and records
|
|
80
|
+
* the outcome synchronously on its capture slot, while this runtime observes
|
|
81
|
+
* the publication from the separate `agent_end` handler. */
|
|
82
|
+
const SDK_ONLY_TERMINAL_PUBLICATION_WAIT_MS = 1_000;
|
|
83
|
+
/** Bounded wait for in-flight workflow gate resolutions to settle during SDK
|
|
84
|
+
* runtime shutdown before proceeding with cleanup. Unresolved resolutions
|
|
85
|
+
* after this bound are abandoned — their durable broker state is the recovery
|
|
86
|
+
* authority, and outcomes are inherently uncertain. */
|
|
87
|
+
const GATE_RESOLUTION_QUIESCENCE_MS = 5_000;
|
|
88
|
+
|
|
44
89
|
class DiffQueryError extends Error {
|
|
45
90
|
constructor(
|
|
46
91
|
readonly code: "not_git_repository" | "diff_too_large",
|
|
@@ -55,7 +100,10 @@ export interface SessionSdkTransport {
|
|
|
55
100
|
readonly sessionId: string;
|
|
56
101
|
readonly stateRoot: string;
|
|
57
102
|
readonly token: string;
|
|
58
|
-
sendFrame(
|
|
103
|
+
sendFrame(
|
|
104
|
+
connectionId: string,
|
|
105
|
+
frame: SdkFrame,
|
|
106
|
+
): void | "written" | "dropped" | Promise<void> | Promise<"written" | "dropped">;
|
|
59
107
|
onFrame(handler: (connectionId: string, frame: SdkFrame) => void): undefined | (() => void);
|
|
60
108
|
onMalformedFrame?(handler: (connectionId: string, message: string) => void): undefined | (() => void);
|
|
61
109
|
start(): Promise<{ url: string }>;
|
|
@@ -76,6 +124,110 @@ export interface SessionSdkRuntimeOptions
|
|
|
76
124
|
configOverrides?: Map<string, unknown>;
|
|
77
125
|
}
|
|
78
126
|
|
|
127
|
+
export interface SdkOnlyInvocationRecord extends InvocationCorrelation {
|
|
128
|
+
kind: InvocationKind | "terminal" | "steer";
|
|
129
|
+
clientRef?: string;
|
|
130
|
+
status: InvocationStatus | "dispatching" | "rejected";
|
|
131
|
+
acceptedAt: number;
|
|
132
|
+
startedAt?: number;
|
|
133
|
+
terminalAt?: number;
|
|
134
|
+
error?: { code: string; message: string };
|
|
135
|
+
outcome?: unknown;
|
|
136
|
+
pendingOutcome?: unknown;
|
|
137
|
+
skillName?: string;
|
|
138
|
+
/** Steer records (origin/dev) carry their own dispatching lifecycle. */
|
|
139
|
+
textDigest?: string;
|
|
140
|
+
createdAt?: number;
|
|
141
|
+
settledAt?: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface SdkOnlyTerminalScopeRecord {
|
|
145
|
+
selection: "turn" | "owned";
|
|
146
|
+
idempotencyKeyHash?: string;
|
|
147
|
+
idempotencyInputHash?: string;
|
|
148
|
+
turnDisposition:
|
|
149
|
+
| "pending"
|
|
150
|
+
| "stopped"
|
|
151
|
+
| "uncertain"
|
|
152
|
+
| "no_effect"
|
|
153
|
+
| "no_effect_reserved"
|
|
154
|
+
| "no_effect_marker_failure";
|
|
155
|
+
terminalPublished?: boolean;
|
|
156
|
+
ownedWorkDisposition: "not_requested" | "left_running" | "stopped" | "uncertain";
|
|
157
|
+
automaticDeliveryDisposition: "enabled" | "none";
|
|
158
|
+
resumeOnOwnedCompletion: boolean;
|
|
159
|
+
turnContinuationFence: {
|
|
160
|
+
state: "retained" | "released";
|
|
161
|
+
abortedAttemptEpoch: number;
|
|
162
|
+
blockedContinuationIds: string[];
|
|
163
|
+
predecessorTombstones: string[];
|
|
164
|
+
ownedCompletionPolicy: "enabled" | "disabled";
|
|
165
|
+
};
|
|
166
|
+
responseState: "pending" | "sent" | "delivered" | "failed";
|
|
167
|
+
responsePayloadHash: string;
|
|
168
|
+
replayPayloadHash?: string;
|
|
169
|
+
acceptedAt: number;
|
|
170
|
+
terminalAt?: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface SdkOnlyEvictedTerminalKeyEntry {
|
|
174
|
+
keyHash: string;
|
|
175
|
+
inputHash: string;
|
|
176
|
+
turnDisposition?: "stopped" | "uncertain" | "no_effect" | "no_effect_reserved" | "no_effect_marker_failure";
|
|
177
|
+
ownedWorkDisposition?: "not_requested" | "left_running" | "stopped" | "uncertain";
|
|
178
|
+
responseState?: "pending" | "sent" | "delivered" | "failed";
|
|
179
|
+
responsePayloadHash?: string;
|
|
180
|
+
replayPayloadHash?: string;
|
|
181
|
+
terminalPublished?: boolean;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface SdkOnlyReconciliationStore {
|
|
185
|
+
readonly path: string | null;
|
|
186
|
+
load(): Promise<unknown[]>;
|
|
187
|
+
transact(mutator: (records: SdkOnlyInvocationRecord[]) => SdkOnlyInvocationRecord[]): Promise<void>;
|
|
188
|
+
snapshotTerminalScopes(): SdkOnlyTerminalScopeRecord[];
|
|
189
|
+
snapshotTerminalKeys(): SdkOnlyEvictedTerminalKeyEntry[];
|
|
190
|
+
transactTerminalScopes(
|
|
191
|
+
mutator: (scopes: SdkOnlyTerminalScopeRecord[]) => SdkOnlyTerminalScopeRecord[],
|
|
192
|
+
): Promise<void>;
|
|
193
|
+
transactTerminalState(
|
|
194
|
+
mutator: (state: { scopes: SdkOnlyTerminalScopeRecord[]; keys: SdkOnlyEvictedTerminalKeyEntry[] }) => {
|
|
195
|
+
scopes: SdkOnlyTerminalScopeRecord[];
|
|
196
|
+
keys: SdkOnlyEvictedTerminalKeyEntry[];
|
|
197
|
+
},
|
|
198
|
+
): Promise<void>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface SdkOnlyTerminalAbortSeams {
|
|
202
|
+
getReconciliationStore?: () => SdkOnlyReconciliationStore | undefined;
|
|
203
|
+
getTerminalTurnEpoch: () => number | undefined;
|
|
204
|
+
getActivePromptHandle: () => string | undefined;
|
|
205
|
+
/** Re-read the active prompt's owning SDK connection for the owner-mismatch
|
|
206
|
+
* recheck; falls back to the runtime-tracked owner when absent (review
|
|
207
|
+
* thread P1). */
|
|
208
|
+
getActivePromptOwnerConnectionId?: () => string | undefined;
|
|
209
|
+
cancelPendingPreflightForTerminalAbort: () => void;
|
|
210
|
+
/** Capture the steering admission snapshot at abort ADMISSION (before any
|
|
211
|
+
* durable transaction), so steers admitted while the abort is in flight
|
|
212
|
+
* classify as post-snapshot (review thread P1). */
|
|
213
|
+
captureTerminalAbortSteeringSnapshot?: () => number | undefined;
|
|
214
|
+
/** Discard the steering snapshot when a replay-only abort never settles
|
|
215
|
+
* (review thread P1). */
|
|
216
|
+
discardTerminalAbortSteeringSnapshot?: (token: number) => void;
|
|
217
|
+
/** Rebind the snapshot to the current turn when the requester's turn
|
|
218
|
+
* wins the race (review thread P1). */
|
|
219
|
+
rebindTerminalAbortSteeringSnapshot?: (token: number) => void;
|
|
220
|
+
abortPromptAndWaitWithTerminal: (
|
|
221
|
+
handle: string,
|
|
222
|
+
options: {
|
|
223
|
+
graceMs: number;
|
|
224
|
+
terminal?: { scope: "turn" | "owned"; expectedEpoch?: number; steeringSnapshotToken?: number };
|
|
225
|
+
},
|
|
226
|
+
) => Promise<{ status: string; terminalScope?: unknown }>;
|
|
227
|
+
/** Test override for the maximum durable terminal reservation rows. */
|
|
228
|
+
maxDurableTerminalReservationsForTests?: number;
|
|
229
|
+
}
|
|
230
|
+
|
|
79
231
|
/**
|
|
80
232
|
* The transport-neutral SDK session runtime.
|
|
81
233
|
*
|
|
@@ -102,7 +254,11 @@ export class SessionSdkSessionRuntime {
|
|
|
102
254
|
sessionId: options.transport.sessionId,
|
|
103
255
|
stateRoot: options.transport.stateRoot,
|
|
104
256
|
token: options.transport.token,
|
|
105
|
-
sendFrame:
|
|
257
|
+
sendFrame: (connectionId, frame) => {
|
|
258
|
+
const result = options.transport.sendFrame(connectionId, frame);
|
|
259
|
+
if (result instanceof Promise) return result.then(outcome => outcome ?? "written");
|
|
260
|
+
return result ?? "written";
|
|
261
|
+
},
|
|
106
262
|
onFrame: options.transport.onFrame,
|
|
107
263
|
});
|
|
108
264
|
this.#connectionDisposer = options.transport.onConnectionClose?.(connectionId => {
|
|
@@ -234,6 +390,10 @@ export interface CreateSdkSessionRuntimeOptions {
|
|
|
234
390
|
settings?: Settings;
|
|
235
391
|
/** Mutable shadow of patched config values merged into query readback. */
|
|
236
392
|
configOverrides?: Map<string, unknown>;
|
|
393
|
+
/** Private session-owned terminal-abort capabilities; never exposed on ExtensionContext. */
|
|
394
|
+
terminalAbortSeams?: SdkOnlyTerminalAbortSeams;
|
|
395
|
+
/** Callback when a frame is admitted to the runtime (test harness). */
|
|
396
|
+
onFrameAdmitted?: () => void;
|
|
237
397
|
}
|
|
238
398
|
|
|
239
399
|
function unavailable(operation: string): () => never {
|
|
@@ -249,8 +409,8 @@ export interface InvocationCorrelation {
|
|
|
249
409
|
turnId: string;
|
|
250
410
|
}
|
|
251
411
|
|
|
252
|
-
export type InvocationKind = "prompt" | "skill";
|
|
253
|
-
type InvocationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed";
|
|
412
|
+
export type InvocationKind = "prompt" | "skill" | "steer";
|
|
413
|
+
type InvocationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed" | "uncertain";
|
|
254
414
|
interface InvocationRecord extends InvocationCorrelation {
|
|
255
415
|
kind: InvocationKind;
|
|
256
416
|
clientRef?: string;
|
|
@@ -261,6 +421,8 @@ interface InvocationRecord extends InvocationCorrelation {
|
|
|
261
421
|
error?: { code: string; message: string };
|
|
262
422
|
}
|
|
263
423
|
export interface InvocationReconciliation {
|
|
424
|
+
/** Shared v2 reconciliation owner; present for durable terminal admission. */
|
|
425
|
+
readonly store?: SdkOnlyReconciliationStore;
|
|
264
426
|
admit(kind: InvocationKind, clientRef?: string): void;
|
|
265
427
|
release(kind: InvocationKind, clientRef?: string): void;
|
|
266
428
|
noteAccepted(kind: InvocationKind, correlation: InvocationCorrelation, clientRef?: string): Promise<void>;
|
|
@@ -272,31 +434,49 @@ export interface InvocationReconciliation {
|
|
|
272
434
|
lookup(kind: InvocationKind, selector: { commandId?: string; turnId?: string; clientRef?: string }): unknown;
|
|
273
435
|
lookupResult(kind: InvocationKind, selector: { commandId?: string; turnId?: string; clientRef?: string }): unknown;
|
|
274
436
|
hydrate(): Promise<void>;
|
|
437
|
+
claimPendingOutcome(
|
|
438
|
+
kind: InvocationKind,
|
|
439
|
+
correlation: InvocationCorrelation,
|
|
440
|
+
outcome: { kind: string; code: string; message: string; provenance?: string },
|
|
441
|
+
): Promise<unknown>;
|
|
442
|
+
finalizeOutcome(
|
|
443
|
+
kind: InvocationKind,
|
|
444
|
+
correlation: InvocationCorrelation,
|
|
445
|
+
outcome?: { kind: string; code: string; message: string; provenance?: string },
|
|
446
|
+
): Promise<void>;
|
|
275
447
|
}
|
|
276
448
|
|
|
277
449
|
export function createInvocationReconciliation(
|
|
278
|
-
options: { stateRoot?: string; sessionId?: string } = {},
|
|
450
|
+
options: { stateRoot?: string; sessionId?: string; store?: SdkOnlyReconciliationStore } = {},
|
|
279
451
|
): InvocationReconciliation {
|
|
280
452
|
const ACTIVE_CAPACITY = 256;
|
|
281
453
|
const TERMINAL_CAPACITY = 512;
|
|
282
|
-
const TERMINAL_TTL_MS = 15 * 60_000;
|
|
283
454
|
const records = new Map<string, InvocationRecord>();
|
|
284
455
|
const reservations = new Map<string, InvocationKind>();
|
|
285
456
|
const reservationCounts = new Map<InvocationKind, number>([
|
|
286
457
|
["prompt", 0],
|
|
287
458
|
["skill", 0],
|
|
459
|
+
["steer", 0],
|
|
288
460
|
]);
|
|
289
461
|
const key = (kind: InvocationKind, correlation: InvocationCorrelation) =>
|
|
290
462
|
`${kind}:${correlation.commandId}:${correlation.turnId}`;
|
|
291
463
|
const ref = (kind: InvocationKind, clientRef: string) => `${kind}\\0${clientRef}`;
|
|
292
464
|
if (options.sessionId && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(options.sessionId))
|
|
293
465
|
throw Object.assign(new Error("Unsafe SDK reconciliation session id."), { code: "invalid_input" });
|
|
466
|
+
const store = options.store;
|
|
467
|
+
// Legacy fs-backed path retained for callers that pass stateRoot/sessionId
|
|
468
|
+
// without a store (origin/dev behavior); the store path is authoritative
|
|
469
|
+
// when present (shared v2 reconciliation owner).
|
|
294
470
|
const reconciliationFile =
|
|
295
|
-
options.stateRoot && options.sessionId
|
|
471
|
+
options.stateRoot && options.sessionId && !store
|
|
296
472
|
? path.join(options.stateRoot, ".sdk-reconciliation", `${options.sessionId}.json`)
|
|
297
473
|
: undefined;
|
|
298
474
|
let persistenceChain: Promise<void> = Promise.resolve();
|
|
299
475
|
const persist = async (): Promise<void> => {
|
|
476
|
+
if (store) {
|
|
477
|
+
await store.transact(() => [...records.values()]);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
300
480
|
if (!reconciliationFile) return;
|
|
301
481
|
const run = async (): Promise<void> => {
|
|
302
482
|
const directory = path.dirname(reconciliationFile);
|
|
@@ -317,11 +497,11 @@ export function createInvocationReconciliation(
|
|
|
317
497
|
);
|
|
318
498
|
await pending;
|
|
319
499
|
};
|
|
500
|
+
// Retention contract (#4547): terminal records are never age-evicted; only
|
|
501
|
+
// the per-kind oldest-terminal-first capacity trim removes them, so a
|
|
502
|
+
// fire-and-wake consumer can still query the canonical terminal outcome
|
|
503
|
+
// until capacity eviction honestly reports `unknown`.
|
|
320
504
|
const cleanup = (): void => {
|
|
321
|
-
const now = Date.now();
|
|
322
|
-
for (const [recordKey, record] of records) {
|
|
323
|
-
if (record.terminalAt !== undefined && record.terminalAt + TERMINAL_TTL_MS <= now) records.delete(recordKey);
|
|
324
|
-
}
|
|
325
505
|
for (const kind of ["prompt", "skill"] as const) {
|
|
326
506
|
const terminal = [...records.entries()]
|
|
327
507
|
.filter(([, record]) => record.kind === kind && record.terminalAt !== undefined)
|
|
@@ -341,6 +521,21 @@ export function createInvocationReconciliation(
|
|
|
341
521
|
return records.get(key(kind, { commandId: selector.commandId, turnId: selector.turnId }));
|
|
342
522
|
};
|
|
343
523
|
const hydrate = async (): Promise<void> => {
|
|
524
|
+
if (store) {
|
|
525
|
+
const loaded = (await store.load()) as SdkOnlyInvocationRecord[];
|
|
526
|
+
for (const candidate of loaded) {
|
|
527
|
+
if (candidate.kind !== "prompt" && candidate.kind !== "skill") continue;
|
|
528
|
+
if (!candidate.commandId || !candidate.turnId || typeof candidate.acceptedAt !== "number") continue;
|
|
529
|
+
const kind = candidate.kind as InvocationKind;
|
|
530
|
+
const record: InvocationRecord = { ...candidate, kind } as InvocationRecord;
|
|
531
|
+
// Never re-hydrate a failure reason that could contain provider secrets
|
|
532
|
+
// into a fresh process (origin/dev sanitization preserved).
|
|
533
|
+
if (record.status === "failed") record.error = sanitizePromptFailure(record.error);
|
|
534
|
+
records.set(key(kind, candidate), record);
|
|
535
|
+
}
|
|
536
|
+
cleanup();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
344
539
|
if (!reconciliationFile) return;
|
|
345
540
|
let raw: string;
|
|
346
541
|
try {
|
|
@@ -377,6 +572,7 @@ export function createInvocationReconciliation(
|
|
|
377
572
|
cleanup();
|
|
378
573
|
};
|
|
379
574
|
return {
|
|
575
|
+
store,
|
|
380
576
|
admit(kind, clientRef) {
|
|
381
577
|
cleanup();
|
|
382
578
|
const active = [...records.values()].filter(
|
|
@@ -479,6 +675,31 @@ export function createInvocationReconciliation(
|
|
|
479
675
|
return result.status === "unknown" ? result : { kind, ...result };
|
|
480
676
|
},
|
|
481
677
|
hydrate,
|
|
678
|
+
async claimPendingOutcome(kind, correlation, outcome) {
|
|
679
|
+
const record = records.get(key(kind, correlation));
|
|
680
|
+
if (!record || record.terminalAt !== undefined || record.kind !== kind) return outcome;
|
|
681
|
+
const pending = (record as unknown as { pendingOutcome?: unknown }).pendingOutcome;
|
|
682
|
+
if (pending !== undefined) return pending;
|
|
683
|
+
(record as unknown as Record<string, unknown>).pendingOutcome = outcome;
|
|
684
|
+
await persist();
|
|
685
|
+
return outcome;
|
|
686
|
+
},
|
|
687
|
+
async finalizeOutcome(kind, correlation, outcome) {
|
|
688
|
+
const record = records.get(key(kind, correlation));
|
|
689
|
+
if (!record || record.terminalAt !== undefined || record.kind !== kind) return;
|
|
690
|
+
const finalOutcome = (outcome ??
|
|
691
|
+
(record as unknown as { pendingOutcome?: { kind: string; code: string; message: string } })
|
|
692
|
+
.pendingOutcome) as { kind: string; code: string; message: string } | undefined;
|
|
693
|
+
record.terminalAt = Date.now();
|
|
694
|
+
if (finalOutcome?.kind === "failed") {
|
|
695
|
+
record.status = "failed";
|
|
696
|
+
record.error = { code: finalOutcome.code, message: finalOutcome.message };
|
|
697
|
+
} else {
|
|
698
|
+
record.status = "terminal_ok";
|
|
699
|
+
}
|
|
700
|
+
(record as unknown as Record<string, unknown>).pendingOutcome = undefined;
|
|
701
|
+
await persist();
|
|
702
|
+
},
|
|
482
703
|
};
|
|
483
704
|
}
|
|
484
705
|
|
|
@@ -498,9 +719,8 @@ export interface SdkSurfaceFactoryOptions {
|
|
|
498
719
|
turnId?: string;
|
|
499
720
|
clientRef?: string;
|
|
500
721
|
}) => unknown;
|
|
722
|
+
steerStatusLookup?: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
|
|
501
723
|
hostTools?: boolean | (() => boolean);
|
|
502
|
-
/** Q30 event-ring watermark source (the live session event stream); synchronous. */
|
|
503
|
-
getEventWatermark?: () => { generation: number; seq: number };
|
|
504
724
|
}
|
|
505
725
|
|
|
506
726
|
/** Shared policy, capability, and query-surface factory for every SDK transport. */
|
|
@@ -528,9 +748,8 @@ function createQuerySurface(
|
|
|
528
748
|
turnId?: string;
|
|
529
749
|
clientRef?: string;
|
|
530
750
|
}) => unknown;
|
|
751
|
+
steerStatusLookup?: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
|
|
531
752
|
hostTools?: boolean | (() => boolean);
|
|
532
|
-
/** Q30 event-ring watermark source (the live session event stream); synchronous. */
|
|
533
|
-
getEventWatermark?: () => { generation: number; seq: number };
|
|
534
753
|
} = {},
|
|
535
754
|
): SessionSurface {
|
|
536
755
|
const policy =
|
|
@@ -804,6 +1023,8 @@ function createQuerySurface(
|
|
|
804
1023
|
turnId?: string;
|
|
805
1024
|
clientRef?: string;
|
|
806
1025
|
}) => (options.turnResultLookup ?? (value => reconciliation.lookupResult(value.kind, value)))(selector),
|
|
1026
|
+
getSteerStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
1027
|
+
(options.steerStatusLookup ?? (value => reconciliation.lookup("steer", value)))(selector),
|
|
807
1028
|
getModelProfiles: async () => {
|
|
808
1029
|
const profiles = ctx.modelRegistry.getModelProfiles();
|
|
809
1030
|
const authenticatedProviders = await collectAuthenticatedProfileProviders(profiles, provider =>
|
|
@@ -820,23 +1041,6 @@ function createQuerySurface(
|
|
|
820
1041
|
})),
|
|
821
1042
|
)) as unknown[];
|
|
822
1043
|
},
|
|
823
|
-
getCheckpointSnapshot: (): { entries: unknown[]; watermark: SdkCheckpointRecord } => {
|
|
824
|
-
// Q30 atomic checkpoint capture (C9): the transcript entries and the
|
|
825
|
-
// event-ring watermark are read in one synchronous call from the host
|
|
826
|
-
// event stream, so the snapshot revision and the subscribe position
|
|
827
|
-
// can never straddle a concurrent append.
|
|
828
|
-
const entries =
|
|
829
|
-
typeof (ctx as Partial<ExtensionContext>).getTranscript === "function" ? ctx.getTranscript() : [];
|
|
830
|
-
const watermark = options.getEventWatermark?.() ?? { generation: 0, seq: 0 };
|
|
831
|
-
return {
|
|
832
|
-
entries,
|
|
833
|
-
watermark: {
|
|
834
|
-
revision: Array.isArray(entries) ? entries.length : 0,
|
|
835
|
-
generation: watermark.generation,
|
|
836
|
-
seq: watermark.seq,
|
|
837
|
-
},
|
|
838
|
-
};
|
|
839
|
-
},
|
|
840
1044
|
installedQueries: policy.installedQueries,
|
|
841
1045
|
};
|
|
842
1046
|
}
|
|
@@ -865,8 +1069,8 @@ export function createSdkSurfaceFactory(
|
|
|
865
1069
|
configOverrides: options.configOverrides,
|
|
866
1070
|
settings: options.settings,
|
|
867
1071
|
turnResultLookup: options.turnResultLookup,
|
|
1072
|
+
steerStatusLookup: options.steerStatusLookup,
|
|
868
1073
|
hostTools: options.hostTools,
|
|
869
|
-
getEventWatermark: options.getEventWatermark,
|
|
870
1074
|
});
|
|
871
1075
|
return {
|
|
872
1076
|
policy,
|
|
@@ -957,7 +1161,13 @@ async function resolveSdkWorkflowGate(
|
|
|
957
1161
|
const completed = workflowGate.lookupCompletedResolution(response);
|
|
958
1162
|
if (completed.kind === "completed") return completed.resolution;
|
|
959
1163
|
if (completed.kind === "accepted_incomplete") {
|
|
960
|
-
|
|
1164
|
+
try {
|
|
1165
|
+
await workflowGate.recoverAcceptedGates();
|
|
1166
|
+
} catch {
|
|
1167
|
+
throw Object.assign(new Error("Workflow gate resolution outcome is uncertain."), {
|
|
1168
|
+
code: "terminal_uncertain",
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
961
1171
|
const recovered = workflowGate.lookupCompletedResolution(response);
|
|
962
1172
|
if (recovered.kind === "completed") return recovered.resolution;
|
|
963
1173
|
throw Object.assign(new Error("Workflow gate resolution outcome is uncertain."), { code: "terminal_uncertain" });
|
|
@@ -969,6 +1179,22 @@ async function resolveSdkWorkflowGate(
|
|
|
969
1179
|
if ((resolution as { status?: unknown }).status === "rejected") workflowGate.clearPreparedTerminalization(id);
|
|
970
1180
|
return resolution;
|
|
971
1181
|
} catch (error) {
|
|
1182
|
+
const completedAfterFailure = workflowGate.lookupCompletedResolution(response);
|
|
1183
|
+
if (completedAfterFailure.kind === "completed") return completedAfterFailure.resolution;
|
|
1184
|
+
if (completedAfterFailure.kind === "accepted_incomplete") {
|
|
1185
|
+
try {
|
|
1186
|
+
await workflowGate.recoverAcceptedGates();
|
|
1187
|
+
} catch {
|
|
1188
|
+
throw Object.assign(new Error("Workflow gate resolution outcome is uncertain."), {
|
|
1189
|
+
code: "terminal_uncertain",
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
const recovered = workflowGate.lookupCompletedResolution(response);
|
|
1193
|
+
if (recovered.kind === "completed") return recovered.resolution;
|
|
1194
|
+
throw Object.assign(new Error("Workflow gate resolution outcome is uncertain."), {
|
|
1195
|
+
code: "terminal_uncertain",
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
972
1198
|
const stillPending = workflowGate.listPendingGates?.().some(gate => gate.gate_id === id) === true;
|
|
973
1199
|
if (stillPending) workflowGate.clearPreparedTerminalization(id);
|
|
974
1200
|
else workflowGate.quarantineGate?.(id);
|
|
@@ -980,12 +1206,26 @@ function createControlSurface(
|
|
|
980
1206
|
ctx: ExtensionContext,
|
|
981
1207
|
api: ExtensionAPI,
|
|
982
1208
|
reconciliation: InvocationReconciliation,
|
|
983
|
-
onAccepted: (
|
|
1209
|
+
onAccepted: (
|
|
1210
|
+
kind: InvocationKind,
|
|
1211
|
+
correlation: InvocationCorrelation,
|
|
1212
|
+
connectionId: string | undefined,
|
|
1213
|
+
startsOwnTurn: boolean,
|
|
1214
|
+
) => void,
|
|
1215
|
+
steerReconciliation: KindAwareReconciliation,
|
|
1216
|
+
onPromotedTurn?: (
|
|
1217
|
+
kind: InvocationKind,
|
|
1218
|
+
correlation: InvocationCorrelation,
|
|
1219
|
+
connectionId: string | undefined,
|
|
1220
|
+
) => void,
|
|
984
1221
|
policy?: SdkSurfacePolicy,
|
|
985
1222
|
settings?: Settings,
|
|
986
1223
|
configOverrides?: Map<string, unknown>,
|
|
987
1224
|
configRevision: { current: number } = { current: 0 },
|
|
988
|
-
|
|
1225
|
+
terminalAbortSeams?: SdkOnlyTerminalAbortSeams,
|
|
1226
|
+
terminalPublicationCapture?: { resolvers?: Array<(observed: boolean) => void> },
|
|
1227
|
+
activePromptOwnerHolder?: { connectionIds?: ReadonlySet<string> },
|
|
1228
|
+
retirePendingOwner?: (correlation: InvocationCorrelation) => void,
|
|
989
1229
|
canResolveGate: () => boolean = () => true,
|
|
990
1230
|
trackGateResolution: <T>(resolution: Promise<T>) => Promise<T> = async resolution => await resolution,
|
|
991
1231
|
): ControlSurface {
|
|
@@ -1042,6 +1282,24 @@ function createControlSurface(
|
|
|
1042
1282
|
: { changed: true };
|
|
1043
1283
|
};
|
|
1044
1284
|
const newCorrelation = () => ({ commandId: crypto.randomUUID(), turnId: crypto.randomUUID() });
|
|
1285
|
+
const pendingPreflights = new Map<string, Set<() => void>>();
|
|
1286
|
+
// The SDK connection that accepted the currently active prompt/skill, if
|
|
1287
|
+
// any: terminal aborts are requester-scoped, so another connection must
|
|
1288
|
+
// never stop it, and an agent-initiated turn (monitor/cron follow-up) has
|
|
1289
|
+
// no owner — every client is refused (review thread P1). Shared with the
|
|
1290
|
+
// runtime extension so agent_end clears it: a stale owner must not
|
|
1291
|
+
// authorize its old client against a later turn it did not submit (review
|
|
1292
|
+
// thread P1).
|
|
1293
|
+
const activePromptOwner = activePromptOwnerHolder ?? { connectionIds: new Set<string>() };
|
|
1294
|
+
const currentRequesterPreflights = (): Set<() => void> => {
|
|
1295
|
+
const key = sdkControlRequesterContext.getStore() ?? "";
|
|
1296
|
+
let pending = pendingPreflights.get(key);
|
|
1297
|
+
if (!pending) {
|
|
1298
|
+
pending = new Set();
|
|
1299
|
+
pendingPreflights.set(key, pending);
|
|
1300
|
+
}
|
|
1301
|
+
return pending;
|
|
1302
|
+
};
|
|
1045
1303
|
const normalizeClientRef = (clientRef: string | undefined): string | undefined => {
|
|
1046
1304
|
if (clientRef === undefined) return undefined;
|
|
1047
1305
|
const trimmed = clientRef.trim();
|
|
@@ -1057,25 +1315,48 @@ function createControlSurface(
|
|
|
1057
1315
|
run: (options: {
|
|
1058
1316
|
onPreflightAccepted: () => void;
|
|
1059
1317
|
onPreflightAcceptCommit: () => Promise<void>;
|
|
1318
|
+
/** Fired when a queued submission (steering or follow-up) is promoted to its own run (SDK ownership correlation). */
|
|
1319
|
+
onQueuedPromoted: () => void;
|
|
1320
|
+
queuedAtDispatch: boolean;
|
|
1060
1321
|
}) => Promise<unknown>,
|
|
1061
1322
|
acceptedFields?: () => Record<string, unknown>,
|
|
1062
1323
|
allowCompletionFallback = false,
|
|
1063
1324
|
alwaysQueued = false,
|
|
1064
1325
|
): Promise<unknown> => {
|
|
1326
|
+
// Capture the REQUESTING connection at admission: a terminal abort from
|
|
1327
|
+
// another SDK connection must never stop the prompt this one accepts
|
|
1328
|
+
// (review thread P1).
|
|
1329
|
+
const requesterConnectionId = sdkControlRequesterContext.getStore();
|
|
1065
1330
|
const retainedClientRef = normalizeClientRef(clientRef);
|
|
1066
1331
|
reconciliation.admit(kind, retainedClientRef);
|
|
1067
1332
|
const correlation = newCorrelation();
|
|
1068
1333
|
const preflight = Promise.withResolvers<void>();
|
|
1069
1334
|
let accepted = false;
|
|
1070
1335
|
let settled = false;
|
|
1336
|
+
const cancelPreflight = () => {
|
|
1337
|
+
if (settled) return;
|
|
1338
|
+
settled = true;
|
|
1339
|
+
preflight.reject(
|
|
1340
|
+
Object.assign(new Error("Prompt preflight was cancelled before execution."), { code: "busy" }),
|
|
1341
|
+
);
|
|
1342
|
+
};
|
|
1343
|
+
const requesterPreflights = currentRequesterPreflights();
|
|
1344
|
+
requesterPreflights.add(cancelPreflight);
|
|
1071
1345
|
const accept = async (): Promise<void> => {
|
|
1072
1346
|
if (settled) return;
|
|
1073
|
-
|
|
1347
|
+
// startsOwnTurn is captured from the pre-dispatch idle snapshot (see
|
|
1348
|
+
// below): re-reading ctx.isIdle() here would observe the session as
|
|
1349
|
+
// already streaming, because the production AgentSession begins its
|
|
1350
|
+
// in-flight bookkeeping before the preflight acceptance callback
|
|
1351
|
+
// (review thread P1).
|
|
1074
1352
|
try {
|
|
1075
1353
|
await reconciliation.noteAccepted(kind, correlation, retainedClientRef);
|
|
1076
1354
|
accepted = true;
|
|
1077
1355
|
settled = true;
|
|
1078
|
-
|
|
1356
|
+
// The accepted submission does NOT own the active turn until its run
|
|
1357
|
+
// actually STARTS: the connection is carried on the pending entry and
|
|
1358
|
+
// associated at agent_start instead (review thread P1).
|
|
1359
|
+
onAccepted(kind, correlation, requesterConnectionId, startsOwnTurn);
|
|
1079
1360
|
preflight.resolve();
|
|
1080
1361
|
} catch (error) {
|
|
1081
1362
|
settled = true;
|
|
@@ -1087,12 +1368,32 @@ function createControlSurface(
|
|
|
1087
1368
|
// sendUserMessage will divert to steer-queue and resolve before the turn runs.
|
|
1088
1369
|
// Re-reading ctx.isIdle() after run() would race — accept() does async fs I/O that
|
|
1089
1370
|
// yields, so isStreaming can flip during the persist window.
|
|
1090
|
-
|
|
1371
|
+
// Queued when the submission is always-queued or the session is NOT idle; the
|
|
1372
|
+
// optional chaining keeps harness contexts without isIdle working (the branch
|
|
1373
|
+
// model treats an absent isIdle as idle).
|
|
1374
|
+
const queuedAtDispatch = alwaysQueued || ctx.isIdle?.() === false;
|
|
1375
|
+
// Decide whether this submission ever starts its OWN turn from the SAME
|
|
1376
|
+
// pre-dispatch snapshot: a plain prompt accepted while another turn
|
|
1377
|
+
// streams is queued as STEERING and consumed inside the current run — it
|
|
1378
|
+
// emits no agent_start, so its pending entry would be wrongly consumed
|
|
1379
|
+
// (and its connection associated as owner) by a later agent-initiated
|
|
1380
|
+
// monitor/cron turn (review thread P1). A follow-up is ALWAYS queued
|
|
1381
|
+
// (never started inline): its ownership entry is created only when the
|
|
1382
|
+
// queued follow-up is actually promoted to a run (review thread P1).
|
|
1383
|
+
// Skills always start their own invocation; a plain prompt starts one
|
|
1384
|
+
// only when idle at dispatch time.
|
|
1385
|
+
const startsOwnTurn = kind === "skill" || (kind === "prompt" && !alwaysQueued && !queuedAtDispatch);
|
|
1091
1386
|
try {
|
|
1092
1387
|
const submission = Promise.resolve(
|
|
1093
1388
|
run({
|
|
1094
|
-
onPreflightAccepted: () => void accept().catch(
|
|
1389
|
+
onPreflightAccepted: () => void accept().catch(() => undefined),
|
|
1095
1390
|
onPreflightAcceptCommit: accept,
|
|
1391
|
+
// A queued submission (busy-accepted steering, or a follow-up) that
|
|
1392
|
+
// is later PROMOTED to its own run needs its pending ownership entry
|
|
1393
|
+
// created at promotion so the submitting connection can
|
|
1394
|
+
// terminal-abort that turn (review threads P1/P2).
|
|
1395
|
+
onQueuedPromoted: () => onPromotedTurn?.(kind, correlation, requesterConnectionId),
|
|
1396
|
+
queuedAtDispatch,
|
|
1096
1397
|
}),
|
|
1097
1398
|
);
|
|
1098
1399
|
void submission.then(
|
|
@@ -1113,7 +1414,7 @@ function createControlSurface(
|
|
|
1113
1414
|
return;
|
|
1114
1415
|
}
|
|
1115
1416
|
if (allowCompletionFallback) {
|
|
1116
|
-
void accept().catch(
|
|
1417
|
+
void accept().catch(() => undefined);
|
|
1117
1418
|
return;
|
|
1118
1419
|
}
|
|
1119
1420
|
settled = true;
|
|
@@ -1127,8 +1428,12 @@ function createControlSurface(
|
|
|
1127
1428
|
if (settled) {
|
|
1128
1429
|
// The submission promise rejects after preflight acceptance only when the
|
|
1129
1430
|
// work itself is over (provider stream interrupt, abort, queue failure).
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1431
|
+
// The accepted run never started (agent_start never fired), so its pending
|
|
1432
|
+
// entry must be retired — otherwise a later agent-initiated
|
|
1433
|
+
// monitor/cron turn's agent_start would shift the stale entry and
|
|
1434
|
+
// associate the failed submission's connection as owner (review
|
|
1435
|
+
// thread P1).
|
|
1436
|
+
retirePendingOwner?.(correlation);
|
|
1132
1437
|
void reconciliation.noteTransition(kind, correlation, { type: "agent_failed", error });
|
|
1133
1438
|
return;
|
|
1134
1439
|
}
|
|
@@ -1146,22 +1451,895 @@ function createControlSurface(
|
|
|
1146
1451
|
} catch (error) {
|
|
1147
1452
|
if (!accepted) reconciliation.release(kind, retainedClientRef);
|
|
1148
1453
|
throw error;
|
|
1454
|
+
} finally {
|
|
1455
|
+
requesterPreflights.delete(cancelPreflight);
|
|
1456
|
+
if (requesterPreflights.size === 0) pendingPreflights.delete(sdkControlRequesterContext.getStore() ?? "");
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
const terminalAbort = async (
|
|
1460
|
+
input: { mode: "terminal"; scope?: "turn" | "owned" },
|
|
1461
|
+
idempotencyKey?: string,
|
|
1462
|
+
): Promise<unknown> => {
|
|
1463
|
+
const scope = input.scope === "owned" ? "owned" : "turn";
|
|
1464
|
+
// Capture the steering snapshot at ADMISSION (before any durable
|
|
1465
|
+
// transaction): client steering admitted while the abort is in flight
|
|
1466
|
+
// classifies as post-snapshot and is preserved at abortPromptAndWait
|
|
1467
|
+
// (review thread P1).
|
|
1468
|
+
const steeringSnapshotToken = terminalAbortSeams?.captureTerminalAbortSteeringSnapshot?.();
|
|
1469
|
+
let steeringSnapshotConsumed = false;
|
|
1470
|
+
const terminalReservationLimit =
|
|
1471
|
+
terminalAbortSeams?.maxDurableTerminalReservationsForTests ?? MAX_DURABLE_TERMINAL_RESERVATIONS;
|
|
1472
|
+
try {
|
|
1473
|
+
// Hash the EXACT response payload this abort will return: the durable row
|
|
1474
|
+
// stores it at finalization so the response-state advance requires
|
|
1475
|
+
// equality instead of trusting a non-pending placeholder (review thread P2).
|
|
1476
|
+
const hashResult = (value: unknown): string =>
|
|
1477
|
+
crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
1478
|
+
const store = reconciliation.store;
|
|
1479
|
+
if (!store?.path || !terminalAbortSeams) {
|
|
1480
|
+
return {
|
|
1481
|
+
ok: true,
|
|
1482
|
+
selection: scope,
|
|
1483
|
+
turn: "no_store",
|
|
1484
|
+
terminal: "terminal_no_effect",
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
const keyHash =
|
|
1488
|
+
typeof idempotencyKey === "string"
|
|
1489
|
+
? crypto.createHash("sha256").update(idempotencyKey).digest("hex")
|
|
1490
|
+
: undefined;
|
|
1491
|
+
const inputHash = crypto
|
|
1492
|
+
.createHash("sha256")
|
|
1493
|
+
.update(JSON.stringify({ mode: "terminal", scope }))
|
|
1494
|
+
.digest("hex");
|
|
1495
|
+
const stored = (record: SdkOnlyTerminalScopeRecord | SdkOnlyEvictedTerminalKeyEntry) => ({
|
|
1496
|
+
responseState: record.responseState ?? "pending",
|
|
1497
|
+
responsePayloadHash: record.responsePayloadHash ?? inputHash,
|
|
1498
|
+
terminalPublished: record.terminalPublished === true,
|
|
1499
|
+
});
|
|
1500
|
+
// The exact response a same-key retry delivers appends the replay envelope
|
|
1501
|
+
// (and, for uncertainty, the replay reason). The delivery hash check
|
|
1502
|
+
// requires exact equality, so the durable row must store BOTH the original
|
|
1503
|
+
// response hash (first write) and the replay-shaped hash (retry write) or a
|
|
1504
|
+
// successfully written replay could never advance a pending row to sent
|
|
1505
|
+
// (review thread P2).
|
|
1506
|
+
const replayShapedHash = (
|
|
1507
|
+
record: SdkOnlyTerminalScopeRecord,
|
|
1508
|
+
result: Record<string, unknown>,
|
|
1509
|
+
payloadHash: string,
|
|
1510
|
+
): string =>
|
|
1511
|
+
crypto
|
|
1512
|
+
.createHash("sha256")
|
|
1513
|
+
.update(
|
|
1514
|
+
JSON.stringify({
|
|
1515
|
+
...result,
|
|
1516
|
+
...(result.turn === "uncertain" && typeof result.reason === "string"
|
|
1517
|
+
? { reason: "replay_uncertain" }
|
|
1518
|
+
: {}),
|
|
1519
|
+
replay: {
|
|
1520
|
+
responseState: record.responseState ?? "pending",
|
|
1521
|
+
responsePayloadHash: payloadHash,
|
|
1522
|
+
terminalPublished: record.terminalPublished === true,
|
|
1523
|
+
},
|
|
1524
|
+
}),
|
|
1525
|
+
)
|
|
1526
|
+
.digest("hex");
|
|
1527
|
+
const replay = (): unknown => {
|
|
1528
|
+
const scopes = store.snapshotTerminalScopes();
|
|
1529
|
+
const existing = keyHash ? scopes.find(record => record.idempotencyKeyHash === keyHash) : undefined;
|
|
1530
|
+
if (existing) {
|
|
1531
|
+
if (existing.idempotencyInputHash !== inputHash)
|
|
1532
|
+
throw Object.assign(new Error("Idempotency key was reused with different input."), {
|
|
1533
|
+
code: "idempotency_conflict",
|
|
1534
|
+
});
|
|
1535
|
+
const persisted = stored(existing);
|
|
1536
|
+
if (existing.turnDisposition === "stopped")
|
|
1537
|
+
return {
|
|
1538
|
+
ok: true,
|
|
1539
|
+
selection: scope,
|
|
1540
|
+
turn: "stopped",
|
|
1541
|
+
...(scope === "owned"
|
|
1542
|
+
? {
|
|
1543
|
+
ownedWork: existing.ownedWorkDisposition === "stopped" ? "stopped" : "uncertain",
|
|
1544
|
+
automaticDelivery: "none",
|
|
1545
|
+
resumeOnOwnedCompletion: false,
|
|
1546
|
+
}
|
|
1547
|
+
: { ownedWork: "left_running", automaticDelivery: "enabled", resumeOnOwnedCompletion: true }),
|
|
1548
|
+
replay: persisted,
|
|
1549
|
+
};
|
|
1550
|
+
if (existing.turnDisposition === "no_effect")
|
|
1551
|
+
return {
|
|
1552
|
+
ok: true,
|
|
1553
|
+
selection: scope,
|
|
1554
|
+
turn: "no_active_turn",
|
|
1555
|
+
terminal: "terminal_no_effect",
|
|
1556
|
+
replay: persisted,
|
|
1557
|
+
};
|
|
1558
|
+
if (existing.turnDisposition === "no_effect_marker_failure")
|
|
1559
|
+
// The initial marker write failed before any destructive work;
|
|
1560
|
+
// replay the SAME no_effect result the request returned, never
|
|
1561
|
+
// a no_active_turn fabrication (review thread P2).
|
|
1562
|
+
return {
|
|
1563
|
+
ok: true,
|
|
1564
|
+
selection: scope,
|
|
1565
|
+
turn: "no_effect",
|
|
1566
|
+
terminal: "terminal_no_effect",
|
|
1567
|
+
replay: persisted,
|
|
1568
|
+
};
|
|
1569
|
+
if (existing.turnDisposition === "no_effect_reserved")
|
|
1570
|
+
// A no-effect reservation that may still transition to active: a
|
|
1571
|
+
// duplicate must never claim no_active_turn over a provisional row
|
|
1572
|
+
// (review thread P2).
|
|
1573
|
+
return {
|
|
1574
|
+
ok: true,
|
|
1575
|
+
selection: scope,
|
|
1576
|
+
turn: "uncertain",
|
|
1577
|
+
ownedWork: scope === "turn" ? "left_running" : "uncertain",
|
|
1578
|
+
automaticDelivery: scope === "turn" ? "enabled" : "none",
|
|
1579
|
+
resumeOnOwnedCompletion: scope === "turn",
|
|
1580
|
+
reason: "reservation_in_flight",
|
|
1581
|
+
replay: persisted,
|
|
1582
|
+
};
|
|
1583
|
+
return {
|
|
1584
|
+
ok: true,
|
|
1585
|
+
selection: scope,
|
|
1586
|
+
turn: "uncertain",
|
|
1587
|
+
ownedWork: scope === "turn" ? "left_running" : "uncertain",
|
|
1588
|
+
automaticDelivery: scope === "turn" ? "enabled" : "none",
|
|
1589
|
+
resumeOnOwnedCompletion: scope === "turn",
|
|
1590
|
+
reason: existing.turnDisposition === "pending" ? "replay_pending" : "replay_uncertain",
|
|
1591
|
+
replay: persisted,
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
if (keyHash) {
|
|
1595
|
+
const tombstone = store.snapshotTerminalKeys().find(record => record.keyHash === keyHash);
|
|
1596
|
+
if (tombstone) {
|
|
1597
|
+
if (tombstone.inputHash !== inputHash)
|
|
1598
|
+
throw Object.assign(new Error("Idempotency key was reused with different input."), {
|
|
1599
|
+
code: "idempotency_conflict",
|
|
1600
|
+
});
|
|
1601
|
+
return tombstone.turnDisposition === "stopped"
|
|
1602
|
+
? {
|
|
1603
|
+
ok: true,
|
|
1604
|
+
selection: scope,
|
|
1605
|
+
turn: "stopped",
|
|
1606
|
+
...(scope === "owned"
|
|
1607
|
+
? {
|
|
1608
|
+
ownedWork: tombstone.ownedWorkDisposition === "stopped" ? "stopped" : "uncertain",
|
|
1609
|
+
automaticDelivery: "none",
|
|
1610
|
+
resumeOnOwnedCompletion: false,
|
|
1611
|
+
}
|
|
1612
|
+
: {
|
|
1613
|
+
ownedWork: "left_running",
|
|
1614
|
+
automaticDelivery: "enabled",
|
|
1615
|
+
resumeOnOwnedCompletion: true,
|
|
1616
|
+
}),
|
|
1617
|
+
replay: stored(tombstone),
|
|
1618
|
+
}
|
|
1619
|
+
: tombstone.turnDisposition === "no_effect"
|
|
1620
|
+
? {
|
|
1621
|
+
ok: true,
|
|
1622
|
+
selection: scope,
|
|
1623
|
+
turn: "no_active_turn",
|
|
1624
|
+
terminal: "terminal_no_effect",
|
|
1625
|
+
replay: stored(tombstone),
|
|
1626
|
+
}
|
|
1627
|
+
: tombstone.turnDisposition === "no_effect_marker_failure"
|
|
1628
|
+
? {
|
|
1629
|
+
ok: true,
|
|
1630
|
+
selection: scope,
|
|
1631
|
+
turn: "no_effect",
|
|
1632
|
+
terminal: "terminal_no_effect",
|
|
1633
|
+
replay: stored(tombstone),
|
|
1634
|
+
}
|
|
1635
|
+
: {
|
|
1636
|
+
ok: true,
|
|
1637
|
+
selection: scope,
|
|
1638
|
+
turn: "uncertain",
|
|
1639
|
+
ownedWork: scope === "turn" ? "left_running" : "uncertain",
|
|
1640
|
+
automaticDelivery: scope === "turn" ? "enabled" : "none",
|
|
1641
|
+
resumeOnOwnedCompletion: scope === "turn",
|
|
1642
|
+
replay: stored(tombstone),
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
return undefined;
|
|
1647
|
+
};
|
|
1648
|
+
const prior = replay();
|
|
1649
|
+
if (prior !== undefined) return prior;
|
|
1650
|
+
const writeNoEffect = async (markerFailure = false): Promise<"ok" | "conflict"> => {
|
|
1651
|
+
try {
|
|
1652
|
+
await store.transactTerminalState(state => {
|
|
1653
|
+
// Atomic recheck: a concurrent request may have committed a
|
|
1654
|
+
// DIFFERENT input under this key after the earlier snapshot
|
|
1655
|
+
// check; appending a second same-key row would make later
|
|
1656
|
+
// replay's .find() by key hash ambiguous (review thread P2).
|
|
1657
|
+
const conflicting = state.scopes.find(record => keyHash && record.idempotencyKeyHash === keyHash);
|
|
1658
|
+
if (conflicting && conflicting.idempotencyInputHash !== inputHash)
|
|
1659
|
+
throw new SdkOnlyIdempotencyConflictError();
|
|
1660
|
+
// A SAME-input live row is durable replay authority (the original
|
|
1661
|
+
// in-flight abort's marker): never replace it with a no-effect
|
|
1662
|
+
// reservation, or the successful abort would replay later as
|
|
1663
|
+
// no_active_turn. Leave the store unchanged and let the caller
|
|
1664
|
+
// re-run the replay snapshot (review thread P2).
|
|
1665
|
+
if (conflicting) {
|
|
1666
|
+
existingReplay = conflicting;
|
|
1667
|
+
return { scopes: state.scopes, keys: state.keys };
|
|
1668
|
+
}
|
|
1669
|
+
// A concurrent admission may ALSO have evicted a same-key row into
|
|
1670
|
+
// the tombstone collection after this request's snapshot; recheck
|
|
1671
|
+
// keys so a different input can never install a fresh marker over
|
|
1672
|
+
// existing durable replay authority (review thread P2). A
|
|
1673
|
+
// same-input tombstone already carries the reservation: leave the
|
|
1674
|
+
// store unchanged and replay it.
|
|
1675
|
+
const tombstone = state.keys.find(record => keyHash && record.keyHash === keyHash);
|
|
1676
|
+
if (tombstone) {
|
|
1677
|
+
if (tombstone.inputHash !== inputHash) throw new SdkOnlyIdempotencyConflictError();
|
|
1678
|
+
existingReplay = tombstone;
|
|
1679
|
+
return { scopes: state.scopes, keys: state.keys };
|
|
1680
|
+
}
|
|
1681
|
+
const preBound: SdkOnlyTerminalScopeRecord[] = [
|
|
1682
|
+
...state.scopes.filter(record => !(keyHash && record.idempotencyKeyHash === keyHash)),
|
|
1683
|
+
{
|
|
1684
|
+
selection: scope,
|
|
1685
|
+
...(keyHash ? { idempotencyKeyHash: keyHash, idempotencyInputHash: inputHash } : {}),
|
|
1686
|
+
// A marker-failure reservation must replay as the SAME
|
|
1687
|
+
// no_effect result it was returned with, so one idempotency
|
|
1688
|
+
// key can never produce no_effect first and no_active_turn
|
|
1689
|
+
// after eviction/restart (review thread P2). The idle-abort
|
|
1690
|
+
// path writes a TRANSITIONAL reserved disposition: the
|
|
1691
|
+
// requester's prompt may become active while the reservation
|
|
1692
|
+
// is awaited, and a duplicate must never claim no_active_turn
|
|
1693
|
+
// over a provisional row — the reserved row is finalized to
|
|
1694
|
+
// plain no_effect only when the recheck confirms no active
|
|
1695
|
+
// turn (review thread P2).
|
|
1696
|
+
turnDisposition: markerFailure
|
|
1697
|
+
? "no_effect_marker_failure"
|
|
1698
|
+
: keyHash
|
|
1699
|
+
? "no_effect_reserved"
|
|
1700
|
+
: "no_effect",
|
|
1701
|
+
ownedWorkDisposition: "not_requested",
|
|
1702
|
+
automaticDeliveryDisposition: scope === "turn" ? "enabled" : "none",
|
|
1703
|
+
resumeOnOwnedCompletion: scope === "turn",
|
|
1704
|
+
turnContinuationFence: {
|
|
1705
|
+
state: "retained",
|
|
1706
|
+
abortedAttemptEpoch: 0,
|
|
1707
|
+
blockedContinuationIds: [],
|
|
1708
|
+
predecessorTombstones: [],
|
|
1709
|
+
ownedCompletionPolicy: scope === "turn" ? "enabled" : "disabled",
|
|
1710
|
+
},
|
|
1711
|
+
responseState: "pending",
|
|
1712
|
+
// marker_failure rows are FINAL as written (the abort returns the
|
|
1713
|
+
// public no_effect disposition immediately, no later
|
|
1714
|
+
// finalization), so store the public payload hash; idle
|
|
1715
|
+
// reservations are finalized by finalizeNoEffectReservation
|
|
1716
|
+
// (review thread P2). The replay-shaped hash is stored too so a
|
|
1717
|
+
// same-key retry's metadata-bearing replay can still advance the
|
|
1718
|
+
// row on delivery (review thread P2).
|
|
1719
|
+
responsePayloadHash: markerFailure
|
|
1720
|
+
? hashResult({
|
|
1721
|
+
ok: true,
|
|
1722
|
+
selection: scope,
|
|
1723
|
+
turn: "no_effect",
|
|
1724
|
+
terminal: "terminal_no_effect",
|
|
1725
|
+
})
|
|
1726
|
+
: inputHash,
|
|
1727
|
+
replayPayloadHash: markerFailure
|
|
1728
|
+
? hashResult({
|
|
1729
|
+
ok: true,
|
|
1730
|
+
selection: scope,
|
|
1731
|
+
turn: "no_effect",
|
|
1732
|
+
terminal: "terminal_no_effect",
|
|
1733
|
+
replay: {
|
|
1734
|
+
responseState: "pending",
|
|
1735
|
+
responsePayloadHash: hashResult({
|
|
1736
|
+
ok: true,
|
|
1737
|
+
selection: scope,
|
|
1738
|
+
turn: "no_effect",
|
|
1739
|
+
terminal: "terminal_no_effect",
|
|
1740
|
+
}),
|
|
1741
|
+
terminalPublished: false,
|
|
1742
|
+
},
|
|
1743
|
+
})
|
|
1744
|
+
: undefined,
|
|
1745
|
+
acceptedAt: Date.now(),
|
|
1746
|
+
},
|
|
1747
|
+
];
|
|
1748
|
+
return boundTerminalRetentionState(state.keys, preBound, terminalReservationLimit);
|
|
1749
|
+
});
|
|
1750
|
+
return "ok";
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
if (error instanceof SdkOnlyIdempotencyConflictError) return "conflict";
|
|
1753
|
+
throw error;
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
// Finalize THIS abort's transitional no_effect_reserved reservation to
|
|
1757
|
+
// plain no_effect once the recheck confirms there is no active turn to
|
|
1758
|
+
// stop: a later same-key retry then replays the deterministic
|
|
1759
|
+
// no_active_turn result instead of reservation_in_flight uncertainty
|
|
1760
|
+
// (review thread P2). Only OUR row (exact key+input, still reserved) is
|
|
1761
|
+
// touched — a concurrent transition that already replaced it is left
|
|
1762
|
+
// alone. The EXACT final response payload hash is stored so the
|
|
1763
|
+
// response-state advance can require equality instead of trusting a
|
|
1764
|
+
// non-pending placeholder (review thread P2).
|
|
1765
|
+
const finalizeNoEffectReservation = async (result: {
|
|
1766
|
+
ok: boolean;
|
|
1767
|
+
selection: string;
|
|
1768
|
+
turn: string;
|
|
1769
|
+
terminal: string;
|
|
1770
|
+
}): Promise<void> => {
|
|
1771
|
+
if (!keyHash) return;
|
|
1772
|
+
const payloadHash = hashResult(result);
|
|
1773
|
+
// The same-key retry delivers the metadata-bearing replay envelope;
|
|
1774
|
+
// store its hash too so the retry's written response can advance the
|
|
1775
|
+
// finalized row (review thread P2).
|
|
1776
|
+
const replayPayloadHash = hashResult({
|
|
1777
|
+
...result,
|
|
1778
|
+
replay: { responseState: "pending", responsePayloadHash: payloadHash, terminalPublished: false },
|
|
1779
|
+
});
|
|
1780
|
+
try {
|
|
1781
|
+
await store.transactTerminalState(state => {
|
|
1782
|
+
const scopes: SdkOnlyTerminalScopeRecord[] = state.scopes.map(record =>
|
|
1783
|
+
record.idempotencyKeyHash === keyHash &&
|
|
1784
|
+
record.idempotencyInputHash === inputHash &&
|
|
1785
|
+
record.turnDisposition === "no_effect_reserved"
|
|
1786
|
+
? {
|
|
1787
|
+
...record,
|
|
1788
|
+
turnDisposition: "no_effect",
|
|
1789
|
+
responsePayloadHash: payloadHash,
|
|
1790
|
+
replayPayloadHash,
|
|
1791
|
+
}
|
|
1792
|
+
: record,
|
|
1793
|
+
);
|
|
1794
|
+
// Finalized reservations become evictable completed rows: apply
|
|
1795
|
+
// the SAME bounded retention as writeNoEffect so a burst of idle
|
|
1796
|
+
// aborts cannot grow the document (review thread P2).
|
|
1797
|
+
return boundTerminalRetentionState(state.keys, scopes, terminalReservationLimit);
|
|
1798
|
+
});
|
|
1799
|
+
} catch {
|
|
1800
|
+
// Best-effort: the row stays reserved (replays as uncertainty)
|
|
1801
|
+
// rather than failing the abort (review thread P2).
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
// Finalize pending markers through the SAME bounded retention as the
|
|
1805
|
+
// admission writes: mapping pending rows to completed dispositions
|
|
1806
|
+
// (uncertain/stopped) must evict the oldest completed rows and retain
|
|
1807
|
+
// tombstones, or a burst of concurrent distinct-key aborts of one slow
|
|
1808
|
+
// turn leaves an arbitrarily large reconciliation document (review
|
|
1809
|
+
// thread P2).
|
|
1810
|
+
const transactBoundedTerminalScopes = async (
|
|
1811
|
+
mutate: (scopes: SdkOnlyTerminalScopeRecord[]) => SdkOnlyTerminalScopeRecord[],
|
|
1812
|
+
): Promise<void> => {
|
|
1813
|
+
await store.transactTerminalState(state => {
|
|
1814
|
+
return boundTerminalRetentionState(state.keys, mutate(state.scopes), terminalReservationLimit);
|
|
1815
|
+
});
|
|
1816
|
+
};
|
|
1817
|
+
let handle = terminalAbortSeams.getActivePromptHandle();
|
|
1818
|
+
let epoch = terminalAbortSeams.getTerminalTurnEpoch();
|
|
1819
|
+
// Set when the no-effect reservation found an existing SAME-input row or
|
|
1820
|
+
// tombstone: the caller re-runs the replay snapshot instead of returning
|
|
1821
|
+
// a no-active result over the original row's replay authority (review
|
|
1822
|
+
// thread P2).
|
|
1823
|
+
let existingReplay: SdkOnlyTerminalScopeRecord | SdkOnlyEvictedTerminalKeyEntry | undefined;
|
|
1824
|
+
// Read the requester's preflight bucket WITHOUT creating one: an
|
|
1825
|
+
// abort-only request that returns via an early replay/marker path (which
|
|
1826
|
+
// never runs the bucket cleanup) must not leave an empty per-connection
|
|
1827
|
+
// entry behind — reconnecting clients would otherwise accumulate buckets
|
|
1828
|
+
// indefinitely (review thread P2).
|
|
1829
|
+
const requesterBucketKey = sdkControlRequesterContext.getStore() ?? "";
|
|
1830
|
+
const requesterPreflights = pendingPreflights.get(requesterBucketKey);
|
|
1831
|
+
// Snapshot the requester's preflight callbacks AT ADMISSION: a successor
|
|
1832
|
+
// turn.prompt pipelined by the same connection while the abort awaits
|
|
1833
|
+
// (e.g. the reconciliation transaction) must never be cancelled as part
|
|
1834
|
+
// of this abort — only the callbacks present when it was admitted are
|
|
1835
|
+
// its to cancel, mirroring the full-bus capture (review thread P1).
|
|
1836
|
+
const admittedRequesterPreflights = new Set(requesterPreflights ?? []);
|
|
1837
|
+
const cancelRequesterPreflights = () => {
|
|
1838
|
+
for (const cancel of [...admittedRequesterPreflights]) cancel();
|
|
1839
|
+
if (admittedRequesterPreflights.size > 0) {
|
|
1840
|
+
// Remove the admitted callbacks from the live set so a preflight
|
|
1841
|
+
// added by a LATER submission is untouched by this abort (and the
|
|
1842
|
+
// bucket cleanup below reflects what actually remains).
|
|
1843
|
+
for (const cancel of admittedRequesterPreflights) requesterPreflights?.delete(cancel);
|
|
1844
|
+
// The seam cancels the SESSION-WIDE preflight controller, so only
|
|
1845
|
+
// invoke it when NO OTHER connection has a pending admission: a
|
|
1846
|
+
// queued requester's abort must reject its own wrapper callback
|
|
1847
|
+
// (above) without cancelling another connection's active
|
|
1848
|
+
// preflight — the aborting requester's admission is already
|
|
1849
|
+
// rejected, so the session-wide abort is never required for it
|
|
1850
|
+
// (review thread P1).
|
|
1851
|
+
const otherConnectionPreflights = [...pendingPreflights.entries()].some(
|
|
1852
|
+
([bucket, callbacks]) => bucket !== requesterBucketKey && callbacks.size > 0,
|
|
1853
|
+
);
|
|
1854
|
+
if (!otherConnectionPreflights) terminalAbortSeams.cancelPendingPreflightForTerminalAbort();
|
|
1855
|
+
}
|
|
1856
|
+
if (requesterPreflights && requesterPreflights.size === 0) {
|
|
1857
|
+
// Abort-only lookups must not retain an empty per-connection bucket:
|
|
1858
|
+
// connections are ephemeral UUIDs and nothing else removes the
|
|
1859
|
+
// bucket when no prompt submission ever registered on it, so a
|
|
1860
|
+
// long-lived runtime handling aborts from reconnecting clients
|
|
1861
|
+
// would accumulate one entry per connection forever (review
|
|
1862
|
+
// thread P2).
|
|
1863
|
+
if (pendingPreflights.get(requesterBucketKey) === requesterPreflights)
|
|
1864
|
+
pendingPreflights.delete(requesterBucketKey);
|
|
1865
|
+
}
|
|
1866
|
+
};
|
|
1867
|
+
if (!handle || epoch === undefined) {
|
|
1868
|
+
if ((await writeNoEffect()) === "conflict") {
|
|
1869
|
+
throw Object.assign(new Error("Idempotency key was reused with different input."), {
|
|
1870
|
+
code: "idempotency_conflict",
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
// A SAME-input row or tombstone existed while the reservation
|
|
1874
|
+
// awaited the store: it is durable replay authority, so replay its
|
|
1875
|
+
// stored result (stopped/pending/uncertain/no_effect) instead of
|
|
1876
|
+
// returning no_active_turn over it (review thread P2).
|
|
1877
|
+
if (existingReplay) {
|
|
1878
|
+
const replayed = replay();
|
|
1879
|
+
if (replayed !== undefined) return replayed;
|
|
1880
|
+
}
|
|
1881
|
+
cancelRequesterPreflights();
|
|
1882
|
+
// A prompt for this requester may have become ACTIVE while the
|
|
1883
|
+
// reservation awaited the filesystem transaction: its submit()
|
|
1884
|
+
// cleanup already removed the preflight callback, so cancelling here
|
|
1885
|
+
// saw an empty set and never reached the session cancellation seam —
|
|
1886
|
+
// and the durable no-effect row would prevent a same-key retry from
|
|
1887
|
+
// ever stopping the now-running prompt. Re-read the active prompt and
|
|
1888
|
+
// fall through to ACTIVE terminalization when it won the race
|
|
1889
|
+
// (review thread P1); the active-turn marker write replaces the
|
|
1890
|
+
// no-effect reservation.
|
|
1891
|
+
const recheckedHandle = terminalAbortSeams.getActivePromptHandle();
|
|
1892
|
+
const recheckedEpoch = terminalAbortSeams.getTerminalTurnEpoch();
|
|
1893
|
+
if (!recheckedHandle || recheckedEpoch === undefined) {
|
|
1894
|
+
// No prompt won the race: finalize the reserved row so a later
|
|
1895
|
+
// same-key retry replays this deterministic no_active_turn result
|
|
1896
|
+
// (review thread P2).
|
|
1897
|
+
const noActiveTurnResult = {
|
|
1898
|
+
ok: true,
|
|
1899
|
+
selection: scope,
|
|
1900
|
+
turn: "no_active_turn",
|
|
1901
|
+
terminal: "terminal_no_effect",
|
|
1902
|
+
};
|
|
1903
|
+
await finalizeNoEffectReservation(noActiveTurnResult);
|
|
1904
|
+
return noActiveTurnResult;
|
|
1905
|
+
}
|
|
1906
|
+
handle = recheckedHandle;
|
|
1907
|
+
epoch = recheckedEpoch;
|
|
1908
|
+
// The requester's OWN prompt won the race: rebind the snapshot
|
|
1909
|
+
// to the current turn so the settlement classifies steering
|
|
1910
|
+
// admitted since admission as post-snapshot (review thread P1).
|
|
1911
|
+
if (steeringSnapshotToken !== undefined) {
|
|
1912
|
+
terminalAbortSeams?.rebindTerminalAbortSteeringSnapshot?.(steeringSnapshotToken);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
// Requester ownership: the active prompt belongs to the SDK connection
|
|
1916
|
+
// that accepted it. Another connection's terminal abort must not stop it
|
|
1917
|
+
// (review thread P1) — no-op with an idle reservation, mirroring the
|
|
1918
|
+
// per-connection selection of the full bus path. The owner is re-read
|
|
1919
|
+
// through the seam when provided (deterministic tests) and otherwise
|
|
1920
|
+
// from the runtime-tracked accepting connection.
|
|
1921
|
+
const abortingConnectionId = sdkControlRequesterContext.getStore();
|
|
1922
|
+
const currentOwnerConnectionIds = (): ReadonlySet<string> => {
|
|
1923
|
+
const seam = terminalAbortSeams.getActivePromptOwnerConnectionId?.();
|
|
1924
|
+
// The seam reports a single deterministic owner (test harnesses); the
|
|
1925
|
+
// runtime holder may carry every connection whose follow-up was
|
|
1926
|
+
// promoted into the current run (review thread P2).
|
|
1927
|
+
return seam === undefined ? (activePromptOwner.connectionIds ?? new Set<string>()) : new Set([seam]);
|
|
1928
|
+
};
|
|
1929
|
+
// FAIL CLOSED unless the active handle is POSITIVELY associated with the
|
|
1930
|
+
// requester: an undefined owner (agent-initiated monitor/cron turn, or
|
|
1931
|
+
// cleared after a terminal lifecycle boundary) authorizes no client, and
|
|
1932
|
+
// a stale prior owner authorizes only that old client — never a later
|
|
1933
|
+
// turn it did not submit (review thread P1).
|
|
1934
|
+
if (handle && (abortingConnectionId === undefined || !currentOwnerConnectionIds().has(abortingConnectionId))) {
|
|
1935
|
+
if ((await writeNoEffect()) === "conflict") {
|
|
1936
|
+
throw Object.assign(new Error("Idempotency key was reused with different input."), {
|
|
1937
|
+
code: "idempotency_conflict",
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
if (existingReplay) {
|
|
1941
|
+
const replayed = replay();
|
|
1942
|
+
if (replayed !== undefined) return replayed;
|
|
1943
|
+
}
|
|
1944
|
+
cancelRequesterPreflights();
|
|
1945
|
+
// A prompt for this requester may have become ACTIVE while the
|
|
1946
|
+
// no-effect reservation awaited the filesystem transaction: the
|
|
1947
|
+
// owner-mismatch decision was taken against the OLD owner, and the
|
|
1948
|
+
// newly active submission already removed its preflight callback, so
|
|
1949
|
+
// cancelling here saw an empty set and never reached the session
|
|
1950
|
+
// cancellation seam — and the durable no-effect row would prevent a
|
|
1951
|
+
// same-key retry from ever stopping the now-running prompt. Re-read
|
|
1952
|
+
// the active prompt, its epoch, and its owner; when the ABORTING
|
|
1953
|
+
// connection now owns the turn, fall through to ACTIVE
|
|
1954
|
+
// terminalization (the active-turn marker write replaces the
|
|
1955
|
+
// no-effect reservation) (review thread P1).
|
|
1956
|
+
const recheckedHandle = terminalAbortSeams.getActivePromptHandle();
|
|
1957
|
+
const recheckedEpoch = terminalAbortSeams.getTerminalTurnEpoch();
|
|
1958
|
+
const recheckedOwners = currentOwnerConnectionIds();
|
|
1959
|
+
if (
|
|
1960
|
+
!recheckedHandle ||
|
|
1961
|
+
recheckedEpoch === undefined ||
|
|
1962
|
+
abortingConnectionId === undefined ||
|
|
1963
|
+
!recheckedOwners.has(abortingConnectionId)
|
|
1964
|
+
) {
|
|
1965
|
+
// The turn is still not the aborting connection's: finalize the
|
|
1966
|
+
// reserved row so a later same-key retry replays no_active_turn
|
|
1967
|
+
// deterministically (review thread P2).
|
|
1968
|
+
const noActiveTurnResult = {
|
|
1969
|
+
ok: true,
|
|
1970
|
+
selection: scope,
|
|
1971
|
+
turn: "no_active_turn",
|
|
1972
|
+
terminal: "terminal_no_effect",
|
|
1973
|
+
};
|
|
1974
|
+
await finalizeNoEffectReservation(noActiveTurnResult);
|
|
1975
|
+
return noActiveTurnResult;
|
|
1976
|
+
}
|
|
1977
|
+
handle = recheckedHandle;
|
|
1978
|
+
epoch = recheckedEpoch;
|
|
1979
|
+
// The requester's OWN prompt won the race: rebind the snapshot
|
|
1980
|
+
// to the current turn so the settlement classifies steering
|
|
1981
|
+
// admitted since admission as post-snapshot (review thread P1).
|
|
1982
|
+
if (steeringSnapshotToken !== undefined) {
|
|
1983
|
+
terminalAbortSeams?.rebindTerminalAbortSteeringSnapshot?.(steeringSnapshotToken);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
let pendingReplay: SdkOnlyTerminalScopeRecord | undefined;
|
|
1987
|
+
let tombstoneReplay: SdkOnlyEvictedTerminalKeyEntry | undefined;
|
|
1988
|
+
try {
|
|
1989
|
+
await store.transactTerminalState(state => {
|
|
1990
|
+
// Atomic recheck (same rationale as writeNoEffect): never wipe a
|
|
1991
|
+
// row a concurrent request committed under this key (review thread
|
|
1992
|
+
// P2). A same-input PENDING row is an in-flight duplicate admitted
|
|
1993
|
+
// past the snapshot (dispatch-cache eviction): replay it instead of
|
|
1994
|
+
// replacing the marker, so the duplicate cannot race terminalization
|
|
1995
|
+
// and flip the row to uncertain while the original returns stopped
|
|
1996
|
+
// (or execute the abort twice).
|
|
1997
|
+
const conflicting = state.scopes.find(record => keyHash && record.idempotencyKeyHash === keyHash);
|
|
1998
|
+
if (conflicting) {
|
|
1999
|
+
if (conflicting.idempotencyInputHash !== inputHash) throw new SdkOnlyIdempotencyConflictError();
|
|
2000
|
+
if (conflicting.turnDisposition === "pending") {
|
|
2001
|
+
pendingReplay = conflicting;
|
|
2002
|
+
return { scopes: state.scopes, keys: state.keys };
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
// A concurrent admission may ALSO have evicted a same-key row into
|
|
2006
|
+
// the tombstone collection after this request's snapshot. Recheck
|
|
2007
|
+
// keys so a different input can never install a fresh marker over
|
|
2008
|
+
// existing durable replay authority; a same-input tombstone already
|
|
2009
|
+
// carries replay authority, so never install a second marker here
|
|
2010
|
+
// (review thread P2).
|
|
2011
|
+
const tombstone = state.keys.find(record => keyHash && record.keyHash === keyHash);
|
|
2012
|
+
if (tombstone) {
|
|
2013
|
+
if (tombstone.inputHash !== inputHash) throw new SdkOnlyIdempotencyConflictError();
|
|
2014
|
+
tombstoneReplay = tombstone;
|
|
2015
|
+
return { scopes: state.scopes, keys: state.keys };
|
|
2016
|
+
}
|
|
2017
|
+
const preBound: SdkOnlyTerminalScopeRecord[] = [
|
|
2018
|
+
...state.scopes.filter(record => !(keyHash && record.idempotencyKeyHash === keyHash)),
|
|
2019
|
+
{
|
|
2020
|
+
selection: scope,
|
|
2021
|
+
...(keyHash ? { idempotencyKeyHash: keyHash, idempotencyInputHash: inputHash } : {}),
|
|
2022
|
+
turnDisposition: "pending",
|
|
2023
|
+
terminalPublished: false,
|
|
2024
|
+
ownedWorkDisposition: "not_requested",
|
|
2025
|
+
automaticDeliveryDisposition: scope === "turn" ? "enabled" : "none",
|
|
2026
|
+
resumeOnOwnedCompletion: scope === "turn",
|
|
2027
|
+
turnContinuationFence: {
|
|
2028
|
+
state: "retained",
|
|
2029
|
+
abortedAttemptEpoch: epoch,
|
|
2030
|
+
blockedContinuationIds: [],
|
|
2031
|
+
predecessorTombstones: [],
|
|
2032
|
+
ownedCompletionPolicy: scope === "turn" ? "enabled" : "disabled",
|
|
2033
|
+
},
|
|
2034
|
+
responseState: "pending",
|
|
2035
|
+
responsePayloadHash: inputHash,
|
|
2036
|
+
acceptedAt: Date.now(),
|
|
2037
|
+
},
|
|
2038
|
+
];
|
|
2039
|
+
return boundTerminalRetentionState(state.keys, preBound, terminalReservationLimit);
|
|
2040
|
+
});
|
|
2041
|
+
} catch (error) {
|
|
2042
|
+
if (error instanceof SdkOnlyIdempotencyConflictError) {
|
|
2043
|
+
throw Object.assign(new Error("Idempotency key was reused with different input."), {
|
|
2044
|
+
code: "idempotency_conflict",
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
// Marker persistence failed before any destructive work: reserve a
|
|
2048
|
+
// distinct marker-failure disposition so replay returns the same
|
|
2049
|
+
// no_effect result (review thread P2).
|
|
2050
|
+
if ((await writeNoEffect(true)) === "conflict") {
|
|
2051
|
+
throw Object.assign(new Error("Idempotency key was reused with different input."), {
|
|
2052
|
+
code: "idempotency_conflict",
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
if (existingReplay) {
|
|
2056
|
+
const replayed = replay();
|
|
2057
|
+
if (replayed !== undefined) return replayed;
|
|
2058
|
+
}
|
|
2059
|
+
return {
|
|
2060
|
+
ok: true,
|
|
2061
|
+
selection: scope,
|
|
2062
|
+
turn: "no_effect",
|
|
2063
|
+
terminal: "terminal_no_effect",
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
if (pendingReplay) {
|
|
2067
|
+
// An in-flight duplicate of this exact key+input was already admitted;
|
|
2068
|
+
// replay its pending row WITHOUT touching the seam, so the duplicate
|
|
2069
|
+
// cannot abort the run a second time or race the terminalization.
|
|
2070
|
+
return {
|
|
2071
|
+
ok: true,
|
|
2072
|
+
selection: scope,
|
|
2073
|
+
turn: "uncertain",
|
|
2074
|
+
ownedWork: scope === "turn" ? "left_running" : "uncertain",
|
|
2075
|
+
automaticDelivery: scope === "turn" ? "enabled" : "none",
|
|
2076
|
+
resumeOnOwnedCompletion: scope === "turn",
|
|
2077
|
+
reason: "replay_pending",
|
|
2078
|
+
replay: {
|
|
2079
|
+
responseState: pendingReplay.responseState,
|
|
2080
|
+
responsePayloadHash: pendingReplay.responsePayloadHash,
|
|
2081
|
+
terminalPublished: pendingReplay.terminalPublished === true,
|
|
2082
|
+
},
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
if (tombstoneReplay) {
|
|
2086
|
+
// The key gained durable replay authority via an eviction tombstone
|
|
2087
|
+
// while this request was in flight; never install a second marker or
|
|
2088
|
+
// run the abort. Re-run the replay snapshot (the tombstone is now
|
|
2089
|
+
// visible) so the STORED result is returned (review thread P2).
|
|
2090
|
+
const replayed = replay();
|
|
2091
|
+
if (replayed !== undefined) return replayed;
|
|
2092
|
+
}
|
|
2093
|
+
// A new prompt won the race while the marker was being persisted. Never
|
|
2094
|
+
// apply this request to that later handle; replay remains a safe uncertainty.
|
|
2095
|
+
if (
|
|
2096
|
+
terminalAbortSeams.getActivePromptHandle() !== handle ||
|
|
2097
|
+
terminalAbortSeams.getTerminalTurnEpoch() !== epoch
|
|
2098
|
+
) {
|
|
2099
|
+
const result = {
|
|
2100
|
+
ok: true,
|
|
2101
|
+
selection: scope,
|
|
2102
|
+
turn: "uncertain",
|
|
2103
|
+
ownedWork: scope === "turn" ? "left_running" : "uncertain",
|
|
2104
|
+
automaticDelivery: scope === "turn" ? "enabled" : "none",
|
|
2105
|
+
resumeOnOwnedCompletion: scope === "turn",
|
|
2106
|
+
reason: "active_turn_changed",
|
|
2107
|
+
};
|
|
2108
|
+
const activeTurnPayloadHash = hashResult(result);
|
|
2109
|
+
await transactBoundedTerminalScopes(scopes =>
|
|
2110
|
+
scopes.map(record =>
|
|
2111
|
+
(keyHash
|
|
2112
|
+
? record.idempotencyKeyHash === keyHash
|
|
2113
|
+
: record.turnContinuationFence.abortedAttemptEpoch === epoch) &&
|
|
2114
|
+
record.turnDisposition === "pending"
|
|
2115
|
+
? {
|
|
2116
|
+
...record,
|
|
2117
|
+
turnDisposition: "uncertain",
|
|
2118
|
+
responsePayloadHash: activeTurnPayloadHash,
|
|
2119
|
+
replayPayloadHash: replayShapedHash(record, result, activeTurnPayloadHash),
|
|
2120
|
+
terminalAt: Date.now(),
|
|
2121
|
+
}
|
|
2122
|
+
: record,
|
|
2123
|
+
),
|
|
2124
|
+
);
|
|
2125
|
+
return result;
|
|
2126
|
+
}
|
|
2127
|
+
cancelRequesterPreflights();
|
|
2128
|
+
// Observe the correlated agent_end publication (AC 19) instead of
|
|
2129
|
+
// assuming it: the aborted run's lifecycle event is published
|
|
2130
|
+
// independently by emitLifecycle, so the durable stopped row must only
|
|
2131
|
+
// claim terminalPublished when the publication was actually observed
|
|
2132
|
+
// (review thread P2). Multiple concurrent aborts of the SAME turn (distinct
|
|
2133
|
+
// idempotency keys, same scope) are all admitted and all await the ONE
|
|
2134
|
+
// agent_end the turn emits, so every waiter is registered — a single slot
|
|
2135
|
+
// would resolve only the latest and leave the earlier abort to record a
|
|
2136
|
+
// false negative (review thread P2).
|
|
2137
|
+
const terminalPublication = Promise.withResolvers<boolean>();
|
|
2138
|
+
const removeTerminalPublicationWaiter = () => {
|
|
2139
|
+
const resolvers = terminalPublicationCapture?.resolvers;
|
|
2140
|
+
if (!resolvers) return;
|
|
2141
|
+
const index = resolvers.indexOf(terminalPublication.resolve);
|
|
2142
|
+
if (index >= 0) resolvers.splice(index, 1);
|
|
2143
|
+
};
|
|
2144
|
+
if (terminalPublicationCapture) {
|
|
2145
|
+
if (!terminalPublicationCapture.resolvers) terminalPublicationCapture.resolvers = [];
|
|
2146
|
+
terminalPublicationCapture.resolvers.push(terminalPublication.resolve);
|
|
2147
|
+
}
|
|
2148
|
+
let proof: { status: string; terminalScope?: unknown };
|
|
2149
|
+
try {
|
|
2150
|
+
proof = await terminalAbortSeams.abortPromptAndWaitWithTerminal(handle, {
|
|
2151
|
+
graceMs: 10_000,
|
|
2152
|
+
terminal: {
|
|
2153
|
+
scope,
|
|
2154
|
+
expectedEpoch: epoch,
|
|
2155
|
+
...(steeringSnapshotToken !== undefined ? { steeringSnapshotToken } : {}),
|
|
2156
|
+
},
|
|
2157
|
+
});
|
|
2158
|
+
} catch {
|
|
2159
|
+
proof = { status: "unfenced" };
|
|
2160
|
+
}
|
|
2161
|
+
steeringSnapshotConsumed = proof.status === "settled";
|
|
2162
|
+
if (proof.status !== "settled" || proof.terminalScope === undefined) {
|
|
2163
|
+
removeTerminalPublicationWaiter();
|
|
2164
|
+
const result = {
|
|
2165
|
+
ok: true,
|
|
2166
|
+
selection: scope,
|
|
2167
|
+
turn: "uncertain",
|
|
2168
|
+
ownedWork: scope === "turn" ? "left_running" : "uncertain",
|
|
2169
|
+
automaticDelivery: scope === "turn" ? "enabled" : "none",
|
|
2170
|
+
resumeOnOwnedCompletion: scope === "turn",
|
|
2171
|
+
reason: "worker_unsettled",
|
|
2172
|
+
};
|
|
2173
|
+
const workerUnsettledPayloadHash = hashResult(result);
|
|
2174
|
+
await transactBoundedTerminalScopes(scopes =>
|
|
2175
|
+
scopes.map(record =>
|
|
2176
|
+
(keyHash
|
|
2177
|
+
? record.idempotencyKeyHash === keyHash
|
|
2178
|
+
: record.turnContinuationFence.abortedAttemptEpoch === epoch) &&
|
|
2179
|
+
record.turnDisposition === "pending"
|
|
2180
|
+
? {
|
|
2181
|
+
...record,
|
|
2182
|
+
turnDisposition: "uncertain",
|
|
2183
|
+
ownedWorkDisposition: "uncertain",
|
|
2184
|
+
responsePayloadHash: workerUnsettledPayloadHash,
|
|
2185
|
+
replayPayloadHash: replayShapedHash(record, result, workerUnsettledPayloadHash),
|
|
2186
|
+
terminalAt: Date.now(),
|
|
2187
|
+
}
|
|
2188
|
+
: record,
|
|
2189
|
+
),
|
|
2190
|
+
);
|
|
2191
|
+
return result;
|
|
2192
|
+
}
|
|
2193
|
+
// scope:"owned" must generation-verify and CANCEL the exact owned work
|
|
2194
|
+
// before reporting it stopped: abortPromptAndWaitWithTerminal only aborts
|
|
2195
|
+
// the foreground run and registers the disabled-delivery scope — a
|
|
2196
|
+
// background Bash/task/detached subagent would otherwise keep running
|
|
2197
|
+
// while the client receives stopped_owned (review thread P1).
|
|
2198
|
+
const ownedStopped = true;
|
|
2199
|
+
if (scope === "owned") {
|
|
2200
|
+
const terminalScope = proof.terminalScope as
|
|
2201
|
+
| { abortedAttemptEpoch?: number; lineageIdHash?: string }
|
|
2202
|
+
| undefined;
|
|
2203
|
+
const failOwnedUncertain = async (): Promise<unknown> => {
|
|
2204
|
+
removeTerminalPublicationWaiter();
|
|
2205
|
+
const result = {
|
|
2206
|
+
ok: true,
|
|
2207
|
+
selection: scope,
|
|
2208
|
+
turn: "uncertain",
|
|
2209
|
+
ownedWork: "uncertain",
|
|
2210
|
+
automaticDelivery: "none",
|
|
2211
|
+
resumeOnOwnedCompletion: false,
|
|
2212
|
+
reason: "owned_unsettled",
|
|
2213
|
+
};
|
|
2214
|
+
const ownedUnsettledPayloadHash = hashResult(result);
|
|
2215
|
+
await transactBoundedTerminalScopes(scopes =>
|
|
2216
|
+
scopes.map(record =>
|
|
2217
|
+
(keyHash
|
|
2218
|
+
? record.idempotencyKeyHash === keyHash
|
|
2219
|
+
: record.turnContinuationFence.abortedAttemptEpoch === epoch) &&
|
|
2220
|
+
record.turnDisposition === "pending"
|
|
2221
|
+
? {
|
|
2222
|
+
...record,
|
|
2223
|
+
turnDisposition: "uncertain",
|
|
2224
|
+
ownedWorkDisposition: "uncertain",
|
|
2225
|
+
responsePayloadHash: ownedUnsettledPayloadHash,
|
|
2226
|
+
replayPayloadHash: replayShapedHash(record, result, ownedUnsettledPayloadHash),
|
|
2227
|
+
terminalAt: Date.now(),
|
|
2228
|
+
}
|
|
2229
|
+
: record,
|
|
2230
|
+
),
|
|
2231
|
+
);
|
|
2232
|
+
return result;
|
|
2233
|
+
};
|
|
2234
|
+
if (
|
|
2235
|
+
!terminalScope ||
|
|
2236
|
+
terminalScope.abortedAttemptEpoch === undefined ||
|
|
2237
|
+
!terminalScope.lineageIdHash ||
|
|
2238
|
+
isOwnedAttemptRegistrationIncomplete(terminalScope.lineageIdHash, terminalScope.abortedAttemptEpoch)
|
|
2239
|
+
) {
|
|
2240
|
+
// The attempt's registration set may be KNOWN incomplete (registry
|
|
2241
|
+
// saturation or an evicted in-flight binding): never claim
|
|
2242
|
+
// stopped_owned over an incomplete causal set.
|
|
2243
|
+
return await failOwnedUncertain();
|
|
2244
|
+
}
|
|
2245
|
+
const exactJobs = findOwnedRegistrationsForTurn(
|
|
2246
|
+
terminalScope.lineageIdHash,
|
|
2247
|
+
terminalScope.abortedAttemptEpoch,
|
|
2248
|
+
);
|
|
2249
|
+
if (exactJobs.length > 0) {
|
|
2250
|
+
// Resolve the manager from the ABORTING ENDPOINT captured on the
|
|
2251
|
+
// registrations — never the process-global last-created session,
|
|
2252
|
+
// which could cancel a foreign same-id job and report stopped_owned
|
|
2253
|
+
// while the aborting session's job keeps running (review thread P1).
|
|
2254
|
+
const endpointId = exactJobs[0]?.endpointId;
|
|
2255
|
+
const manager = AsyncJobManager.forEndpoint(endpointId) ?? AsyncJobManager.instance();
|
|
2256
|
+
if (!manager || (await settleOwnedWork(manager, exactJobs, 500)) !== "stopped") {
|
|
2257
|
+
return await failOwnedUncertain();
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
// The worker settled; await the correlated agent_end publication for a
|
|
2262
|
+
// bounded window and persist the OBSERVED result (review thread P2). A
|
|
2263
|
+
// publication that never lands (lifecycle listener absent, still
|
|
2264
|
+
// pending, or failed) yields observed=false — the durable row never
|
|
2265
|
+
// claims a terminal event reached clients unless it was actually
|
|
2266
|
+
// published.
|
|
2267
|
+
const observed = await Promise.race([
|
|
2268
|
+
terminalPublication.promise,
|
|
2269
|
+
Bun.sleep(SDK_ONLY_TERMINAL_PUBLICATION_WAIT_MS).then(() => false as const),
|
|
2270
|
+
]);
|
|
2271
|
+
removeTerminalPublicationWaiter();
|
|
2272
|
+
const terminalPublished = observed === true;
|
|
2273
|
+
const result = {
|
|
2274
|
+
ok: true,
|
|
2275
|
+
selection: scope,
|
|
2276
|
+
turn: "stopped",
|
|
2277
|
+
...(scope === "turn"
|
|
2278
|
+
? { ownedWork: "left_running", automaticDelivery: "enabled", resumeOnOwnedCompletion: true }
|
|
2279
|
+
: {
|
|
2280
|
+
ownedWork: ownedStopped ? "stopped" : "uncertain",
|
|
2281
|
+
automaticDelivery: "none",
|
|
2282
|
+
resumeOnOwnedCompletion: false,
|
|
2283
|
+
}),
|
|
2284
|
+
};
|
|
2285
|
+
const payloadHash = crypto.createHash("sha256").update(JSON.stringify(result)).digest("hex");
|
|
2286
|
+
await transactBoundedTerminalScopes(scopes =>
|
|
2287
|
+
scopes.map(record =>
|
|
2288
|
+
(keyHash
|
|
2289
|
+
? record.idempotencyKeyHash === keyHash
|
|
2290
|
+
: record.turnContinuationFence.abortedAttemptEpoch === epoch) && record.turnDisposition === "pending"
|
|
2291
|
+
? {
|
|
2292
|
+
...record,
|
|
2293
|
+
turnDisposition: "stopped",
|
|
2294
|
+
terminalPublished,
|
|
2295
|
+
ownedWorkDisposition:
|
|
2296
|
+
scope === "turn" ? "left_running" : ownedStopped ? "stopped" : "uncertain",
|
|
2297
|
+
responsePayloadHash: payloadHash,
|
|
2298
|
+
// The replay envelope carries the POST-CAS publication
|
|
2299
|
+
// flag; the replay-shaped hash must be computed from the
|
|
2300
|
+
// updated row or a written replay could never match it
|
|
2301
|
+
// (review thread P2).
|
|
2302
|
+
replayPayloadHash: replayShapedHash({ ...record, terminalPublished }, result, payloadHash),
|
|
2303
|
+
terminalAt: Date.now(),
|
|
2304
|
+
}
|
|
2305
|
+
: record,
|
|
2306
|
+
),
|
|
2307
|
+
);
|
|
2308
|
+
return result;
|
|
2309
|
+
} finally {
|
|
2310
|
+
// A replay-only abort (or any pre-settlement failure) never consumed its
|
|
2311
|
+
// snapshot: discard it so a later real abort cannot consume the stale
|
|
2312
|
+
// entry and treat steering admitted since the replay as post-abort
|
|
2313
|
+
// (review thread P1).
|
|
2314
|
+
if (steeringSnapshotToken !== undefined && !steeringSnapshotConsumed) {
|
|
2315
|
+
terminalAbortSeams?.discardTerminalAbortSteeringSnapshot?.(steeringSnapshotToken);
|
|
2316
|
+
}
|
|
1149
2317
|
}
|
|
1150
2318
|
};
|
|
1151
2319
|
return {
|
|
1152
|
-
authorizeElevationClaim: (sdkId, input, capability) =>
|
|
1153
|
-
elevationAuthorityToken !== undefined &&
|
|
1154
|
-
verifyElevationCapability(elevationAuthorityToken, capability, sdkId, input),
|
|
1155
2320
|
prompt: async (text, images, clientRef) =>
|
|
1156
|
-
submit("prompt", clientRef, options =>
|
|
2321
|
+
submit("prompt", clientRef, ({ queuedAtDispatch, ...options }) =>
|
|
1157
2322
|
api.sendUserMessage(
|
|
1158
2323
|
typeof images === "undefined" ? text : ([{ type: "text", text }, ...(images as never[])] as never),
|
|
1159
|
-
options,
|
|
2324
|
+
queuedAtDispatch ? { ...options, queuedAtDispatch: true } : options,
|
|
1160
2325
|
),
|
|
1161
2326
|
),
|
|
1162
|
-
steer: async text => {
|
|
1163
|
-
|
|
1164
|
-
|
|
2327
|
+
steer: async (text, clientRef) => {
|
|
2328
|
+
const retainedClientRef = normalizeClientRef(clientRef);
|
|
2329
|
+
if (retainedClientRef === undefined) {
|
|
2330
|
+
const correlation = newCorrelation();
|
|
2331
|
+
await api.sendUserMessage(text, { deliverAs: "steer" });
|
|
2332
|
+
return { accepted: true, ...correlation };
|
|
2333
|
+
}
|
|
2334
|
+
const durable = steerReconciliation;
|
|
2335
|
+
const reservation = await durable.reserveSteer(retainedClientRef, text);
|
|
2336
|
+
if (reservation.replay) return { accepted: reservation.result.status === "accepted", ...reservation.result };
|
|
2337
|
+
try {
|
|
2338
|
+
await api.sendUserMessage(text, { deliverAs: "steer" });
|
|
2339
|
+
return { accepted: true, ...(await durable.settleSteer(retainedClientRef, "accepted")) };
|
|
2340
|
+
} catch (error) {
|
|
2341
|
+
return { accepted: false, ...(await durable.settleSteer(retainedClientRef, "rejected", error)) };
|
|
2342
|
+
}
|
|
1165
2343
|
},
|
|
1166
2344
|
followUp: async text =>
|
|
1167
2345
|
submit(
|
|
@@ -1170,14 +2348,16 @@ function createControlSurface(
|
|
|
1170
2348
|
options => api.sendUserMessage(text, { ...options, deliverAs: "followUp" }),
|
|
1171
2349
|
undefined,
|
|
1172
2350
|
false,
|
|
2351
|
+
// Follow-ups never start inline; ownership correlates at promotion.
|
|
1173
2352
|
true,
|
|
1174
2353
|
),
|
|
1175
2354
|
abort: () => {
|
|
1176
2355
|
ctx.abort();
|
|
1177
2356
|
return { aborted: true };
|
|
1178
2357
|
},
|
|
2358
|
+
abortTerminal: terminalAbort,
|
|
1179
2359
|
abortAndPrompt: async text => {
|
|
1180
|
-
ctx.abort();
|
|
2360
|
+
await ctx.abort();
|
|
1181
2361
|
return await submit("prompt", undefined, options => api.sendUserMessage(text, options));
|
|
1182
2362
|
},
|
|
1183
2363
|
answerAsk: unavailable("ask.answer"),
|
|
@@ -1271,7 +2451,11 @@ function createControlSurface(
|
|
|
1271
2451
|
newSession: () => typed("session.new"),
|
|
1272
2452
|
forkSession: () => typed("session.fork"),
|
|
1273
2453
|
resumeSession: id => typed("session.resume", { id }),
|
|
1274
|
-
closeSession:
|
|
2454
|
+
closeSession: capability =>
|
|
2455
|
+
typed(
|
|
2456
|
+
"session.close",
|
|
2457
|
+
capability === undefined ? {} : { [BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD]: capability },
|
|
2458
|
+
),
|
|
1275
2459
|
switchSession: id => typed("session.switch", { id }),
|
|
1276
2460
|
branchSession: entryId => typed("session.branch", { entryId }),
|
|
1277
2461
|
renameSession: name => typed("session.rename", { name }),
|
|
@@ -1377,25 +2561,108 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
1377
2561
|
revisions: RevisionStore;
|
|
1378
2562
|
cursors: CursorRegistry;
|
|
1379
2563
|
reconciliation: InvocationReconciliation;
|
|
1380
|
-
|
|
2564
|
+
steerReconciliation: KindAwareReconciliation;
|
|
2565
|
+
deadlineManager: PromptDeadlineManager;
|
|
2566
|
+
pending: Array<{
|
|
2567
|
+
kind: InvocationKind;
|
|
2568
|
+
correlation: InvocationCorrelation;
|
|
2569
|
+
connectionId: string | undefined;
|
|
2570
|
+
}>;
|
|
1381
2571
|
registerBroker: () => Promise<void>;
|
|
1382
2572
|
fenceGateResolutions: () => void;
|
|
1383
2573
|
waitForGateResolutionQuiescence: () => Promise<void>;
|
|
1384
2574
|
activeInvocation?: { kind: InvocationKind; correlation: InvocationCorrelation };
|
|
2575
|
+
drainedInvocations?: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }>;
|
|
1385
2576
|
disposeGate?: () => void;
|
|
1386
2577
|
}
|
|
1387
2578
|
| undefined;
|
|
2579
|
+
// Shared with the control surface's terminal abort: the correlated
|
|
2580
|
+
// agent_end publication capture (AC 19). terminalAbort installs a fresh
|
|
2581
|
+
// resolver before settling the abort; emitLifecycle resolves it with the
|
|
2582
|
+
// OBSERVED publication result when the aborted run's lifecycle event
|
|
2583
|
+
// lands, so the durable stopped row never claims terminalPublished without
|
|
2584
|
+
// observing it (review thread P2). Single slot: only the abort that
|
|
2585
|
+
// reaches the stopped path awaits it; a concurrent abort for the same turn
|
|
2586
|
+
// is settled by the durable marker transaction instead.
|
|
2587
|
+
const terminalPublicationCapture: { resolvers?: Array<(observed: boolean) => void> } = {};
|
|
2588
|
+
// Shared with the control surface: the SDK connection owning the currently
|
|
2589
|
+
// active prompt/skill turn. Cleared at every agent_end (terminal lifecycle
|
|
2590
|
+
// boundary) so a stale owner never authorizes an abort against a later
|
|
2591
|
+
// turn it did not submit, and never set for agent-initiated turns (review
|
|
2592
|
+
// thread P1).
|
|
2593
|
+
const activePromptOwnerHolder: { connectionIds?: Set<string> } = {};
|
|
1388
2594
|
const emitLifecycle = async (type: "agent_start" | "agent_end", ctx: ExtensionContext): Promise<void> => {
|
|
1389
2595
|
const current = active;
|
|
1390
2596
|
if (!current) return;
|
|
1391
|
-
if (type === "agent_start")
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
2597
|
+
if (type === "agent_start") {
|
|
2598
|
+
// Drain EVERY entry admitted for this run: a continuation may promote
|
|
2599
|
+
// several follow-ups (each with its own requester correlation) into one
|
|
2600
|
+
// run, and each submitting connection must be able to terminal-abort it.
|
|
2601
|
+
// Entries are only created for submissions that actually start their own
|
|
2602
|
+
// turn (queued-while-streaming submissions never push), so a mid-prompt
|
|
2603
|
+
// continuation agent_start with an empty queue leaves the current owner
|
|
2604
|
+
// untouched (review thread P1).
|
|
2605
|
+
const drained = current.pending.splice(0);
|
|
2606
|
+
current.activeInvocation = drained[0];
|
|
2607
|
+
if (drained.length > 0) {
|
|
2608
|
+
const owners = new Set<string>();
|
|
2609
|
+
for (const entry of drained) if (entry.connectionId !== undefined) owners.add(entry.connectionId);
|
|
2610
|
+
activePromptOwnerHolder.connectionIds = owners;
|
|
2611
|
+
// A single run may drain several follow-ups promoted together; each
|
|
2612
|
+
// has its own durable record that must reach terminal state, so keep
|
|
2613
|
+
// the full batch for the transition pass below (review thread P1).
|
|
2614
|
+
current.drainedInvocations = drained.map(({ kind, correlation }) => ({ kind, correlation }));
|
|
2615
|
+
}
|
|
2616
|
+
if (current.activeInvocation?.kind === "prompt") {
|
|
2617
|
+
current.deadlineManager.onAccepted(current.activeInvocation.correlation);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
// Observe whether the lifecycle publication actually landed: a terminal
|
|
2621
|
+
// abort awaits this result so its durable row only claims
|
|
2622
|
+
// terminalPublished when the correlated agent_end event reached the
|
|
2623
|
+
// ring/broadcast (review thread P2). Reconciliation or event failure is
|
|
2624
|
+
// recorded as observed=false, never rethrown into the api handler.
|
|
2625
|
+
let observed = true;
|
|
2626
|
+
try {
|
|
2627
|
+
const transitions =
|
|
2628
|
+
current.drainedInvocations && current.drainedInvocations.length > 0
|
|
2629
|
+
? current.drainedInvocations
|
|
2630
|
+
: current.activeInvocation
|
|
2631
|
+
? [current.activeInvocation]
|
|
2632
|
+
: [];
|
|
2633
|
+
for (const invocation of transitions) {
|
|
2634
|
+
await current.reconciliation.noteTransition(invocation.kind, invocation.correlation, { type } as never);
|
|
2635
|
+
if ((type as string) === "agent_end" || (type as string) === "agent_failed") {
|
|
2636
|
+
if (invocation.kind === "prompt") current.deadlineManager.clear(invocation.correlation);
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
current.runtime.emitEvent({ type, sessionId: ctx.sessionManager.getSessionId() });
|
|
2640
|
+
} catch {
|
|
2641
|
+
observed = false;
|
|
2642
|
+
}
|
|
2643
|
+
if (type === "agent_end") {
|
|
2644
|
+
if (current.activeInvocation?.kind === "prompt") {
|
|
2645
|
+
current.deadlineManager.clear(current.activeInvocation.correlation);
|
|
2646
|
+
} else if (current.drainedInvocations) {
|
|
2647
|
+
for (const inv of current.drainedInvocations)
|
|
2648
|
+
if (inv.kind === "prompt") current.deadlineManager.clear(inv.correlation);
|
|
2649
|
+
}
|
|
2650
|
+
current.activeInvocation = undefined;
|
|
2651
|
+
current.drainedInvocations = undefined;
|
|
2652
|
+
// The turn is over: no connection owns it anymore. Clearing here means
|
|
2653
|
+
// an abort against a later agent-initiated turn (monitor/cron
|
|
2654
|
+
// follow-up) finds no owner and fails closed, instead of letting the
|
|
2655
|
+
// previous prompt's owner stop a turn it did not submit (review
|
|
2656
|
+
// thread P1).
|
|
2657
|
+
activePromptOwnerHolder.connectionIds = undefined;
|
|
2658
|
+
// Resolve EVERY concurrent waiter for the aborted turn: the turn emits
|
|
2659
|
+
// exactly one agent_end, and each admitted abort of it must observe the
|
|
2660
|
+
// same publication result rather than a single latest-wins slot (review
|
|
2661
|
+
// thread P2).
|
|
2662
|
+
const resolvers = terminalPublicationCapture.resolvers;
|
|
2663
|
+
terminalPublicationCapture.resolvers = undefined;
|
|
2664
|
+
for (const resolve of resolvers ?? []) resolve(observed);
|
|
2665
|
+
}
|
|
1399
2666
|
};
|
|
1400
2667
|
api.on("agent_start", async (_event, ctx) => await emitLifecycle("agent_start", ctx));
|
|
1401
2668
|
api.on("agent_end", async (_event, ctx) => await emitLifecycle("agent_end", ctx));
|
|
@@ -1408,6 +2675,18 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
1408
2675
|
api.on("turn_end", (_event, ctx) =>
|
|
1409
2676
|
active?.runtime.emitEvent({ type: "turn_end", sessionId: ctx.sessionManager.getSessionId() }),
|
|
1410
2677
|
);
|
|
2678
|
+
api.on("tool_execution_start", async (_event, ctx) => {
|
|
2679
|
+
const current = active;
|
|
2680
|
+
if (current?.activeInvocation?.kind !== "prompt") return;
|
|
2681
|
+
current.deadlineManager.onAttributableEvent(current.activeInvocation.correlation, "tool_execution_start");
|
|
2682
|
+
void ctx;
|
|
2683
|
+
});
|
|
2684
|
+
api.on("tool_execution_end", async (_event, ctx) => {
|
|
2685
|
+
const current = active;
|
|
2686
|
+
if (current?.activeInvocation?.kind !== "prompt") return;
|
|
2687
|
+
current.deadlineManager.onAttributableEvent(current.activeInvocation.correlation, "tool_execution_end");
|
|
2688
|
+
void ctx;
|
|
2689
|
+
});
|
|
1411
2690
|
const errorCode = (error: unknown): string | undefined =>
|
|
1412
2691
|
typeof error === "object" &&
|
|
1413
2692
|
error !== null &&
|
|
@@ -1420,23 +2699,36 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
1420
2699
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
1421
2700
|
const stateRoot = path.join(ctx.cwd, ".gjc", "state");
|
|
1422
2701
|
const token = crypto.randomBytes(24).toString("base64url");
|
|
1423
|
-
const elevationAuthorityToken = crypto.randomBytes(32).toString("base64url");
|
|
1424
|
-
const elevationAuthorityFile = elevationAuthorityPath(stateRoot, sessionId);
|
|
1425
|
-
await fs.mkdir(path.dirname(elevationAuthorityFile), { recursive: true, mode: 0o700 });
|
|
1426
|
-
await Bun.write(elevationAuthorityFile, `${JSON.stringify({ version: 1, token: elevationAuthorityToken })}\n`);
|
|
1427
|
-
await fs.chmod(elevationAuthorityFile, 0o600);
|
|
1428
2702
|
const transport = await options.createTransport({ sessionId, stateRoot, token });
|
|
1429
2703
|
const revisions = new RevisionStore(sessionId, Date.now, { storageDir: stateRoot });
|
|
1430
2704
|
const cursors = new CursorRegistry(token, revisions);
|
|
1431
|
-
const
|
|
2705
|
+
const reconciliationStore = options.terminalAbortSeams?.getReconciliationStore?.();
|
|
2706
|
+
const reconciliation = createInvocationReconciliation({ store: reconciliationStore });
|
|
1432
2707
|
await reconciliation.hydrate();
|
|
1433
|
-
const
|
|
2708
|
+
const sessionFile =
|
|
2709
|
+
(typeof ctx.sessionManager.getSessionFile === "function" ? ctx.sessionManager.getSessionFile() : undefined) ??
|
|
2710
|
+
resolveReconciliationSessionFile(undefined, stateRoot, sessionId);
|
|
2711
|
+
const steerReconciliation = createKindAwareReconciliation({
|
|
2712
|
+
store: createReconciliationStore({ sessionFile, sessionId }),
|
|
2713
|
+
});
|
|
2714
|
+
await steerReconciliation.hydrateFromStore();
|
|
2715
|
+
const deadlineManager = new PromptDeadlineManager({
|
|
2716
|
+
reconciliation,
|
|
2717
|
+
getLeaseMs: () => {
|
|
2718
|
+
const v = options.settings?.get("sdk.promptDeadlineMs" as never) as number | undefined;
|
|
2719
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 1_800_000;
|
|
2720
|
+
},
|
|
2721
|
+
getMaxMs: () => {
|
|
2722
|
+
const v = options.settings?.get("sdk.promptMaxRuntimeMs" as never) as number | undefined;
|
|
2723
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 21_600_000;
|
|
2724
|
+
},
|
|
2725
|
+
});
|
|
2726
|
+
const pending: Array<{
|
|
2727
|
+
kind: InvocationKind;
|
|
2728
|
+
correlation: InvocationCorrelation;
|
|
2729
|
+
connectionId: string | undefined;
|
|
2730
|
+
}> = [];
|
|
1434
2731
|
const configRevision = { current: 0 };
|
|
1435
|
-
// The event-ring watermark lives on the runtime host, which is constructed
|
|
1436
|
-
// after the surface factory; the lazy source indirection lets the surface
|
|
1437
|
-
// read the live generation/seq once the runtime exists (Q30 atomic
|
|
1438
|
-
// capture, C9).
|
|
1439
|
-
let eventWatermarkSource: () => { generation: number; seq: number } = () => ({ generation: 0, seq: 0 });
|
|
1440
2732
|
let acceptingGateResolutions = true;
|
|
1441
2733
|
const inFlightGateResolutions = new Set<Promise<unknown>>();
|
|
1442
2734
|
const trackGateResolution = <T>(resolution: Promise<T>): Promise<T> => {
|
|
@@ -1446,8 +2738,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
1446
2738
|
};
|
|
1447
2739
|
const waitForGateResolutionQuiescence = async (): Promise<void> => {
|
|
1448
2740
|
const settled = Promise.allSettled(inFlightGateResolutions);
|
|
1449
|
-
const timeout = Bun.sleep(
|
|
1450
|
-
|
|
2741
|
+
const timeout = Bun.sleep(GATE_RESOLUTION_QUIESCENCE_MS).then(() => {
|
|
2742
|
+
logger.warn("SDK workflow gate resolution drain timed out; proceeding with uncertain outcomes.");
|
|
1451
2743
|
});
|
|
1452
2744
|
await Promise.race([settled, timeout]);
|
|
1453
2745
|
};
|
|
@@ -1457,23 +2749,47 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
1457
2749
|
api,
|
|
1458
2750
|
reconciliation,
|
|
1459
2751
|
turnResultLookup: selector => reconciliation.lookupResult(selector.kind, selector),
|
|
2752
|
+
steerStatusLookup: selector => steerReconciliation.lookupSteer(selector),
|
|
1460
2753
|
configOverrides: options.configOverrides,
|
|
1461
2754
|
settings: options.settings,
|
|
1462
|
-
getEventWatermark: () => eventWatermarkSource(),
|
|
1463
2755
|
});
|
|
1464
2756
|
const queryHandlers = new QueryHandlers(surfaceFactory.query, sessionId, revisions, cursors);
|
|
1465
2757
|
const controlSurface = createControlSurface(
|
|
1466
2758
|
ctx,
|
|
1467
2759
|
api,
|
|
1468
2760
|
reconciliation,
|
|
1469
|
-
(kind, correlation) => {
|
|
1470
|
-
pending
|
|
2761
|
+
(kind, correlation, connectionId, startsOwnTurn) => {
|
|
2762
|
+
// Only submissions that start their OWN turn get a pending entry: a
|
|
2763
|
+
// steering-queued submission consumed inside the current run never
|
|
2764
|
+
// emits the agent_start that would consume the entry, so leaving it
|
|
2765
|
+
// queued would assign its stale connection as owner of a later
|
|
2766
|
+
// agent-initiated turn (review thread P1).
|
|
2767
|
+
if (startsOwnTurn) pending.push({ kind, correlation, connectionId });
|
|
2768
|
+
},
|
|
2769
|
+
steerReconciliation,
|
|
2770
|
+
(kind, correlation, connectionId) => {
|
|
2771
|
+
// A steering-queued submission PROMOTED to its own run (finished
|
|
2772
|
+
// prompt unwinding) starts with an empty pending queue at its
|
|
2773
|
+
// agent_start; create the entry at promotion so the submitting
|
|
2774
|
+
// connection owns that turn (review thread P2).
|
|
2775
|
+
pending.push({ kind, correlation, connectionId });
|
|
1471
2776
|
},
|
|
1472
2777
|
surfaceFactory.policy,
|
|
1473
2778
|
options.settings,
|
|
1474
2779
|
options.configOverrides,
|
|
1475
2780
|
configRevision,
|
|
1476
|
-
|
|
2781
|
+
options.terminalAbortSeams,
|
|
2782
|
+
terminalPublicationCapture,
|
|
2783
|
+
activePromptOwnerHolder,
|
|
2784
|
+
correlation => {
|
|
2785
|
+
// An accepted submission that settles WITHOUT starting (a rejection
|
|
2786
|
+
// after acceptance) must not leave its pending entry behind: remove
|
|
2787
|
+
// the matching correlation so a later agent-initiated turn never
|
|
2788
|
+
// inherits the failed submission's connection as owner (review
|
|
2789
|
+
// thread P1).
|
|
2790
|
+
const index = pending.findIndex(entry => entry.correlation === correlation);
|
|
2791
|
+
if (index >= 0) pending.splice(index, 1);
|
|
2792
|
+
},
|
|
1477
2793
|
() => acceptingGateResolutions,
|
|
1478
2794
|
trackGateResolution,
|
|
1479
2795
|
);
|
|
@@ -1544,24 +2860,23 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
1544
2860
|
};
|
|
1545
2861
|
runtime = new SessionSdkSessionRuntime({
|
|
1546
2862
|
transport,
|
|
1547
|
-
control: async (
|
|
2863
|
+
control: async (connectionId, frame) => {
|
|
2864
|
+
options.onFrameAdmitted?.();
|
|
1548
2865
|
const request = controlRequestFromFrame(frame as Record<string, unknown>);
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
2866
|
+
// Scope preflight cancellation to the REQUESTING SDK connection: the
|
|
2867
|
+
// requester preflight buckets are keyed by this context, so a
|
|
2868
|
+
// terminal abort from one client must never cancel another client's
|
|
2869
|
+
// pending preflight (review thread P1).
|
|
2870
|
+
return sdkControlRequesterContext.run(connectionId, () =>
|
|
2871
|
+
dispatchControl(
|
|
2872
|
+
controlSurface,
|
|
2873
|
+
OPERATIONS.find(operation => operation.kind === "control" && operation.sdkId === request.operation),
|
|
2874
|
+
request,
|
|
2875
|
+
),
|
|
1553
2876
|
);
|
|
1554
2877
|
},
|
|
1555
2878
|
query: async (connectionId, frame) => {
|
|
1556
2879
|
const request = frame as Record<string, unknown>;
|
|
1557
|
-
// D1: a non-string top-level cursor is rejected, never silently
|
|
1558
|
-
// dropped into a fresh-snapshot query.
|
|
1559
|
-
if (request.cursor !== undefined && typeof request.cursor !== "string")
|
|
1560
|
-
return {
|
|
1561
|
-
id: typeof request.id === "string" ? request.id : undefined,
|
|
1562
|
-
ok: false,
|
|
1563
|
-
error: { code: "invalid_input", message: "cursor must be a non-empty string" },
|
|
1564
|
-
};
|
|
1565
2880
|
return queryHandlers.dispatch({
|
|
1566
2881
|
id: typeof request.id === "string" ? request.id : undefined,
|
|
1567
2882
|
query: typeof request.query === "string" ? request.query : "",
|
|
@@ -1574,18 +2889,86 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
1574
2889
|
});
|
|
1575
2890
|
},
|
|
1576
2891
|
onRequest: options.onSdkRequest,
|
|
2892
|
+
onControlResponseDelivery: async (_connectionId, request, response, outcome) => {
|
|
2893
|
+
// Strict key validation: a malformed retry (e.g. numeric
|
|
2894
|
+
// idempotencyKey) rejected by dispatch must NEVER hash to the same
|
|
2895
|
+
// key as a legitimate stored string and advance its response state
|
|
2896
|
+
// (review thread P2).
|
|
2897
|
+
if (
|
|
2898
|
+
!reconciliationStore ||
|
|
2899
|
+
request.operation !== "turn.abort" ||
|
|
2900
|
+
typeof request.idempotencyKey !== "string" ||
|
|
2901
|
+
typeof request.input !== "object" ||
|
|
2902
|
+
request.input === null ||
|
|
2903
|
+
(request.input as { mode?: unknown }).mode !== "terminal"
|
|
2904
|
+
)
|
|
2905
|
+
return;
|
|
2906
|
+
const input = request.input as Record<string, unknown>;
|
|
2907
|
+
const mode = input.mode;
|
|
2908
|
+
const rawScope = input.scope;
|
|
2909
|
+
if (mode !== "terminal") return;
|
|
2910
|
+
if (rawScope !== undefined && rawScope !== "turn" && rawScope !== "owned") return;
|
|
2911
|
+
// A malformed retry (e.g. {mode:"terminal", scope:"turn", extra:true})
|
|
2912
|
+
// rejected by dispatch must never hash as the valid turn input and
|
|
2913
|
+
// advance the legitimate stored row (review thread P2).
|
|
2914
|
+
for (const key of Object.keys(input)) if (key !== "mode" && key !== "scope") return;
|
|
2915
|
+
const scopeInput = rawScope === "owned" ? "owned" : "turn";
|
|
2916
|
+
const keyHash = crypto.createHash("sha256").update(String(request.idempotencyKey)).digest("hex");
|
|
2917
|
+
const inputHash = crypto
|
|
2918
|
+
.createHash("sha256")
|
|
2919
|
+
.update(JSON.stringify({ mode: "terminal", scope: scopeInput }))
|
|
2920
|
+
.digest("hex");
|
|
2921
|
+
// Hash the ACTUAL written response payload: the durable state may only
|
|
2922
|
+
// advance when the written response corresponds to the row's payload.
|
|
2923
|
+
// When more than 256 concurrent requests evict an in-flight abort from
|
|
2924
|
+
// the dispatch cache, a same-key retry can return pending_replay while
|
|
2925
|
+
// the original is still terminalizing — matching only key+input would
|
|
2926
|
+
// mark the original marker sent for the retry's uncertainty response,
|
|
2927
|
+
// and the original's later stopped CAS would replace the payload hash
|
|
2928
|
+
// without resetting the state, making durable replay claim the stopped
|
|
2929
|
+
// payload was sent when only the pending response was written (review
|
|
2930
|
+
// thread P2). A final non-pending row whose stored hash is the input
|
|
2931
|
+
// placeholder (no_effect/uncertain) still advances: its own response is
|
|
2932
|
+
// the only one written for it.
|
|
2933
|
+
const responsePayloadHash =
|
|
2934
|
+
response && typeof response === "object" && "result" in response
|
|
2935
|
+
? crypto
|
|
2936
|
+
.createHash("sha256")
|
|
2937
|
+
.update(JSON.stringify((response as { result: unknown }).result))
|
|
2938
|
+
.digest("hex")
|
|
2939
|
+
: undefined;
|
|
2940
|
+
// Require EXACT payload equality: finalization now stores the precise
|
|
2941
|
+
// final response hash for every disposition (including uncertainty and
|
|
2942
|
+
// no-effect), so a pending_replay retry whose payload differs can never
|
|
2943
|
+
// mark the durable row sent (review thread P2).
|
|
2944
|
+
const payloadMatches = (record: { responsePayloadHash?: string; replayPayloadHash?: string }) =>
|
|
2945
|
+
responsePayloadHash !== undefined &&
|
|
2946
|
+
(record.responsePayloadHash === responsePayloadHash || record.replayPayloadHash === responsePayloadHash);
|
|
2947
|
+
await reconciliationStore.transactTerminalState(state => ({
|
|
2948
|
+
scopes: state.scopes.map(record =>
|
|
2949
|
+
record.idempotencyKeyHash === keyHash &&
|
|
2950
|
+
record.idempotencyInputHash === inputHash &&
|
|
2951
|
+
record.responseState === "pending" &&
|
|
2952
|
+
payloadMatches(record)
|
|
2953
|
+
? { ...record, responseState: outcome === "written" ? "sent" : "failed" }
|
|
2954
|
+
: record,
|
|
2955
|
+
),
|
|
2956
|
+
keys: state.keys.map(record =>
|
|
2957
|
+
record.keyHash === keyHash &&
|
|
2958
|
+
record.inputHash === inputHash &&
|
|
2959
|
+
record.responseState === "pending" &&
|
|
2960
|
+
payloadMatches(record)
|
|
2961
|
+
? { ...record, responseState: outcome === "written" ? "sent" : "failed" }
|
|
2962
|
+
: record,
|
|
2963
|
+
),
|
|
2964
|
+
}));
|
|
2965
|
+
},
|
|
1577
2966
|
installProviderDefinitions,
|
|
1578
2967
|
onProviderDefinitionsRemoved: removeProviderDefinitions,
|
|
1579
2968
|
afterControlResponse: async (_connectionId, request, response) => {
|
|
1580
2969
|
if (request.operation === "session.close" && response.ok === true) ctx.shutdown();
|
|
1581
2970
|
},
|
|
1582
2971
|
});
|
|
1583
|
-
// The runtime host now owns the live event stream; point the surface's
|
|
1584
|
-
// checkpoint watermark at the real generation/seq (C9).
|
|
1585
|
-
eventWatermarkSource = () => ({
|
|
1586
|
-
generation: runtime.host.events.generation,
|
|
1587
|
-
seq: runtime.host.events.sequence,
|
|
1588
|
-
});
|
|
1589
2972
|
const disposeGate = ctx.workflowGate?.onGateEmitted?.(gate =>
|
|
1590
2973
|
runtime.emitEvent({ kind: "workflow_gate", payload: gate }),
|
|
1591
2974
|
);
|
|
@@ -1617,6 +3000,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
1617
3000
|
revisions,
|
|
1618
3001
|
cursors,
|
|
1619
3002
|
reconciliation,
|
|
3003
|
+
steerReconciliation,
|
|
3004
|
+
deadlineManager,
|
|
1620
3005
|
pending,
|
|
1621
3006
|
registerBroker,
|
|
1622
3007
|
fenceGateResolutions: () => {
|
|
@@ -1643,6 +3028,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
1643
3028
|
revisions,
|
|
1644
3029
|
cursors,
|
|
1645
3030
|
reconciliation,
|
|
3031
|
+
steerReconciliation,
|
|
3032
|
+
deadlineManager,
|
|
1646
3033
|
pending,
|
|
1647
3034
|
registerBroker,
|
|
1648
3035
|
fenceGateResolutions: () => {
|
|
@@ -1661,6 +3048,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
1661
3048
|
const stopActive = async (): Promise<void> => {
|
|
1662
3049
|
const current = active;
|
|
1663
3050
|
if (!current) return;
|
|
3051
|
+
current.deadlineManager.clearAll();
|
|
1664
3052
|
current.fenceGateResolutions();
|
|
1665
3053
|
try {
|
|
1666
3054
|
await current.waitForGateResolutionQuiescence();
|