@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
|
@@ -0,0 +1,1253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Private terminal-abort machinery for C04 `turn.abort` `mode:"terminal"`.
|
|
3
|
+
*
|
|
4
|
+
* Corrected semantics (approved plan, user-directed; see the plan's prominent
|
|
5
|
+
* design note): `scope:"turn"` stops the ROOT WORKER's current turn and blocks
|
|
6
|
+
* ONLY its own continuation routes (same-turn retry, TTSR/`agent.continue`,
|
|
7
|
+
* steering continuation, hidden-next-turn, maintenance/worker successor,
|
|
8
|
+
* accepted-pre-close same-attempt continuation). Left-running owned work
|
|
9
|
+
* (background Bash/task jobs, detached subagents) keeps running and its
|
|
10
|
+
* completions are DELIVERED NORMALLY through the existing
|
|
11
|
+
* YieldQueue -> `agent.followUp`/`agent.prompt` path so the root worker can
|
|
12
|
+
* resume with a fresh attempt. Owned delivery is intentionally NOT suppressed.
|
|
13
|
+
*
|
|
14
|
+
* The earlier stage-04 no-successor delivery fence was a misunderstanding and
|
|
15
|
+
* must not be reinstated under any name.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
18
|
+
|
|
19
|
+
/** Origin class assigned to every causal callback/queue entry before escape. */
|
|
20
|
+
export type DeliveryOrigin =
|
|
21
|
+
| Readonly<{
|
|
22
|
+
kind: "turn-continuation";
|
|
23
|
+
lineageIdHash: string;
|
|
24
|
+
attemptEpoch: number;
|
|
25
|
+
continuationId: string;
|
|
26
|
+
}>
|
|
27
|
+
| Readonly<{
|
|
28
|
+
kind: "owned-completion";
|
|
29
|
+
lineageIdHash: string;
|
|
30
|
+
attemptEpoch: number;
|
|
31
|
+
registration: TurnRegistrationKey;
|
|
32
|
+
}>
|
|
33
|
+
| Readonly<{ kind: "ordinary"; source: string }>;
|
|
34
|
+
|
|
35
|
+
/** Exact causal registration key bound before a job/subagent handle escapes. */
|
|
36
|
+
export interface TurnRegistrationKey {
|
|
37
|
+
/** Endpoint (top-level session) identity that owns this job: the registry is
|
|
38
|
+
* process-global and job ids/generations restart per AsyncJobManager, so
|
|
39
|
+
* two concurrent sessions both mint bg_1/job:1 — without the endpoint in
|
|
40
|
+
* the key the second registration would overwrite the first and an owned
|
|
41
|
+
* abort of the first turn could find an empty causal set (review thread
|
|
42
|
+
* P1). Absent (legacy/test fixtures) = a default scope. */
|
|
43
|
+
endpointId?: string;
|
|
44
|
+
endpointGeneration: number;
|
|
45
|
+
lineageIdHash: string;
|
|
46
|
+
promptAttemptEpoch: number;
|
|
47
|
+
jobId: string;
|
|
48
|
+
jobGeneration: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Per-completion delivery key: registration tuple plus entry identity. */
|
|
52
|
+
export type TurnDeliveryKey = TurnRegistrationKey & {
|
|
53
|
+
entryId: string;
|
|
54
|
+
progressSeq?: number;
|
|
55
|
+
};
|
|
56
|
+
/** Private origin envelope carried through the plain AgentMessage boundary. */
|
|
57
|
+
export interface OwnedCompletionEnvelope {
|
|
58
|
+
lineageIdHash: string;
|
|
59
|
+
promptAttemptEpoch: number;
|
|
60
|
+
/** Exact registered five-tuple so the final gate can validate source authority. */
|
|
61
|
+
registration: TurnRegistrationKey;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type TurnContinuationFenceState = "open" | "closing" | "closed" | "retained" | "released";
|
|
65
|
+
|
|
66
|
+
export type OwnedCompletionPolicy = "enabled" | "disabled";
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Continuation fence lifecycle: `open -> closing -> closed` happens
|
|
70
|
+
* synchronously before the first await that interrupts the root turn. Closing
|
|
71
|
+
* records exact continuation tombstones and invalidates ONLY continuation
|
|
72
|
+
* tokens; it never invalidates an owned-completion token, cancels a manager
|
|
73
|
+
* job, or creates a turn delivery receipt. `retained` keeps tombstones for
|
|
74
|
+
* restart/later-owned binding; `released` requires exact tokens gone, teardown
|
|
75
|
+
* with no live continuation, or bounded durable retention. Host response
|
|
76
|
+
* success/replay/retry never releases it.
|
|
77
|
+
*/
|
|
78
|
+
export interface TurnContinuationFence {
|
|
79
|
+
state: TurnContinuationFenceState;
|
|
80
|
+
lineageIdHash: string;
|
|
81
|
+
abortedAttemptEpoch: number;
|
|
82
|
+
terminalScopeId: string;
|
|
83
|
+
blockedContinuationIds: ReadonlySet<string>;
|
|
84
|
+
predecessorTombstones: ReadonlySet<string>;
|
|
85
|
+
ownedCompletionPolicy: OwnedCompletionPolicy;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The one gate consulted immediately before turn-origin continuation calls and
|
|
90
|
+
* owned-completion admission.
|
|
91
|
+
*
|
|
92
|
+
* `authorizeContinuation` denies any post-close same-turn continuation and
|
|
93
|
+
* allows only a call already linearized as a predecessor before close.
|
|
94
|
+
* `authorizeOwnedCompletion` does NOT consult the closed continuation state as
|
|
95
|
+
* a suppression flag; it validates exact source metadata and, when allowed,
|
|
96
|
+
* AgentSession allocates a FRESH attempt/lineage for the new turn.
|
|
97
|
+
*/
|
|
98
|
+
export interface TurnContinuationGate {
|
|
99
|
+
close(reason: "terminal-turn"): void;
|
|
100
|
+
authorizeContinuation(origin: DeliveryOrigin): "deny" | "allow-predecessor";
|
|
101
|
+
authorizeOwnedCompletion(origin: DeliveryOrigin): "allow-new-turn" | "deny";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type OwnedDeliverySettlementPath =
|
|
105
|
+
| "enqueue-acknowledged-return"
|
|
106
|
+
| "acknowledgeDeliveries-queue-purge"
|
|
107
|
+
| "delivery-loop-acknowledged-skip"
|
|
108
|
+
| "deliverDelivery-acknowledged-return"
|
|
109
|
+
| "terminal-wait-acknowledge-suppression-purge"
|
|
110
|
+
| "filtered-drain-post-selection-suppression";
|
|
111
|
+
|
|
112
|
+
/** Owned-scope-only settlement observer (never installed for turn scope). */
|
|
113
|
+
export type OwnedDeliverySettlementObserver = (event: {
|
|
114
|
+
key: TurnDeliveryKey;
|
|
115
|
+
path: OwnedDeliverySettlementPath;
|
|
116
|
+
action: "owned_settled" | "owned_absent";
|
|
117
|
+
}) => void;
|
|
118
|
+
|
|
119
|
+
/** Safe, bounded reasons surfaced on `terminal_uncertain` responses. */
|
|
120
|
+
export const TERMINAL_UNCERTAIN_REASONS = [
|
|
121
|
+
"persistence_unavailable",
|
|
122
|
+
"publication_failed",
|
|
123
|
+
"delivery_failed",
|
|
124
|
+
"owned_unsettled",
|
|
125
|
+
"worker_unsettled",
|
|
126
|
+
"unknown_origin",
|
|
127
|
+
"registration_authority_unavailable",
|
|
128
|
+
] as const;
|
|
129
|
+
export type TerminalUncertainReason = (typeof TERMINAL_UNCERTAIN_REASONS)[number];
|
|
130
|
+
|
|
131
|
+
export interface TerminalScopeDispositions {
|
|
132
|
+
selection: "turn" | "owned";
|
|
133
|
+
turnDisposition: "pending" | "stopped" | "uncertain";
|
|
134
|
+
ownedWorkDisposition: "not_requested" | "left_running" | "stopped" | "uncertain";
|
|
135
|
+
automaticDeliveryDisposition: "enabled" | "none";
|
|
136
|
+
resumeOnOwnedCompletion: boolean;
|
|
137
|
+
}
|
|
138
|
+
export interface ActiveTerminalScope {
|
|
139
|
+
scopeId: string;
|
|
140
|
+
lineageIdHash: string;
|
|
141
|
+
abortedAttemptEpoch: number;
|
|
142
|
+
gate: TurnContinuationGate;
|
|
143
|
+
fence: TurnContinuationFence;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const MAX_ACTIVE_TERMINAL_SCOPES = 1024;
|
|
147
|
+
const MAX_OWNED_REGISTRATIONS = 8192;
|
|
148
|
+
const MAX_RETAINED_OWNERSHIP_TUPLES = 2048;
|
|
149
|
+
const MAX_RETAINED_ATTEMPT_POLICIES = 2048;
|
|
150
|
+
const activeScopes = new Map<string, ActiveTerminalScope>();
|
|
151
|
+
/**
|
|
152
|
+
* Compact authorization evidence for registrations evicted by the 8,192 cap:
|
|
153
|
+
* a TERMINAL job may still have an owned-completion envelope queued but not
|
|
154
|
+
* yet consumed/acknowledged, and evicting its registration would remove the
|
|
155
|
+
* authorizeOwnedCompletion authority (the queued result would be denied after
|
|
156
|
+
* a scope:"turn" abort, losing the promised fresh completion). Evicted tuples
|
|
157
|
+
* are retained here (bounded, FIFO) and removed once the delivery settles
|
|
158
|
+
* (unregisterOwnedRegistration) — review thread P2.
|
|
159
|
+
*/
|
|
160
|
+
const retainedOwnershipTuples = new Map<string, TurnRegistrationKey>();
|
|
161
|
+
const activeScopeByAttempt = new Map<string, string>();
|
|
162
|
+
const ownedRegistrations = new Map<string, TurnRegistrationKey>();
|
|
163
|
+
/**
|
|
164
|
+
* Compact attempt-policy tombstones for scopes evicted by the cap: when a
|
|
165
|
+
* scope is evicted, its ownedCompletionPolicy is retained so a still-running
|
|
166
|
+
* owned completion from that attempt keeps classifying correctly (fresh
|
|
167
|
+
* resume for scope:"turn", drop for scope:"owned") instead of degrading to
|
|
168
|
+
* ordinary (review thread P2). Bounded; oldest tombstone evicted first.
|
|
169
|
+
*/
|
|
170
|
+
const retainedAttemptPolicies = new Map<string, { ownedCompletionPolicy: "enabled" | "disabled" }>();
|
|
171
|
+
/** Attempts whose owned-registration set is KNOWN incomplete (registry
|
|
172
|
+
* saturation skipped a registration under the 8192 cap). Bounded by the
|
|
173
|
+
* number of saturation events — fail-closed authority for those attempts.
|
|
174
|
+
*/
|
|
175
|
+
const incompleteOwnedAttempts = new Set<string>();
|
|
176
|
+
/** Evicted call key → its attempt marker + retained lineage until that tool call settles. */
|
|
177
|
+
const incompleteToolCallWindows = new Map<string, { incompleteKey: string; binding: LineageBinding }>();
|
|
178
|
+
/** Number of evicted tool-call windows still capable of registering per attempt. */
|
|
179
|
+
const incompleteAttemptWindowCounts = new Map<string, number>();
|
|
180
|
+
/**
|
|
181
|
+
* Saturation authority owner map (attempt key -> endpoint id): when the
|
|
182
|
+
* process-global registry is full with no evictable tuple, the skipped
|
|
183
|
+
* registration's attempt can never prove an exact causal set. The authority
|
|
184
|
+
* is retained PER AFFECTED ATTEMPT (lineage+epoch, unique per session) and
|
|
185
|
+
* retired when its endpoint's registrations are torn down — a transient
|
|
186
|
+
* backlog must not permanently disable owned aborts for unrelated sessions
|
|
187
|
+
* or endpoints (review thread P2).
|
|
188
|
+
*/
|
|
189
|
+
const saturatedAttemptEndpoints = new Map<string, string>();
|
|
190
|
+
/**
|
|
191
|
+
* EvictKeys whose TERMINAL tuple could not move into the retained evidence
|
|
192
|
+
* (its cap is full): the tuple stays in the LIVE map so authorizeOwnedCompletion
|
|
193
|
+
* still finds it, and the eviction scan skips these keys so a later
|
|
194
|
+
* registration does not re-evict the same protected tuple (review thread P2).
|
|
195
|
+
*/
|
|
196
|
+
const retentionBacklog = new Set<string>();
|
|
197
|
+
|
|
198
|
+
/** Register one active terminal scope (scopeId -> seam). Bounded; evicts oldest. */
|
|
199
|
+
export function registerTerminalScope(scope: ActiveTerminalScope): boolean {
|
|
200
|
+
if (activeScopes.size >= MAX_ACTIVE_TERMINAL_SCOPES) {
|
|
201
|
+
const oldest = activeScopes.keys().next().value;
|
|
202
|
+
if (oldest !== undefined) {
|
|
203
|
+
const evicted = activeScopes.get(oldest);
|
|
204
|
+
if (evicted) {
|
|
205
|
+
if (retainedAttemptPolicies.size >= MAX_RETAINED_ATTEMPT_POLICIES) {
|
|
206
|
+
// Evict only a tombstone whose attempt has NO remaining
|
|
207
|
+
// registrations (its owned completions have settled) — never
|
|
208
|
+
// by age alone while a registration could still classify and
|
|
209
|
+
// require the policy (review thread P2).
|
|
210
|
+
let evictPolicyKey: string | undefined;
|
|
211
|
+
for (const [candidateKey] of retainedAttemptPolicies) {
|
|
212
|
+
const separator = candidateKey.lastIndexOf("\u0000");
|
|
213
|
+
const lineage = candidateKey.slice(0, separator);
|
|
214
|
+
const epoch = Number(candidateKey.slice(separator + 1));
|
|
215
|
+
const hasRegistrations =
|
|
216
|
+
[...ownedRegistrations.values()].some(
|
|
217
|
+
reg => reg.lineageIdHash === lineage && reg.promptAttemptEpoch === epoch,
|
|
218
|
+
) ||
|
|
219
|
+
// A TERMINAL job awaiting delivery may have been
|
|
220
|
+
// evicted into the retained evidence; its attempt is
|
|
221
|
+
// NOT settled and its policy must survive (review P2).
|
|
222
|
+
[...retainedOwnershipTuples.values()].some(
|
|
223
|
+
reg => reg.lineageIdHash === lineage && reg.promptAttemptEpoch === epoch,
|
|
224
|
+
);
|
|
225
|
+
if (!hasRegistrations) {
|
|
226
|
+
evictPolicyKey = candidateKey;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (evictPolicyKey === undefined) {
|
|
231
|
+
// Every retained tombstone still has LIVE registrations:
|
|
232
|
+
// evicting the OLDEST ACTIVE scope would drop ITS policy
|
|
233
|
+
// while its work remains registered/classifiable, changing
|
|
234
|
+
// its later completion from fresh/drop to ordinary. FAIL
|
|
235
|
+
// the NEW scope admission instead — the oldest scope keeps
|
|
236
|
+
// its policy (review thread P2).
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
retainedAttemptPolicies.delete(evictPolicyKey);
|
|
240
|
+
}
|
|
241
|
+
retainedAttemptPolicies.set(`${evicted.lineageIdHash}\u0000${evicted.abortedAttemptEpoch}`, {
|
|
242
|
+
ownedCompletionPolicy: evicted.fence.ownedCompletionPolicy,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
unregisterTerminalScope(oldest);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
activeScopes.set(scope.scopeId, scope);
|
|
249
|
+
activeScopeByAttempt.set(`${scope.lineageIdHash}\u0000${scope.abortedAttemptEpoch}`, scope.scopeId);
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Look up the active terminal scope for an aborted attempt (exact lineage+epoch). */
|
|
254
|
+
export function lookupTerminalScope(lineageIdHash: string, attemptEpoch: number): ActiveTerminalScope | undefined {
|
|
255
|
+
const scopeId = activeScopeByAttempt.get(`${lineageIdHash}\u0000${attemptEpoch}`);
|
|
256
|
+
return scopeId === undefined ? undefined : activeScopes.get(scopeId);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function unregisterTerminalScope(scopeId: string): void {
|
|
260
|
+
const scope = activeScopes.get(scopeId);
|
|
261
|
+
if (!scope) return;
|
|
262
|
+
activeScopes.delete(scopeId);
|
|
263
|
+
activeScopeByAttempt.delete(`${scope.lineageIdHash}\u0000${scope.abortedAttemptEpoch}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Record an exact owned registration before its handle escapes (bounded). */
|
|
267
|
+
export function registerOwnedRegistration(
|
|
268
|
+
key: TurnRegistrationKey,
|
|
269
|
+
options?: { isJobTerminal?: (candidate: TurnRegistrationKey) => boolean | undefined },
|
|
270
|
+
): void {
|
|
271
|
+
// Key by (endpoint, jobId, jobGeneration): job ids and generations restart
|
|
272
|
+
// per AsyncJobManager, so CONCURRENT sessions' bg_1/job:1 must coexist —
|
|
273
|
+
// the endpoint identity prevents the second registration from overwriting
|
|
274
|
+
// the first (review thread P1). A same-ENDPOINT (jobId, jobGeneration)
|
|
275
|
+
// rebind (replaced manager within one session) still overwrites the stale
|
|
276
|
+
// entry; a same-turn re-registration (exact tuple) is a no-op.
|
|
277
|
+
const mapKey = `${key.endpointId ?? ""}\u0000${key.jobId}\u0000${key.jobGeneration}`;
|
|
278
|
+
const existing = ownedRegistrations.get(mapKey);
|
|
279
|
+
if (
|
|
280
|
+
existing &&
|
|
281
|
+
existing.lineageIdHash === key.lineageIdHash &&
|
|
282
|
+
existing.promptAttemptEpoch === key.promptAttemptEpoch &&
|
|
283
|
+
existing.endpointId === key.endpointId
|
|
284
|
+
) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (ownedRegistrations.size >= MAX_OWNED_REGISTRATIONS) {
|
|
288
|
+
// Prefer evicting a TERMINAL registration: its job already finished, so
|
|
289
|
+
// a long-lived LIVE job must never be dropped by shorter finished jobs —
|
|
290
|
+
// otherwise scope:"owned" would omit it from the exact causal set and
|
|
291
|
+
// could report stopped_owned while it keeps running (review thread P2).
|
|
292
|
+
let evictKey: string | undefined;
|
|
293
|
+
if (options?.isJobTerminal) {
|
|
294
|
+
for (const [candidateKey, candidate] of ownedRegistrations) {
|
|
295
|
+
// Skip tuples already protected in the retention backlog (their
|
|
296
|
+
// pending authorization must not be re-evicted, review thread P2).
|
|
297
|
+
if (retentionBacklog.has(candidateKey)) continue;
|
|
298
|
+
if (options.isJobTerminal(candidate) === true) {
|
|
299
|
+
evictKey = candidateKey;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (evictKey === undefined) {
|
|
305
|
+
// ALL retained registrations are live: evicting any of them would lose
|
|
306
|
+
// a still-running job's ownership authority. FAIL CLOSED instead —
|
|
307
|
+
// do not evict, skip the new registration. Mark the SKIPPED
|
|
308
|
+
// registration's attempt as having an incomplete causal set: the
|
|
309
|
+
// unregistered job may still launch, so its exact set is unknowable.
|
|
310
|
+
// The marker is scoped to this attempt — never a process-lifetime
|
|
311
|
+
// daemon flag — and is retired when the attempt's endpoint is torn
|
|
312
|
+
// down (review thread P2).
|
|
313
|
+
markAttemptSaturated(key);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const evictedTuple = ownedRegistrations.get(evictKey);
|
|
317
|
+
ownedRegistrations.delete(evictKey);
|
|
318
|
+
if (evictedTuple) {
|
|
319
|
+
if (retainedOwnershipTuples.size >= MAX_RETAINED_OWNERSHIP_TUPLES) {
|
|
320
|
+
// Retained evidence at cap: NEVER drop a pending authorization by
|
|
321
|
+
// age — keep the terminal tuple in the LIVE map (backlogged) so
|
|
322
|
+
// authorizeOwnedCompletion still finds it while its completion
|
|
323
|
+
// envelope is queued (review thread P2). The live map only
|
|
324
|
+
// transiently exceeds its cap under pathological concurrent
|
|
325
|
+
// pending deliveries; the backlog is cleared as deliveries settle.
|
|
326
|
+
retentionBacklog.add(evictKey);
|
|
327
|
+
ownedRegistrations.set(evictKey, evictedTuple);
|
|
328
|
+
// The pending tuple cannot move to retained evidence, so the new
|
|
329
|
+
// registration has NO eviction headroom: FAIL CLOSED instead of
|
|
330
|
+
// inserting it and letting both the live map and the backlog grow
|
|
331
|
+
// without bound while deliveries stay stalled. The skipped
|
|
332
|
+
// registration's attempt is marked incomplete (scoped to the
|
|
333
|
+
// attempt, retired at endpoint teardown) (review thread P2).
|
|
334
|
+
markAttemptSaturated(key);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
retainedOwnershipTuples.set(evictKey, evictedTuple);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
ownedRegistrations.set(mapKey, key);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Record an attempt whose owned-registration set is KNOWN incomplete — the
|
|
344
|
+
* fail-closed authority for an EVICTED in-flight lineage binding whose tool
|
|
345
|
+
* may still launch unregistered work. Every marker is paired with a
|
|
346
|
+
* registration window that settles it at afterToolCall, so the set is bounded
|
|
347
|
+
* by the open-window map. Registry-saturation skips record the affected
|
|
348
|
+
* attempt through markAttemptSaturated instead (review thread P2). */
|
|
349
|
+
function markAttemptRegistrationIncomplete(lineageIdHash: string, attemptEpoch: number): void {
|
|
350
|
+
const incompleteKey = `${lineageIdHash}\u0000${attemptEpoch}`;
|
|
351
|
+
// A marker can be cleared only once its affected call is known unable to
|
|
352
|
+
// register. FIFO aging loses that authority while a delayed in-flight tool
|
|
353
|
+
// may still launch, allowing scope:"owned" to claim stopped_owned over a
|
|
354
|
+
// live unregistered job (review thread P2).
|
|
355
|
+
incompleteOwnedAttempts.add(incompleteKey);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Record a REGISTRY-SATURATION skip for an attempt: the new registration was
|
|
359
|
+
* refused while the process-global registry had no evictable tuple, so the
|
|
360
|
+
* skipped job may still launch unregistered and the attempt's exact causal
|
|
361
|
+
* set can never be proven. Unlike window-backed markers there is no
|
|
362
|
+
* registration window to settle these; they are scoped to the affected
|
|
363
|
+
* attempt and retired when the owning endpoint's registrations are torn down
|
|
364
|
+
* (retireOwnedRegistrationsForEndpoint), so a transient backlog never
|
|
365
|
+
* permanently disables owned aborts daemon-wide (review thread P2). */
|
|
366
|
+
function markAttemptSaturated(key: TurnRegistrationKey): void {
|
|
367
|
+
const incompleteKey = `${key.lineageIdHash}\u0000${key.promptAttemptEpoch}`;
|
|
368
|
+
incompleteOwnedAttempts.add(incompleteKey);
|
|
369
|
+
saturatedAttemptEndpoints.set(incompleteKey, key.endpointId ?? "");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Whether an attempt's owned registration set is KNOWN incomplete (an
|
|
373
|
+
* evicted in-flight lineage binding or a registry-saturation skip) — a
|
|
374
|
+
* scope:"owned" abort of that exact attempt must fail closed to uncertainty
|
|
375
|
+
* (review thread P2). */
|
|
376
|
+
export function isOwnedAttemptRegistrationIncomplete(lineageIdHash: string, attemptEpoch: number): boolean {
|
|
377
|
+
// Per-attempt incomplete authority: window-backed markers from evicted
|
|
378
|
+
// in-flight lineage bindings AND registry-saturation skips both land in
|
|
379
|
+
// incompleteOwnedAttempts, so the check is scoped to the attempt — never a
|
|
380
|
+
// process-lifetime daemon flag — and saturation markers retire at endpoint
|
|
381
|
+
// teardown (review thread P2).
|
|
382
|
+
return incompleteOwnedAttempts.has(`${lineageIdHash}\u0000${attemptEpoch}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Exact (jobId, jobGeneration) lookup for completion-origin classification. */
|
|
386
|
+
export function lookupOwnedRegistration(
|
|
387
|
+
jobId: string,
|
|
388
|
+
jobGeneration: string,
|
|
389
|
+
endpointId?: string,
|
|
390
|
+
): TurnRegistrationKey | undefined {
|
|
391
|
+
const prefix = `${endpointId ?? ""}\u0000${jobId}\u0000${jobGeneration}`;
|
|
392
|
+
if (endpointId !== undefined) {
|
|
393
|
+
const exact = ownedRegistrations.get(prefix);
|
|
394
|
+
if (exact) return exact;
|
|
395
|
+
// The live entry may have been evicted by the cap with its compact
|
|
396
|
+
// authorization evidence retained (pending delivery — review P2).
|
|
397
|
+
const retained = retainedOwnershipTuples.get(prefix);
|
|
398
|
+
if (retained) return retained;
|
|
399
|
+
}
|
|
400
|
+
// An ENDPOINT-qualified lookup NEVER falls back to the cross-endpoint scan:
|
|
401
|
+
// with concurrent sessions reusing bg_1/job:1, the scan could return
|
|
402
|
+
// another session's registration — B's monitor would attach A's ownership
|
|
403
|
+
// envelope, or B's Job/Bash settlement would unregister A's tuple while A's
|
|
404
|
+
// job keeps running (review thread P1). The scan is reserved for callers
|
|
405
|
+
// that OMIT an endpoint (legacy callers/tests).
|
|
406
|
+
if (endpointId === undefined) {
|
|
407
|
+
for (const reg of ownedRegistrations.values()) {
|
|
408
|
+
if (reg.jobId === jobId && reg.jobGeneration === jobGeneration) return reg;
|
|
409
|
+
}
|
|
410
|
+
for (const reg of retainedOwnershipTuples.values()) {
|
|
411
|
+
if (reg.jobId === jobId && reg.jobGeneration === jobGeneration) return reg;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function unregisterOwnedRegistration(key: TurnRegistrationKey): void {
|
|
418
|
+
const mapKey = `${key.endpointId ?? ""}\u0000${key.jobId}\u0000${key.jobGeneration}`;
|
|
419
|
+
// Only remove the EXACT five-tuple: the (jobId, jobGeneration) pair may
|
|
420
|
+
// have been REBOUND to another lineage/epoch/endpoint (which
|
|
421
|
+
// registerOwnedRegistration explicitly allows), and deleting by the pair
|
|
422
|
+
// alone would drop the replacement job's authoritative registration — a
|
|
423
|
+
// later scope:"owned" abort could then omit it and report stopped_owned
|
|
424
|
+
// while it stays active (review thread P1).
|
|
425
|
+
const matches = (r: TurnRegistrationKey) =>
|
|
426
|
+
r.lineageIdHash === key.lineageIdHash &&
|
|
427
|
+
r.promptAttemptEpoch === key.promptAttemptEpoch &&
|
|
428
|
+
r.endpointGeneration === key.endpointGeneration &&
|
|
429
|
+
r.jobId === key.jobId &&
|
|
430
|
+
r.jobGeneration === key.jobGeneration;
|
|
431
|
+
const existing = ownedRegistrations.get(mapKey);
|
|
432
|
+
if (existing && matches(existing)) {
|
|
433
|
+
ownedRegistrations.delete(mapKey);
|
|
434
|
+
}
|
|
435
|
+
// The registration may already have been EVICTED into the retained
|
|
436
|
+
// evidence (a TERMINAL job whose envelope is still queued) while its
|
|
437
|
+
// delivery now settles; settle the retained tuple too. Without this the
|
|
438
|
+
// stale evidence would fill the FIFO and evict authorization for
|
|
439
|
+
// genuinely queued completions (review thread P2).
|
|
440
|
+
const retained = retainedOwnershipTuples.get(mapKey);
|
|
441
|
+
if (retained && matches(retained)) {
|
|
442
|
+
retainedOwnershipTuples.delete(mapKey);
|
|
443
|
+
}
|
|
444
|
+
retentionBacklog.delete(mapKey);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Enumerate every exact owned registration belonging to one aborted turn
|
|
448
|
+
* (matching lineage + attempt epoch). Used by `scope:"owned"` cleanup to
|
|
449
|
+
* capture the exact causal job set; foreign/unclassified work is never
|
|
450
|
+
* returned and is never swept.
|
|
451
|
+
*/
|
|
452
|
+
/** Retire EVERY owned registration owned by a disposing endpoint (live,
|
|
453
|
+
* retained-evidence, and backlogged tuples): after the endpoint's manager is
|
|
454
|
+
* unregistered the tuples can no longer reach a delivery settlement boundary,
|
|
455
|
+
* and future managers deliberately cannot classify foreign-endpoint tuples as
|
|
456
|
+
* terminal for eviction — repeatedly disposing distinct sessions with pending
|
|
457
|
+
* jobs would accumulate tuples until the 8192-entry registry saturates and
|
|
458
|
+
* all later owned aborts fail closed (review thread P2). */
|
|
459
|
+
export function retireOwnedRegistrationsForEndpoint(endpointId: string): void {
|
|
460
|
+
for (const reg of [...ownedRegistrations.values(), ...retainedOwnershipTuples.values()]) {
|
|
461
|
+
if (reg.endpointId === endpointId) unregisterOwnedRegistration(reg);
|
|
462
|
+
}
|
|
463
|
+
// Retire the endpoint's saturation authority too: the skipped
|
|
464
|
+
// (unregistered) job's work is torn down with the endpoint, so the affected
|
|
465
|
+
// attempts no longer need to fail closed (review thread P2).
|
|
466
|
+
for (const [attemptKey, owner] of [...saturatedAttemptEndpoints]) {
|
|
467
|
+
if (owner === endpointId) {
|
|
468
|
+
saturatedAttemptEndpoints.delete(attemptKey);
|
|
469
|
+
incompleteOwnedAttempts.delete(attemptKey);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function findOwnedRegistrationsForTurn(lineageIdHash: string, attemptEpoch: number): TurnRegistrationKey[] {
|
|
475
|
+
const matches: TurnRegistrationKey[] = [];
|
|
476
|
+
for (const key of ownedRegistrations.values()) {
|
|
477
|
+
if (key.lineageIdHash === lineageIdHash && key.promptAttemptEpoch === attemptEpoch) {
|
|
478
|
+
matches.push(key);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// A live registration evicted into the retained evidence (a terminal job
|
|
482
|
+
// whose execution promise is still unwinding) is still exact causal work of
|
|
483
|
+
// this attempt: include it so a scope:"owned" abort generation-verifies,
|
|
484
|
+
// cancels, and waits for its quiescence before claiming stopped_owned
|
|
485
|
+
// (review thread P2). unregisterOwnedRegistration settles retained tuples
|
|
486
|
+
// too, so the settlement path retires them.
|
|
487
|
+
for (const key of retainedOwnershipTuples.values()) {
|
|
488
|
+
if (key.lineageIdHash === lineageIdHash && key.promptAttemptEpoch === attemptEpoch) {
|
|
489
|
+
matches.push(key);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return matches;
|
|
493
|
+
}
|
|
494
|
+
export interface OwnedCompletionClassification {
|
|
495
|
+
lineageIdHash: string;
|
|
496
|
+
promptAttemptEpoch: number;
|
|
497
|
+
registration: TurnRegistrationKey;
|
|
498
|
+
terminalScopeId: string;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Classify a manager completion/progress delivery against the terminal-abort
|
|
503
|
+
* registries. Returns an exact owned-completion classification ONLY when the
|
|
504
|
+
* job carries an exact registered five-tuple AND a terminal scope exists for
|
|
505
|
+
* that turn. Missing or mismatched metadata fails closed (undefined) and the
|
|
506
|
+
* delivery is then ordinary. Classification is source/lineage-based, never
|
|
507
|
+
* timing-based; a closed terminal record does NOT suppress an exact
|
|
508
|
+
* left-running owned completion (corrected turn semantics).
|
|
509
|
+
*/
|
|
510
|
+
export function classifyOwnedCompletion(
|
|
511
|
+
jobId: string,
|
|
512
|
+
jobGeneration: string | undefined,
|
|
513
|
+
): OwnedCompletionClassification | undefined {
|
|
514
|
+
if (!jobGeneration) return undefined;
|
|
515
|
+
const registration = lookupOwnedRegistration(jobId, jobGeneration);
|
|
516
|
+
if (!registration) return undefined;
|
|
517
|
+
const scope = lookupTerminalScope(registration.lineageIdHash, registration.promptAttemptEpoch);
|
|
518
|
+
if (!scope) return undefined;
|
|
519
|
+
return {
|
|
520
|
+
lineageIdHash: registration.lineageIdHash,
|
|
521
|
+
promptAttemptEpoch: registration.promptAttemptEpoch,
|
|
522
|
+
registration,
|
|
523
|
+
terminalScopeId: scope.scopeId,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Whether an owned-completion envelope is authorized by its owning terminal
|
|
528
|
+
* scope as a fresh-turn resume. Used at batch build (sdk/session.ts) and the
|
|
529
|
+
* final injection boundary (agent-session.ts): a denied envelope — owned scope
|
|
530
|
+
* (policy disabled), forged/unregistered tuple, or vanished scope — must be
|
|
531
|
+
* dropped/partitioned out so stopped work can never call followUp/prompt.
|
|
532
|
+
*/
|
|
533
|
+
/**
|
|
534
|
+
* Classify an owned-completion envelope into three states. A registration
|
|
535
|
+
* without a terminal scope (no abort yet) is ORDINARY — normal delivery;
|
|
536
|
+
* only a scope with the owned policy disabled DROPS, and a turn-scope
|
|
537
|
+
* enabled policy is FRESH (new-turn resume). This lets the batch keep
|
|
538
|
+
* ownership on the entry and reclassify at flush/abort time (review
|
|
539
|
+
* thread P1: a completion finished before the abort must not become an
|
|
540
|
+
* unpurgeable ordinary entry that can still resume the agent).
|
|
541
|
+
*/
|
|
542
|
+
/**
|
|
543
|
+
* Structural validation for envelopes carried through the public AgentMessage
|
|
544
|
+
* `details` boundary: `ExtensionAPI.sendMessage` allows arbitrary details, so
|
|
545
|
+
* a malformed `ownedCompletions` entry (object, null item, missing tuple
|
|
546
|
+
* fields) must never crash the delivery path — invalid entries are skipped,
|
|
547
|
+
* never classified (review thread P2).
|
|
548
|
+
*/
|
|
549
|
+
export function isOwnedCompletionEnvelope(value: unknown): value is OwnedCompletionEnvelope {
|
|
550
|
+
if (typeof value !== "object" || value === null) return false;
|
|
551
|
+
const envelope = value as Record<string, unknown>;
|
|
552
|
+
if (typeof envelope.lineageIdHash !== "string") return false;
|
|
553
|
+
if (typeof envelope.promptAttemptEpoch !== "number") return false;
|
|
554
|
+
const registration = envelope.registration;
|
|
555
|
+
if (typeof registration !== "object" || registration === null) return false;
|
|
556
|
+
const key = registration as Record<string, unknown>;
|
|
557
|
+
return (
|
|
558
|
+
typeof key.jobId === "string" &&
|
|
559
|
+
typeof key.jobGeneration === "string" &&
|
|
560
|
+
typeof key.endpointGeneration === "number" &&
|
|
561
|
+
typeof key.lineageIdHash === "string" &&
|
|
562
|
+
typeof key.promptAttemptEpoch === "number" &&
|
|
563
|
+
(key.endpointId === undefined || typeof key.endpointId === "string")
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function classifyOwnedEnvelope(envelope: OwnedCompletionEnvelope): "ordinary" | "fresh" | "drop" {
|
|
568
|
+
const scope = lookupTerminalScope(envelope.lineageIdHash, envelope.promptAttemptEpoch);
|
|
569
|
+
if (scope) {
|
|
570
|
+
return scope.gate.authorizeOwnedCompletion({
|
|
571
|
+
kind: "owned-completion",
|
|
572
|
+
lineageIdHash: envelope.lineageIdHash,
|
|
573
|
+
attemptEpoch: envelope.promptAttemptEpoch,
|
|
574
|
+
registration: envelope.registration,
|
|
575
|
+
}) === "allow-new-turn"
|
|
576
|
+
? "fresh"
|
|
577
|
+
: "drop";
|
|
578
|
+
}
|
|
579
|
+
// The scope was evicted by the cap but its attempt-policy tombstone
|
|
580
|
+
// survives: classify by the retained policy while the registration is
|
|
581
|
+
// still exact (review thread P2).
|
|
582
|
+
const retained = retainedAttemptPolicies.get(`${envelope.lineageIdHash}\u0000${envelope.promptAttemptEpoch}`);
|
|
583
|
+
if (!retained) return "ordinary";
|
|
584
|
+
if (retained.ownedCompletionPolicy === "disabled") return "drop";
|
|
585
|
+
const registered = lookupOwnedRegistration(
|
|
586
|
+
envelope.registration.jobId,
|
|
587
|
+
envelope.registration.jobGeneration,
|
|
588
|
+
envelope.registration.endpointId,
|
|
589
|
+
);
|
|
590
|
+
// Compare EVERY tuple component against the authoritative registration: a
|
|
591
|
+
// (jobId, jobGeneration) may have been REBOUND to another lineage/epoch
|
|
592
|
+
// since the envelope was built, and classifying the stale envelope as fresh
|
|
593
|
+
// would resume work under the wrong ownership (review thread P2).
|
|
594
|
+
if (!registered) return "ordinary";
|
|
595
|
+
if (registered.endpointGeneration !== envelope.registration.endpointGeneration) return "ordinary";
|
|
596
|
+
if (registered.lineageIdHash !== envelope.lineageIdHash) return "ordinary";
|
|
597
|
+
if (registered.promptAttemptEpoch !== envelope.promptAttemptEpoch) return "ordinary";
|
|
598
|
+
if (registered.jobId !== envelope.registration.jobId) return "ordinary";
|
|
599
|
+
if (registered.jobGeneration !== envelope.registration.jobGeneration) return "ordinary";
|
|
600
|
+
return "fresh";
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Whether an envelope must be kept in the batch (not an owned-scope drop). */
|
|
604
|
+
export function isOwnedCompletionEnvelopeAllowed(envelope: OwnedCompletionEnvelope): boolean {
|
|
605
|
+
return classifyOwnedEnvelope(envelope) !== "drop";
|
|
606
|
+
}
|
|
607
|
+
/** Structural subset of AsyncJobManager used by owned-stop settlement (avoids an import cycle). */
|
|
608
|
+
export interface OwnedStopManager {
|
|
609
|
+
cancel(jobId: string): boolean;
|
|
610
|
+
getJob(jobId: string): { generation?: string; status?: string } | undefined;
|
|
611
|
+
getJobPromise?(jobId: string, generation: string): Promise<void> | undefined;
|
|
612
|
+
acknowledgeDeliveries(jobIds: string[]): number;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Settle exact owned work for `scope:"owned"`: generation-verified cancel, a
|
|
617
|
+
* fixed grace, a second quiescence proof, then a delivery purge. Returns
|
|
618
|
+
* "stopped" only when every captured job is terminal after the grace; a reused
|
|
619
|
+
* job id with a new generation, a missing/evicted record, or still-running/
|
|
620
|
+
* paused work fails closed to "unsettled" (AC 16/36 — foreign work is never
|
|
621
|
+
* swept and unprovable quiescence never claims stopped).
|
|
622
|
+
*/
|
|
623
|
+
export async function settleOwnedWork(
|
|
624
|
+
manager: OwnedStopManager,
|
|
625
|
+
exactJobs: TurnRegistrationKey[],
|
|
626
|
+
graceMs: number,
|
|
627
|
+
): Promise<"stopped" | "unsettled"> {
|
|
628
|
+
let generationExact = true;
|
|
629
|
+
for (const reg of exactJobs) {
|
|
630
|
+
const live = manager.getJob(reg.jobId);
|
|
631
|
+
if (live !== undefined && live.generation !== reg.jobGeneration) {
|
|
632
|
+
generationExact = false;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
manager.cancel(reg.jobId);
|
|
636
|
+
}
|
|
637
|
+
if (!generationExact) return "unsettled";
|
|
638
|
+
const deadline = Date.now() + graceMs;
|
|
639
|
+
await Bun.sleep(graceMs);
|
|
640
|
+
// Second proof: every captured job must still be the EXACT captured
|
|
641
|
+
// generation and terminal (cancelled/completed/failed). A reused job id
|
|
642
|
+
// with a NEW generation during the grace, a missing/evicted record, or a
|
|
643
|
+
// still-running/paused job fails closed — foreign work is never swept and
|
|
644
|
+
// unprovable quiescence never claims stopped. Additionally, the ACTUAL job
|
|
645
|
+
// promise must settle within the grace: AsyncJobManager.cancel() updates
|
|
646
|
+
// status to "cancelled" eagerly while the job function may still be
|
|
647
|
+
// unwinding, so the status alone is not proof of quiescence (review P1).
|
|
648
|
+
const quiescent = await Promise.all(
|
|
649
|
+
exactJobs.map(async reg => {
|
|
650
|
+
const job = manager.getJob(reg.jobId);
|
|
651
|
+
if (!job || job.generation !== reg.jobGeneration) return false;
|
|
652
|
+
if (job.status === "running" || job.status === "paused") return false;
|
|
653
|
+
const promise = manager.getJobPromise?.(reg.jobId, reg.jobGeneration);
|
|
654
|
+
if (!promise) return true; // no promise exposed — fall back to status
|
|
655
|
+
let settled = false;
|
|
656
|
+
try {
|
|
657
|
+
await Promise.race([
|
|
658
|
+
promise.then(
|
|
659
|
+
() => {
|
|
660
|
+
settled = true;
|
|
661
|
+
},
|
|
662
|
+
() => {
|
|
663
|
+
settled = true;
|
|
664
|
+
},
|
|
665
|
+
),
|
|
666
|
+
Bun.sleep(Math.max(0, deadline - Date.now())),
|
|
667
|
+
]);
|
|
668
|
+
} catch {
|
|
669
|
+
// ignore: a rejected promise still counts as unwound
|
|
670
|
+
}
|
|
671
|
+
return settled;
|
|
672
|
+
}),
|
|
673
|
+
);
|
|
674
|
+
if (!quiescent.every(Boolean)) return "unsettled";
|
|
675
|
+
manager.acknowledgeDeliveries(exactJobs.map(reg => reg.jobId));
|
|
676
|
+
// The jobs are terminal and their deliveries are suppressed: the owned
|
|
677
|
+
// registrations are settled, so remove them — otherwise they occupy the
|
|
678
|
+
// attempt indefinitely and the retained-policy tombstone eviction cannot
|
|
679
|
+
// distinguish live work from finished work (review thread P2).
|
|
680
|
+
for (const reg of exactJobs) unregisterOwnedRegistration(reg);
|
|
681
|
+
return "stopped";
|
|
682
|
+
}
|
|
683
|
+
export interface LineageBinding {
|
|
684
|
+
lineageIdHash: string;
|
|
685
|
+
promptAttemptEpoch: number;
|
|
686
|
+
endpointGeneration: number;
|
|
687
|
+
/** Endpoint (top-level session) identity that minted this binding. */
|
|
688
|
+
endpointId?: string;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const MAX_LINEAGE_BINDINGS = 8192;
|
|
692
|
+
// Keyed by (endpointId, toolCallId): concurrent sessions receive the SAME
|
|
693
|
+
// provider-local toolCallId, and a toolCallId-only key lets the later
|
|
694
|
+
// session's beforeToolCall overwrite the first session's binding — the first
|
|
695
|
+
// session's background Bash/task job would then be attributed to the second
|
|
696
|
+
// session's endpoint and lineage (review thread P1).
|
|
697
|
+
const lineageByToolCall = new Map<string, LineageBinding>();
|
|
698
|
+
// Tool calls whose afterToolCall settlement already ran. A later eviction of
|
|
699
|
+
// such a binding must NOT open a registration window: the call and its
|
|
700
|
+
// background registrations settled long ago, and the only removal path
|
|
701
|
+
// (settleToolLineageRegistrationWindow) will never run again, so the window
|
|
702
|
+
// and its incomplete-attempt marker would leak forever (review thread P2).
|
|
703
|
+
// Bounded by lineageByToolCall: entries are cleared on rebind and eviction.
|
|
704
|
+
const settledToolCallLineages = new Set<string>();
|
|
705
|
+
const toolCallLineageKey = (endpointId: string | undefined, toolCallId: string) =>
|
|
706
|
+
`${endpointId ?? ""}\u0000${toolCallId}`;
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Bind immutable lineage/attempt metadata to an attempt-scoped tool call
|
|
710
|
+
* identity (toolCallId). The binding is set once at prompt admission and must
|
|
711
|
+
* never be mutated from a session-current fallback; missing/mismatched
|
|
712
|
+
* context fails closed (resolve returns undefined).
|
|
713
|
+
*/
|
|
714
|
+
export function bindToolLineage(toolCallId: string, binding: LineageBinding): void {
|
|
715
|
+
if (lineageByToolCall.size >= MAX_LINEAGE_BINDINGS) {
|
|
716
|
+
const oldest = lineageByToolCall.keys().next().value;
|
|
717
|
+
if (oldest !== undefined) {
|
|
718
|
+
const evicted = lineageByToolCall.get(oldest);
|
|
719
|
+
lineageByToolCall.delete(oldest);
|
|
720
|
+
if (evicted) {
|
|
721
|
+
if (settledToolCallLineages.has(oldest)) {
|
|
722
|
+
// The evicted call already settled (afterToolCall ran): its
|
|
723
|
+
// owned registrations were captured during execution, so there
|
|
724
|
+
// is no in-flight window to protect. Retire the binding without
|
|
725
|
+
// creating a window or an incomplete marker — both would leak
|
|
726
|
+
// forever because the settlement callback never runs again
|
|
727
|
+
// (review thread P2).
|
|
728
|
+
settledToolCallLineages.delete(oldest);
|
|
729
|
+
} else {
|
|
730
|
+
// An evicted binding may belong to an IN-FLIGHT tool call whose
|
|
731
|
+
// background tool has not yet reached registerOwnedIfLineaged:
|
|
732
|
+
// its job would then launch with no owned registration and no
|
|
733
|
+
// evidence. Mark the evicted attempt's registration authority
|
|
734
|
+
// incomplete so a later scope:"owned" abort of THAT attempt fails
|
|
735
|
+
// closed to uncertainty instead of reporting stopped_owned
|
|
736
|
+
// over an incomplete causal set.
|
|
737
|
+
const incompleteKey = `${evicted.lineageIdHash}\u0000${evicted.promptAttemptEpoch}`;
|
|
738
|
+
markAttemptRegistrationIncomplete(evicted.lineageIdHash, evicted.promptAttemptEpoch);
|
|
739
|
+
// Retain the full lineage binding in the window: if this in-flight tool
|
|
740
|
+
// call still launches a background job, registerOwnedIfLineaged can
|
|
741
|
+
// register it under the retained lineage instead of leaving the
|
|
742
|
+
// attempt's causal set empty (review thread P2).
|
|
743
|
+
incompleteToolCallWindows.set(oldest, { incompleteKey, binding: evicted });
|
|
744
|
+
incompleteAttemptWindowCounts.set(
|
|
745
|
+
incompleteKey,
|
|
746
|
+
(incompleteAttemptWindowCounts.get(incompleteKey) ?? 0) + 1,
|
|
747
|
+
);
|
|
748
|
+
// The per-attempt marker above is deliberately scoped to the
|
|
749
|
+
// evicted attempt (never process-lifetime): every tool call binds
|
|
750
|
+
// a lineage entry and production never unbinds it (bindings
|
|
751
|
+
// intentionally survive the tool call so resumed registrations
|
|
752
|
+
// re-use the original id and task batches register one job per
|
|
753
|
+
// item), so a healthy long-running daemon inevitably reaches the
|
|
754
|
+
// 8,192 cap and the first eviction is ordinary historical churn —
|
|
755
|
+
// the evicted call and its job have settled long ago. A
|
|
756
|
+
// process-lifetime flag on ANY eviction would make every
|
|
757
|
+
// subsequent scope:"owned" abort fail with uncertainty forever,
|
|
758
|
+
// even when all evicted calls settled (review thread P2).
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
// A rebind supersedes any prior execution of the same id: the new execution
|
|
764
|
+
// is in flight until its own afterToolCall settles.
|
|
765
|
+
settledToolCallLineages.delete(toolCallLineageKey(binding.endpointId, toolCallId));
|
|
766
|
+
lineageByToolCall.set(toolCallLineageKey(binding.endpointId, toolCallId), binding);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
export function resolveToolLineage(toolCallId: string | undefined, endpointId?: string): LineageBinding | undefined {
|
|
770
|
+
if (toolCallId === undefined) return undefined;
|
|
771
|
+
if (endpointId !== undefined) return lineageByToolCall.get(toolCallLineageKey(endpointId, toolCallId));
|
|
772
|
+
// Legacy endpoint-less callers: match by toolCallId. Prefer a binding whose
|
|
773
|
+
// endpoint equals the caller's, else the first match (review thread P1 —
|
|
774
|
+
// endpoint-aware callers must pass their endpoint to disambiguate
|
|
775
|
+
// concurrent sessions).
|
|
776
|
+
for (const [key, binding] of lineageByToolCall) {
|
|
777
|
+
if (key.endsWith(`\u0000${toolCallId}`)) return binding;
|
|
778
|
+
}
|
|
779
|
+
return undefined;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** Close an evicted tool's registration window after its execution settles. */
|
|
783
|
+
export function settleToolLineageRegistrationWindow(toolCallId: string, endpointId?: string): void {
|
|
784
|
+
const key = toolCallLineageKey(endpointId, toolCallId);
|
|
785
|
+
// Mark the execution settled ONLY while its binding remains in the lineage
|
|
786
|
+
// map: a later FIFO eviction of that binding consults this set and removes
|
|
787
|
+
// the key. An already-evicted binding lives only in the window closed
|
|
788
|
+
// below — retaining the marker there would leak the key forever because
|
|
789
|
+
// no eviction can ever execute the delete (review thread P2).
|
|
790
|
+
if (lineageByToolCall.has(key)) settledToolCallLineages.add(key);
|
|
791
|
+
const window = incompleteToolCallWindows.get(key);
|
|
792
|
+
if (window !== undefined) {
|
|
793
|
+
incompleteToolCallWindows.delete(key);
|
|
794
|
+
const remaining = (incompleteAttemptWindowCounts.get(window.incompleteKey) ?? 1) - 1;
|
|
795
|
+
if (remaining <= 0) {
|
|
796
|
+
incompleteAttemptWindowCounts.delete(window.incompleteKey);
|
|
797
|
+
// The same attempt may ALSO carry saturation evidence (an owned job
|
|
798
|
+
// whose registration was rejected because the owned registry was
|
|
799
|
+
// full): keep the incomplete marker while either source remains, or
|
|
800
|
+
// a later scope:"owned" abort could omit that still-live job and
|
|
801
|
+
// claim stopped_owned (review thread P2).
|
|
802
|
+
if (!saturatedAttemptEndpoints.has(window.incompleteKey)) {
|
|
803
|
+
incompleteOwnedAttempts.delete(window.incompleteKey);
|
|
804
|
+
}
|
|
805
|
+
} else {
|
|
806
|
+
incompleteAttemptWindowCounts.set(window.incompleteKey, remaining);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export function unbindToolLineage(toolCallId: string, endpointId?: string): void {
|
|
812
|
+
if (endpointId !== undefined) {
|
|
813
|
+
lineageByToolCall.delete(toolCallLineageKey(endpointId, toolCallId));
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
for (const key of lineageByToolCall.keys()) {
|
|
817
|
+
if (key.endsWith(`\u0000${toolCallId}`)) lineageByToolCall.delete(key);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Mint an unforgeable opaque lineage id for one prompt turn. The hash binds
|
|
823
|
+
* session id, attempt epoch, and a per-session secret; it never contains
|
|
824
|
+
* prompt body and cannot be re-derived from public session data. It is
|
|
825
|
+
* created before model/tool execution and must never be mutated from a
|
|
826
|
+
* session-current fallback.
|
|
827
|
+
*/
|
|
828
|
+
export function mintTurnLineageIdHash(sessionId: string, promptAttemptEpoch: number, sessionSecret: string): string {
|
|
829
|
+
return createHash("sha256")
|
|
830
|
+
.update(`turn-lineage-v1:${sessionId}\u0000${promptAttemptEpoch}\u0000${sessionSecret}`)
|
|
831
|
+
.digest("hex");
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Register an exact owned registration when the tool call carries immutable
|
|
835
|
+
* lineage metadata. The generation is read synchronously from the manager's
|
|
836
|
+
* job record; a missing generation fails closed (no ownership claim). A
|
|
837
|
+
* registry failure never breaks ordinary registration.
|
|
838
|
+
*/
|
|
839
|
+
export function registerOwnedIfLineaged(
|
|
840
|
+
manager: { getJob?(id: string): { generation?: string; status?: string } | undefined },
|
|
841
|
+
toolCallId: string | undefined,
|
|
842
|
+
jobId: string,
|
|
843
|
+
endpointId?: string,
|
|
844
|
+
): void {
|
|
845
|
+
try {
|
|
846
|
+
let lineage = resolveToolLineage(toolCallId, endpointId);
|
|
847
|
+
if (!lineage && toolCallId !== undefined) {
|
|
848
|
+
// The lineage binding may have been EVICTED by the cap while this tool
|
|
849
|
+
// call was still in flight. The eviction retained the binding's lineage
|
|
850
|
+
// in the registration window: register the launched job under that
|
|
851
|
+
// retained lineage so the attempt's causal set is complete instead of
|
|
852
|
+
// empty. Settling the window here would remove the attempt's only
|
|
853
|
+
// fail-closed marker while the unregistered job keeps running and a
|
|
854
|
+
// later scope:"owned" abort could claim stopped_owned over nothing
|
|
855
|
+
// (review thread P2). The window (and attempt marker) settles at
|
|
856
|
+
// afterToolCall once the tool call's execution ends.
|
|
857
|
+
lineage = incompleteToolCallWindows.get(toolCallLineageKey(endpointId, toolCallId))?.binding;
|
|
858
|
+
}
|
|
859
|
+
if (!lineage) return;
|
|
860
|
+
const job = manager.getJob?.(jobId);
|
|
861
|
+
const jobGeneration = job?.generation;
|
|
862
|
+
if (!jobGeneration) return;
|
|
863
|
+
registerOwnedRegistration(
|
|
864
|
+
{
|
|
865
|
+
...(lineage.endpointId ? { endpointId: lineage.endpointId } : {}),
|
|
866
|
+
endpointGeneration: lineage.endpointGeneration,
|
|
867
|
+
lineageIdHash: lineage.lineageIdHash,
|
|
868
|
+
promptAttemptEpoch: lineage.promptAttemptEpoch,
|
|
869
|
+
jobId,
|
|
870
|
+
jobGeneration,
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
// Only FINISHED jobs are evictable under the cap, so a long-lived
|
|
874
|
+
// live job's ownership tuple is never dropped by shorter completed
|
|
875
|
+
// jobs (review thread P2). The check is scoped to the CANDIDATE's
|
|
876
|
+
// OWN endpoint: a candidate from another session (same reusable
|
|
877
|
+
// job id bg_1) must not be judged terminal using THIS manager's
|
|
878
|
+
// unrelated bg_1 record (review thread P2).
|
|
879
|
+
isJobTerminal: candidate => {
|
|
880
|
+
if ((candidate.endpointId ?? "") !== (lineage.endpointId ?? "")) return undefined;
|
|
881
|
+
const status = manager.getJob?.(candidate.jobId)?.status;
|
|
882
|
+
return status === "completed" || status === "cancelled" || status === "failed" || status === "evicted";
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
);
|
|
886
|
+
} catch {
|
|
887
|
+
// ignore: never break ordinary registration
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/** Retire the exact (endpoint, jobId, jobGeneration) owned registration when
|
|
892
|
+
* its completion delivery is dead-lettered by the manager (delivery-queue
|
|
893
|
+
* overflow or retry exhaustion): no message is injected and no later
|
|
894
|
+
* consumption boundary will settle it, so the terminal tuple would otherwise
|
|
895
|
+
* occupy the global registries until saturation (review thread P2). The
|
|
896
|
+
* delivery is enqueued only when the job is terminal, so no live job's
|
|
897
|
+
* authority is dropped. */
|
|
898
|
+
export function retireOwnedRegistrationForDeadLetter(
|
|
899
|
+
endpointId: string | undefined,
|
|
900
|
+
jobId: string,
|
|
901
|
+
jobGeneration: string,
|
|
902
|
+
): void {
|
|
903
|
+
const tuple = lookupOwnedRegistration(jobId, jobGeneration, endpointId);
|
|
904
|
+
if (tuple) unregisterOwnedRegistration(tuple);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
let attemptEpochCounter = 0;
|
|
908
|
+
|
|
909
|
+
/** Monotonic fresh-attempt epoch for `resumeFromOwnedCompletion` allocation. */
|
|
910
|
+
export function nextPromptAttemptEpoch(): number {
|
|
911
|
+
return ++attemptEpochCounter;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** Mint a fresh terminal scope id (opaque, never persisted raw). */
|
|
915
|
+
export function newTerminalScopeId(): string {
|
|
916
|
+
return randomUUID();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
export interface TurnContinuationSeam {
|
|
920
|
+
fence: TurnContinuationFence;
|
|
921
|
+
gate: TurnContinuationGate;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Create a continuation fence + gate for one terminal scope. The fence starts
|
|
926
|
+
* `open` and is closed synchronously via `gate.close()` before the root turn is
|
|
927
|
+
* interrupted. Continuation authorization is source-based (lineageIdHash +
|
|
928
|
+
* attemptEpoch + continuationId); timing alone never authorizes.
|
|
929
|
+
*/
|
|
930
|
+
export function createTurnContinuationSeam(options: {
|
|
931
|
+
lineageIdHash: string;
|
|
932
|
+
abortedAttemptEpoch: number;
|
|
933
|
+
terminalScopeId: string;
|
|
934
|
+
ownedCompletionPolicy?: OwnedCompletionPolicy;
|
|
935
|
+
blockedContinuationIds?: readonly string[];
|
|
936
|
+
}): TurnContinuationSeam {
|
|
937
|
+
const blocked = new Set<string>(options.blockedContinuationIds ?? []);
|
|
938
|
+
const predecessors = new Set<string>();
|
|
939
|
+
let state: TurnContinuationFenceState = "open";
|
|
940
|
+
|
|
941
|
+
const fence: TurnContinuationFence = {
|
|
942
|
+
state: "open",
|
|
943
|
+
lineageIdHash: options.lineageIdHash,
|
|
944
|
+
abortedAttemptEpoch: options.abortedAttemptEpoch,
|
|
945
|
+
terminalScopeId: options.terminalScopeId,
|
|
946
|
+
blockedContinuationIds: blocked,
|
|
947
|
+
predecessorTombstones: predecessors,
|
|
948
|
+
ownedCompletionPolicy: options.ownedCompletionPolicy ?? "enabled",
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
const gate: TurnContinuationGate = {
|
|
952
|
+
close(_reason: "terminal-turn") {
|
|
953
|
+
if (state === "closing" || state === "closed") return;
|
|
954
|
+
state = "closing";
|
|
955
|
+
state = "closed";
|
|
956
|
+
fence.state = state;
|
|
957
|
+
},
|
|
958
|
+
authorizeContinuation(origin) {
|
|
959
|
+
if (origin.kind !== "turn-continuation") return "deny";
|
|
960
|
+
if (origin.lineageIdHash !== fence.lineageIdHash || origin.attemptEpoch !== fence.abortedAttemptEpoch)
|
|
961
|
+
return "deny";
|
|
962
|
+
// A call linearized BEFORE close is a predecessor: record it once and
|
|
963
|
+
// allow it to finish its already-started work; it must never start a
|
|
964
|
+
// successor. After close, only recorded predecessors pass; every other
|
|
965
|
+
// same-turn continuation (retry/TTSR/steering/hidden/maintenance) is
|
|
966
|
+
// denied.
|
|
967
|
+
if (state === "open" || state === "closing") {
|
|
968
|
+
predecessors.add(origin.continuationId);
|
|
969
|
+
return "allow-predecessor";
|
|
970
|
+
}
|
|
971
|
+
return predecessors.has(origin.continuationId) ? "allow-predecessor" : "deny";
|
|
972
|
+
},
|
|
973
|
+
authorizeOwnedCompletion(origin) {
|
|
974
|
+
// Owned completion is intentionally NOT suppressed by a closed turn
|
|
975
|
+
// record. Validate exact source metadata and fail closed otherwise.
|
|
976
|
+
if (origin.kind !== "owned-completion") return "deny";
|
|
977
|
+
if (!origin.registration || typeof origin.registration !== "object") return "deny";
|
|
978
|
+
if (origin.lineageIdHash !== fence.lineageIdHash) return "deny";
|
|
979
|
+
if (origin.attemptEpoch !== fence.abortedAttemptEpoch) return "deny";
|
|
980
|
+
const { endpointGeneration, promptAttemptEpoch, jobId, jobGeneration } = origin.registration;
|
|
981
|
+
if (promptAttemptEpoch !== fence.abortedAttemptEpoch) return "deny";
|
|
982
|
+
if (fence.ownedCompletionPolicy === "disabled") return "deny";
|
|
983
|
+
if (
|
|
984
|
+
!Number.isFinite(endpointGeneration) ||
|
|
985
|
+
typeof jobId !== "string" ||
|
|
986
|
+
!jobId ||
|
|
987
|
+
typeof jobGeneration !== "string" ||
|
|
988
|
+
!jobGeneration
|
|
989
|
+
)
|
|
990
|
+
return "deny";
|
|
991
|
+
// The tuple must be an EXACT registered five-tuple: an unregistered,
|
|
992
|
+
// forged, or mutated registration fails closed even when the outer
|
|
993
|
+
// lineage/epoch match the aborted turn (AC 25 — missing/copied/
|
|
994
|
+
// mismatched origin never authorizes an automatic call). The lookup
|
|
995
|
+
// is ENDPOINT-scoped so concurrent sessions' same reusable job id
|
|
996
|
+
// never resolves to a foreign session's registration, which would
|
|
997
|
+
// deny a valid completion (review thread P1).
|
|
998
|
+
const registered = lookupOwnedRegistration(jobId, jobGeneration, origin.registration.endpointId);
|
|
999
|
+
if (!registered) return "deny";
|
|
1000
|
+
if (
|
|
1001
|
+
registered.lineageIdHash !== origin.lineageIdHash ||
|
|
1002
|
+
registered.promptAttemptEpoch !== promptAttemptEpoch ||
|
|
1003
|
+
registered.endpointGeneration !== endpointGeneration ||
|
|
1004
|
+
(registered.endpointId ?? "") !== (origin.registration.endpointId ?? "")
|
|
1005
|
+
)
|
|
1006
|
+
return "deny";
|
|
1007
|
+
return "allow-new-turn";
|
|
1008
|
+
},
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
return { fence, gate };
|
|
1012
|
+
}
|
|
1013
|
+
export interface RegisteredTerminalScope {
|
|
1014
|
+
scopeId: string;
|
|
1015
|
+
lineageIdHash: string;
|
|
1016
|
+
promptAttemptEpoch: number;
|
|
1017
|
+
seam: TurnContinuationSeam;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Create, register, and synchronously close a terminal scope for one aborted
|
|
1022
|
+
* turn. The fence closes before the first await that interrupts the root turn;
|
|
1023
|
+
* owned-completion policy is enabled for `scope:"turn"` (left-running owned
|
|
1024
|
+
* delivery intentionally resumes the agent as a fresh turn) and disabled for
|
|
1025
|
+
* `scope:"owned"`. Registered scopes are process-local and bounded; the exact
|
|
1026
|
+
* (lineageIdHash, attemptEpoch) key makes later owned-completion classification
|
|
1027
|
+
* source-exact and fail-closed.
|
|
1028
|
+
*/
|
|
1029
|
+
export function registerTerminalTurnScope(options: {
|
|
1030
|
+
lineageIdHash: string;
|
|
1031
|
+
promptAttemptEpoch: number;
|
|
1032
|
+
terminalScopeId?: string;
|
|
1033
|
+
ownedCompletionPolicy?: OwnedCompletionPolicy;
|
|
1034
|
+
blockedContinuationIds?: readonly string[];
|
|
1035
|
+
}): RegisteredTerminalScope | undefined {
|
|
1036
|
+
// Concurrent aborts of the SAME turn (dispatch-cache eviction can admit two
|
|
1037
|
+
// same-turn requests before either settles) must not overwrite the attempt
|
|
1038
|
+
// index with a second scope of a different owned-completion policy — a
|
|
1039
|
+
// losing scope:"owned" request would otherwise make a successful
|
|
1040
|
+
// scope:"turn" request's completions drop instead of resuming the fresh
|
|
1041
|
+
// turn (review thread P2). Reuse the first scope's authority.
|
|
1042
|
+
const existing = lookupTerminalScope(options.lineageIdHash, options.promptAttemptEpoch);
|
|
1043
|
+
if (existing) {
|
|
1044
|
+
// A concurrent abort with a DIFFERENT owned-completion policy must not
|
|
1045
|
+
// reuse this scope's gate: the losing request would report semantics
|
|
1046
|
+
// (automaticDelivery/ownedWork) that belong to the winner's policy — an
|
|
1047
|
+
// owned request winning would let the concurrent turn request claim
|
|
1048
|
+
// left_running/enabled, and vice versa. Fail the mismatched admission
|
|
1049
|
+
// closed so nothing is attributed to the losing request (review thread
|
|
1050
|
+
// P1); the winner's scope and policy stay untouched.
|
|
1051
|
+
if (existing.fence.ownedCompletionPolicy !== options.ownedCompletionPolicy) return undefined;
|
|
1052
|
+
return {
|
|
1053
|
+
scopeId: existing.scopeId,
|
|
1054
|
+
lineageIdHash: existing.lineageIdHash,
|
|
1055
|
+
promptAttemptEpoch: existing.abortedAttemptEpoch,
|
|
1056
|
+
seam: { fence: existing.fence, gate: existing.gate },
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
const terminalScopeId = options.terminalScopeId ?? newTerminalScopeId();
|
|
1060
|
+
const seam = createTurnContinuationSeam({
|
|
1061
|
+
lineageIdHash: options.lineageIdHash,
|
|
1062
|
+
abortedAttemptEpoch: options.promptAttemptEpoch,
|
|
1063
|
+
terminalScopeId,
|
|
1064
|
+
ownedCompletionPolicy: options.ownedCompletionPolicy,
|
|
1065
|
+
blockedContinuationIds: options.blockedContinuationIds,
|
|
1066
|
+
});
|
|
1067
|
+
seam.gate.close("terminal-turn");
|
|
1068
|
+
const admitted = registerTerminalScope({
|
|
1069
|
+
scopeId: terminalScopeId,
|
|
1070
|
+
lineageIdHash: options.lineageIdHash,
|
|
1071
|
+
abortedAttemptEpoch: options.promptAttemptEpoch,
|
|
1072
|
+
gate: seam.gate,
|
|
1073
|
+
fence: seam.fence,
|
|
1074
|
+
});
|
|
1075
|
+
if (!admitted) return undefined;
|
|
1076
|
+
return {
|
|
1077
|
+
scopeId: terminalScopeId,
|
|
1078
|
+
lineageIdHash: options.lineageIdHash,
|
|
1079
|
+
promptAttemptEpoch: options.promptAttemptEpoch,
|
|
1080
|
+
seam,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* TEST-ONLY: clear the module-global terminal-abort registries so tests get
|
|
1086
|
+
* isolated lineage/binding/scope state. Never call from production code —
|
|
1087
|
+
* the registries are intentionally process-lifetime in the runtime.
|
|
1088
|
+
*/
|
|
1089
|
+
export function resetTerminalAbortRegistriesForTests(): void {
|
|
1090
|
+
activeScopes.clear();
|
|
1091
|
+
activeScopeByAttempt.clear();
|
|
1092
|
+
ownedRegistrations.clear();
|
|
1093
|
+
lineageByToolCall.clear();
|
|
1094
|
+
settledToolCallLineages.clear();
|
|
1095
|
+
retainedOwnershipTuples.clear();
|
|
1096
|
+
retainedAttemptPolicies.clear();
|
|
1097
|
+
incompleteOwnedAttempts.clear();
|
|
1098
|
+
incompleteToolCallWindows.clear();
|
|
1099
|
+
incompleteAttemptWindowCounts.clear();
|
|
1100
|
+
retentionBacklog.clear();
|
|
1101
|
+
saturatedAttemptEndpoints.clear();
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Structural subset of a durable terminal-scope row needed for bounding
|
|
1106
|
+
* retention (review thread P2). Only rows with a COMPLETED disposition are
|
|
1107
|
+
* evictable; pending markers are never touched.
|
|
1108
|
+
*/
|
|
1109
|
+
export interface DurableScopeRetentionRow {
|
|
1110
|
+
idempotencyKeyHash?: string;
|
|
1111
|
+
idempotencyInputHash?: string;
|
|
1112
|
+
turnDisposition: "pending" | "no_effect" | (string & {});
|
|
1113
|
+
acceptedAt?: number;
|
|
1114
|
+
ownedWorkDisposition?: "not_requested" | "left_running" | "stopped" | "uncertain";
|
|
1115
|
+
responseState?: "pending" | "sent" | "delivered" | "failed";
|
|
1116
|
+
responsePayloadHash?: string;
|
|
1117
|
+
replayPayloadHash?: string;
|
|
1118
|
+
terminalPublished?: boolean;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Evict the OLDEST COMPLETED terminal-scope rows beyond `cap`, mirroring the
|
|
1123
|
+
* bounded in-memory idempotency cache so a long-lived session cannot grow
|
|
1124
|
+
* the durable reconciliation document indefinitely. Completed dispositions
|
|
1125
|
+
* (stopped/uncertain/no_effect) are evicted oldest-first; pending markers and
|
|
1126
|
+
* TRANSITIONAL no_effect_reserved reservations (an in-flight abort that may
|
|
1127
|
+
* still transition to active) are never evicted — evicting a reserved row
|
|
1128
|
+
* would leave a tombstone that replays uncertainty over an unfinalized
|
|
1129
|
+
* reservation (review thread P2). Returns a new array.
|
|
1130
|
+
*/
|
|
1131
|
+
export function boundCompletedTerminalScopeRows<T extends DurableScopeRetentionRow>(rows: T[], cap: number): T[] {
|
|
1132
|
+
const isCompleted = (row: T): boolean =>
|
|
1133
|
+
row.turnDisposition !== "pending" && row.turnDisposition !== "no_effect_reserved";
|
|
1134
|
+
const retentionKey = (row: T): string => `${row.idempotencyKeyHash ?? ""}\u0000${row.idempotencyInputHash ?? ""}`;
|
|
1135
|
+
const completed = rows.filter(isCompleted);
|
|
1136
|
+
if (completed.length <= cap) return rows;
|
|
1137
|
+
const overflow = completed.length - cap;
|
|
1138
|
+
const evict = new Set(
|
|
1139
|
+
[...completed]
|
|
1140
|
+
.sort((a, b) => (a.acceptedAt ?? 0) - (b.acceptedAt ?? 0))
|
|
1141
|
+
.slice(0, overflow)
|
|
1142
|
+
.map(retentionKey),
|
|
1143
|
+
);
|
|
1144
|
+
// The evict set is keyed by key+input hash, so it must only be applied to
|
|
1145
|
+
// COMPLETED rows: a transitional no_effect_reserved reservation that happens
|
|
1146
|
+
// to share an evicted row's key pair must survive, exactly like a pending
|
|
1147
|
+
// marker (review thread P2).
|
|
1148
|
+
return rows.filter(row => !(isCompleted(row) && evict.has(retentionKey(row))));
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Compact key tombstones for completed rows evicted by the retention cap: the
|
|
1153
|
+
* key+input hashes are retained durably so a same-key retry after dispatch
|
|
1154
|
+
* cache expiry/restart still replays instead of aborting an unrelated later
|
|
1155
|
+
* prompt (review thread P2).
|
|
1156
|
+
*/
|
|
1157
|
+
/** Compact key tombstone with enough disposition metadata to reconstruct the
|
|
1158
|
+
* original replay result (the retention cap can evict stopped/uncertain rows,
|
|
1159
|
+
* not only no-effect reservations — review thread P2). */
|
|
1160
|
+
export interface EvictedTerminalKey {
|
|
1161
|
+
keyHash: string;
|
|
1162
|
+
inputHash: string;
|
|
1163
|
+
turnDisposition: "stopped" | "uncertain" | "no_effect" | "no_effect_marker_failure";
|
|
1164
|
+
ownedWorkDisposition: "not_requested" | "left_running" | "stopped" | "uncertain";
|
|
1165
|
+
responseState?: "pending" | "sent" | "delivered" | "failed";
|
|
1166
|
+
responsePayloadHash?: string;
|
|
1167
|
+
replayPayloadHash?: string;
|
|
1168
|
+
terminalPublished?: boolean;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/** Bound the retained evicted-key tombstone collection FIFO: when a
|
|
1172
|
+
* long-lived session's unique terminal-abort keys evict completed rows until
|
|
1173
|
+
* the tombstone cap is reached, the OLDEST tombstones expire instead of the
|
|
1174
|
+
* next finalization throwing after the destructive stop already happened —
|
|
1175
|
+
* the client would otherwise receive an error while its durable row stays
|
|
1176
|
+
* pending, and subsequent aborts repeat the failure and accumulate
|
|
1177
|
+
* non-evictable pending rows (review thread P2). A dropped tombstone only
|
|
1178
|
+
* loses replay authority for keys older than the bound; the idempotency
|
|
1179
|
+
* guarantee degrades to the in-memory cache horizon instead of disabling
|
|
1180
|
+
* future aborts. */
|
|
1181
|
+
export function boundEvictedTerminalKeys<T extends { keyHash: string }>(keys: T[], cap: number): T[] {
|
|
1182
|
+
if (keys.length <= cap) return keys;
|
|
1183
|
+
return keys.slice(keys.length - cap);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
export function collectEvictedTerminalKeys<T extends DurableScopeRetentionRow>(
|
|
1187
|
+
before: T[],
|
|
1188
|
+
after: T[],
|
|
1189
|
+
): EvictedTerminalKey[] {
|
|
1190
|
+
const afterKeys = new Set(after.map(s => `${s.idempotencyKeyHash ?? ""}\u0000${s.idempotencyInputHash ?? ""}`));
|
|
1191
|
+
const evicted: EvictedTerminalKey[] = [];
|
|
1192
|
+
for (const row of before) {
|
|
1193
|
+
// A TRANSITIONAL reservation is never evictable, so it must never mint a
|
|
1194
|
+
// tombstone either: the same completed-row definition as
|
|
1195
|
+
// `boundCompletedTerminalScopeRows` (review thread P2).
|
|
1196
|
+
if (row.turnDisposition === "pending" || row.turnDisposition === "no_effect_reserved") continue;
|
|
1197
|
+
const key = `${row.idempotencyKeyHash ?? ""}\u0000${row.idempotencyInputHash ?? ""}`;
|
|
1198
|
+
if (!afterKeys.has(key) && row.idempotencyKeyHash && row.idempotencyInputHash) {
|
|
1199
|
+
evicted.push({
|
|
1200
|
+
keyHash: row.idempotencyKeyHash,
|
|
1201
|
+
inputHash: row.idempotencyInputHash,
|
|
1202
|
+
turnDisposition: row.turnDisposition as "stopped" | "uncertain" | "no_effect" | "no_effect_marker_failure",
|
|
1203
|
+
ownedWorkDisposition: row.ownedWorkDisposition ?? "not_requested",
|
|
1204
|
+
responseState: row.responseState,
|
|
1205
|
+
responsePayloadHash: row.responsePayloadHash,
|
|
1206
|
+
// A pending finalized row's written replay must still advance the
|
|
1207
|
+
// evicted tombstone; preserve its replay-shaped hash (review
|
|
1208
|
+
// thread P2).
|
|
1209
|
+
replayPayloadHash: row.replayPayloadHash,
|
|
1210
|
+
terminalPublished: row.terminalPublished,
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return evicted;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/** Durable terminal-scope reservation cap. Idle/already-terminal aborts write
|
|
1218
|
+
* durable no-effect reservations, so a client sending idle aborts with unique
|
|
1219
|
+
* keys must not grow the reconciliation document indefinitely: only the OLDEST
|
|
1220
|
+
* COMPLETED rows beyond this cap are evicted (review thread P2). */
|
|
1221
|
+
export const MAX_DURABLE_TERMINAL_RESERVATIONS = 256;
|
|
1222
|
+
/** Retained evicted-key tombstone cap; see {@link boundEvictedTerminalKeys}. */
|
|
1223
|
+
export const MAX_RETAINED_TERMINAL_KEY_TOMBSTONES = 4096;
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Apply the durable terminal-scope retention bound to a pending
|
|
1227
|
+
* `transactTerminalState` mutation: evict the oldest COMPLETED scope rows past
|
|
1228
|
+
* {@link MAX_DURABLE_TERMINAL_RESERVATIONS}, retain a compact key tombstone for
|
|
1229
|
+
* every evicted row ATOMICALLY with the scope write, and FIFO-expire tombstones
|
|
1230
|
+
* past {@link MAX_RETAINED_TERMINAL_KEY_TOMBSTONES} instead of throwing after a
|
|
1231
|
+
* destructive stop already happened (review thread P2).
|
|
1232
|
+
*
|
|
1233
|
+
* Every durable terminal-state write — admission markers, no-effect
|
|
1234
|
+
* reservations, reservation finalization, and pending-marker transitions — must
|
|
1235
|
+
* go through this single bound in BOTH session runtimes (the notifications-hosted
|
|
1236
|
+
* bus runtime and the SDK-only host runtime); a hand-rolled copy is how the two
|
|
1237
|
+
* paths drift apart.
|
|
1238
|
+
*/
|
|
1239
|
+
export function boundTerminalRetentionState<Row extends DurableScopeRetentionRow, Key extends { keyHash: string }>(
|
|
1240
|
+
priorKeys: readonly Key[],
|
|
1241
|
+
nextScopes: Row[],
|
|
1242
|
+
maxScopes: number = MAX_DURABLE_TERMINAL_RESERVATIONS,
|
|
1243
|
+
): { scopes: Row[]; keys: Array<Key | EvictedTerminalKey> } {
|
|
1244
|
+
const scopes = boundCompletedTerminalScopeRows(nextScopes, maxScopes);
|
|
1245
|
+
const evicted = collectEvictedTerminalKeys(nextScopes, scopes);
|
|
1246
|
+
return {
|
|
1247
|
+
scopes,
|
|
1248
|
+
keys: boundEvictedTerminalKeys<Key | EvictedTerminalKey>(
|
|
1249
|
+
[...priorKeys, ...evicted],
|
|
1250
|
+
MAX_RETAINED_TERMINAL_KEY_TOMBSTONES,
|
|
1251
|
+
),
|
|
1252
|
+
};
|
|
1253
|
+
}
|