@gajae-code/coding-agent 0.13.2 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +215 -8
- package/README.md +2 -0
- package/bin/gajaessi.js +4 -0
- package/dist/types/async/job-manager.d.ts +55 -2
- package/dist/types/async/support.d.ts +14 -0
- package/dist/types/capability/mcp.d.ts +6 -0
- package/dist/types/capability/types.d.ts +6 -0
- package/dist/types/cli/accounts-cli.d.ts +19 -0
- package/dist/types/cli/args.d.ts +3 -0
- package/dist/types/cli/config-cli.d.ts +6 -0
- package/dist/types/cli/crash-cli.d.ts +10 -0
- package/dist/types/cli/customize-doctor.d.ts +145 -0
- package/dist/types/cli/daemon-cli.d.ts +3 -1
- package/dist/types/cli/mcp-cli.d.ts +11 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/root-flags.d.ts +12 -0
- package/dist/types/cli/setup-cli.d.ts +3 -1
- package/dist/types/cli/skills-cli.d.ts +2 -1
- package/dist/types/cli/update-cli.d.ts +19 -1
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/commands/accounts.d.ts +40 -0
- package/dist/types/commands/crash.d.ts +24 -0
- package/dist/types/commands/customize.d.ts +22 -0
- package/dist/types/commands/daemon.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/commands/skills.d.ts +6 -1
- package/dist/types/commands/update.d.ts +7 -0
- package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
- package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
- package/dist/types/config/config-schema-version.d.ts +10 -0
- package/dist/types/config/file-lock.d.ts +18 -0
- package/dist/types/config/model-profile-activation.d.ts +3 -1
- package/dist/types/config/model-registry.d.ts +10 -1
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +73 -9
- package/dist/types/config/settings.d.ts +63 -0
- package/dist/types/config/skill-settings-defaults.d.ts +26 -0
- package/dist/types/config/task-delegation.d.ts +39 -0
- package/dist/types/config/update-channel.d.ts +10 -0
- package/dist/types/coordinator-mcp/server.d.ts +5 -1
- package/dist/types/crash/index-store.d.ts +87 -0
- package/dist/types/crash/nudge.d.ts +50 -0
- package/dist/types/crash/record-loader.d.ts +17 -0
- package/dist/types/crash/report.d.ts +134 -0
- package/dist/types/crash/sanitize.d.ts +25 -0
- package/dist/types/cursor.d.ts +13 -2
- package/dist/types/customization/import.d.ts +37 -0
- package/dist/types/customization/index.d.ts +4 -0
- package/dist/types/customization/inventory.d.ts +25 -0
- package/dist/types/customization/mutations.d.ts +32 -0
- package/dist/types/customization/types.d.ts +115 -0
- package/dist/types/daemon/control-types.d.ts +8 -1
- package/dist/types/daemon/operator-contract.d.ts +4 -4
- package/dist/types/discovery/claude.d.ts +39 -1
- package/dist/types/discovery/codex.d.ts +39 -1
- package/dist/types/discovery/index.d.ts +2 -0
- package/dist/types/discovery/mcp-compat.d.ts +30 -0
- package/dist/types/edit/renderer.d.ts +23 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +21 -4
- package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
- package/dist/types/extensibility/hooks/loader.d.ts +19 -5
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
- package/dist/types/extensibility/skill-management.d.ts +107 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
- package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
- package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
- package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
- package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
- package/dist/types/hooks/events.d.ts +60 -0
- package/dist/types/hooks/normalize.d.ts +83 -0
- package/dist/types/hooks/skill-state.d.ts +9 -2
- package/dist/types/main.d.ts +2 -1
- package/dist/types/modes/acp/abort-scope.d.ts +13 -0
- package/dist/types/modes/acp/acp-agent.d.ts +7 -4
- package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
- package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
- package/dist/types/modes/components/customization/index.d.ts +1 -0
- package/dist/types/modes/components/dynamic-theme-text.d.ts +9 -0
- package/dist/types/modes/components/gajae-pet-widget.d.ts +6 -28
- package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
- package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
- package/dist/types/modes/components/oauth-selector.d.ts +10 -4
- package/dist/types/modes/components/pet-capability.d.ts +7 -21
- package/dist/types/modes/components/settings-selector.d.ts +5 -0
- package/dist/types/modes/components/status-line/gh.d.ts +6 -2
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
- package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/theme/defaults/index.d.ts +118 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
- package/dist/types/runtime-mcp/config.d.ts +9 -0
- package/dist/types/runtime-mcp/manager.d.ts +26 -1
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
- package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
- package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
- package/dist/types/runtime-mcp/protocol.d.ts +278 -0
- package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
- package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
- package/dist/types/runtime-mcp/types.d.ts +102 -10
- package/dist/types/sdk/acp/adapter.d.ts +26 -5
- package/dist/types/sdk/broker/broker.d.ts +3 -55
- package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
- package/dist/types/sdk/broker/session-index.d.ts +14 -5
- package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
- package/dist/types/sdk/bus/config.d.ts +11 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
- package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
- package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
- package/dist/types/sdk/bus/index.d.ts +85 -99
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
- package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
- package/dist/types/sdk/bus/notification-service.d.ts +52 -0
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
- package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
- package/dist/types/sdk/bus/session-control.d.ts +25 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
- package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
- package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
- package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
- package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
- package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
- package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
- package/dist/types/sdk/cli/rows.d.ts +2 -11
- package/dist/types/sdk/cli/session-cli.d.ts +15 -25
- package/dist/types/sdk/client/client.d.ts +97 -1
- package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +18 -5
- package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
- package/dist/types/sdk/host/host.d.ts +15 -1
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/host/session-runtime.d.ts +127 -7
- package/dist/types/sdk/host/types.d.ts +1 -1
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +2 -1
- package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
- package/dist/types/sdk/lifecycle/client.d.ts +59 -0
- package/dist/types/sdk/lifecycle/index.d.ts +3 -0
- package/dist/types/sdk/lifecycle/service.d.ts +232 -0
- package/dist/types/sdk/mcp/server.d.ts +9 -6
- package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
- package/dist/types/sdk/prompt-status.d.ts +10 -3
- package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
- package/dist/types/sdk/receipt-state.d.ts +17 -0
- package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
- package/dist/types/sdk/router/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +152 -0
- package/dist/types/sdk/session-list.d.ts +22 -0
- package/dist/types/sdk/session-reconnect.d.ts +43 -1
- package/dist/types/sdk/session.d.ts +34 -1
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
- package/dist/types/session/account-inventory.d.ts +86 -0
- package/dist/types/session/agent-session.d.ts +145 -6
- package/dist/types/session/agent-storage.d.ts +10 -0
- package/dist/types/session/coordinator-tool-label.d.ts +28 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
- package/dist/types/session/messages.d.ts +1 -1
- package/dist/types/session/secure-token-file.d.ts +3 -0
- package/dist/types/session/session-manager.d.ts +47 -1
- package/dist/types/session/startup-auth-config.d.ts +44 -0
- package/dist/types/session/terminal-abort.d.ts +378 -0
- package/dist/types/session/yield-queue.d.ts +7 -0
- package/dist/types/session-import/claude.d.ts +26 -0
- package/dist/types/session-import/codex.d.ts +88 -0
- package/dist/types/session-import/command.d.ts +35 -0
- package/dist/types/session-import/detect.d.ts +22 -0
- package/dist/types/session-import/index.d.ts +7 -0
- package/dist/types/session-import/provider-codex.d.ts +22 -0
- package/dist/types/session-import/provider-service.d.ts +38 -0
- package/dist/types/session-import/redact.d.ts +23 -0
- package/dist/types/session-import/service.d.ts +47 -0
- package/dist/types/session-import/types.d.ts +120 -0
- package/dist/types/setup/paseo/check.d.ts +16 -0
- package/dist/types/setup/paseo/install-saga.d.ts +115 -0
- package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
- package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
- package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
- package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
- package/dist/types/setup/paseo/provider-config.d.ts +35 -0
- package/dist/types/setup/paseo/remove.d.ts +14 -0
- package/dist/types/setup/paseo/result-types.d.ts +59 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
- package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
- package/dist/types/skill-state/active-state.d.ts +5 -1
- package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
- package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
- package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
- package/dist/types/slash-commands/types.d.ts +7 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/tools/ask-answer-registry.d.ts +6 -3
- package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
- package/dist/types/tools/bash.d.ts +2 -6
- package/dist/types/tools/browser/attach.d.ts +11 -0
- package/dist/types/tools/browser/launch.d.ts +20 -0
- package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
- package/dist/types/tools/browser/render.d.ts +1 -0
- package/dist/types/tools/browser.d.ts +3 -1
- package/dist/types/tools/image-gen.d.ts +10 -2
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/tools/monitor.d.ts +1 -1
- package/dist/types/tools/skill-discovery.d.ts +8 -1
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
- package/dist/types/utils/herdr-pane.d.ts +76 -0
- package/package.json +19 -9
- package/scripts/build-binary.ts +2 -1
- package/scripts/build-sdk-package-smoke.ts +2 -13
- package/scripts/capture-sticky-viewport-showcase.ts +0 -1
- package/scripts/compile-args.ts +2 -0
- package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
- package/scripts/generate-sdk-operation-inventory.ts +28 -0
- package/scripts/generate-telegram-baseline-manifest.ts +1 -0
- package/scripts/generate-tool-catalog.ts +6 -4
- package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
- package/src/async/job-manager.ts +352 -17
- package/src/async/support.ts +25 -0
- package/src/autoresearch/dashboard.ts +28 -13
- package/src/capability/index.ts +3 -1
- package/src/capability/mcp.ts +6 -0
- package/src/capability/types.ts +6 -0
- package/src/cli/accounts-cli.ts +395 -0
- package/src/cli/args.ts +23 -0
- package/src/cli/auth-broker-cli.ts +136 -66
- package/src/cli/auth-gateway-cli.ts +94 -70
- package/src/cli/config-cli.ts +27 -2
- package/src/cli/crash-cli.ts +78 -0
- package/src/cli/customize-doctor.ts +1605 -0
- package/src/cli/daemon-cli.ts +12 -3
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/mcp-cli.ts +128 -60
- package/src/cli/notify-cli.ts +8 -14
- package/src/cli/root-flags.ts +8 -0
- package/src/cli/setup-cli.ts +145 -0
- package/src/cli/skills-cli.ts +38 -2
- package/src/cli/update-cli.ts +167 -12
- package/src/cli.ts +4 -0
- package/src/commands/accounts.ts +71 -0
- package/src/commands/crash.ts +35 -0
- package/src/commands/customize.ts +84 -0
- package/src/commands/daemon.ts +2 -2
- package/src/commands/deep-interview.ts +2 -0
- package/src/commands/launch.ts +1 -0
- package/src/commands/mcp.ts +12 -7
- package/src/commands/ralplan.ts +2 -0
- package/src/commands/sdk.ts +65 -32
- package/src/commands/setup.ts +5 -0
- package/src/commands/skills.ts +14 -4
- package/src/commands/ultragoal.ts +10 -1
- package/src/commands/update.ts +26 -15
- package/src/config/atomic-yaml-patch-worker.ts +57 -0
- package/src/config/atomic-yaml-patch.ts +769 -29
- package/src/config/config-schema-version.ts +10 -0
- package/src/config/file-lock.ts +70 -1
- package/src/config/mcp-schema.json +52 -14
- package/src/config/model-profile-activation.ts +23 -2
- package/src/config/model-profiles.ts +88 -21
- package/src/config/model-registry.ts +147 -21
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +77 -7
- package/src/config/settings.ts +4038 -199
- package/src/config/skill-settings-defaults.ts +41 -3
- package/src/config/task-delegation.ts +87 -0
- package/src/config/update-channel.ts +23 -0
- package/src/coordinator-mcp/server.ts +377 -308
- package/src/crash/index-store.ts +723 -0
- package/src/crash/nudge.ts +89 -0
- package/src/crash/record-loader.ts +149 -0
- package/src/crash/report.ts +551 -0
- package/src/crash/sanitize.ts +129 -0
- package/src/cursor.ts +36 -10
- package/src/customization/import.ts +946 -0
- package/src/customization/index.ts +4 -0
- package/src/customization/inventory.ts +335 -0
- package/src/customization/mutations.ts +141 -0
- package/src/customization/types.ts +223 -0
- package/src/daemon/control-types.ts +8 -1
- package/src/daemon/operator-contract.ts +6 -6
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +31 -6
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
- package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
- package/src/discovery/claude-hooks.ts +38 -0
- package/src/discovery/claude.ts +142 -209
- package/src/discovery/codex-hooks.ts +38 -0
- package/src/discovery/codex.ts +132 -198
- package/src/discovery/helpers.ts +8 -1
- package/src/discovery/index.ts +2 -0
- package/src/discovery/mcp-compat.ts +234 -0
- package/src/discovery/mcp-json.ts +6 -0
- package/src/edit/renderer.ts +40 -0
- package/src/edit/streaming.ts +4 -2
- package/src/exec/bash-executor.ts +24 -20
- package/src/export/html/index.ts +51 -2
- package/src/extensibility/extensions/loader.ts +9 -1
- package/src/extensibility/extensions/runner.ts +17 -4
- package/src/extensibility/extensions/types.ts +31 -6
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
- package/src/extensibility/gjc-plugins/installer.ts +9 -5
- package/src/extensibility/gjc-plugins/observability.ts +8 -2
- package/src/extensibility/gjc-plugins/registry.ts +13 -2
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
- package/src/extensibility/gjc-plugins/schema.ts +222 -39
- package/src/extensibility/hooks/loader.ts +154 -10
- package/src/extensibility/runtime-skill-discovery.ts +257 -60
- package/src/extensibility/skill-management.ts +368 -0
- package/src/extensibility/skills.ts +52 -16
- package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
- package/src/gjc-runtime/launch-worktree.ts +56 -2
- package/src/gjc-runtime/linux-proc.ts +7 -1
- package/src/gjc-runtime/ralplan-runtime.ts +100 -164
- package/src/gjc-runtime/session-state-lock.ts +992 -0
- package/src/gjc-runtime/session-state-sidecar.ts +644 -142
- package/src/gjc-runtime/state-writer.ts +39 -14
- package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
- package/src/gjc-runtime/ultragoal-guard.ts +11 -4
- package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
- package/src/gjc-runtime/workflow-settings.ts +596 -0
- package/src/goals/tools/goal-tool.ts +6 -1
- package/src/hooks/events.ts +291 -0
- package/src/hooks/native-skill-hook.ts +4 -0
- package/src/hooks/normalize.ts +369 -0
- package/src/hooks/skill-state.ts +165 -46
- package/src/internal-urls/docs-index.generated.ts +41 -32
- package/src/main.ts +36 -5
- package/src/migrate/mcp-mapper.ts +25 -2
- package/src/modes/acp/abort-scope.ts +29 -0
- package/src/modes/acp/acp-agent.ts +423 -98
- package/src/modes/components/assistant-message.ts +6 -1
- package/src/modes/components/chrome.ts +4 -3
- package/src/modes/components/customization/customization-dashboard.ts +443 -0
- package/src/modes/components/customization/import-wizard.ts +372 -0
- package/src/modes/components/customization/index.ts +1 -0
- package/src/modes/components/dynamic-theme-text.ts +22 -0
- package/src/modes/components/gajae-pet-widget.ts +607 -193
- package/src/modes/components/irc-sidebar.ts +11 -0
- package/src/modes/components/iterm-pet-transport.ts +585 -0
- package/src/modes/components/model-selector.ts +1 -6
- package/src/modes/components/oauth-selector.ts +277 -86
- package/src/modes/components/pet-capability.ts +35 -31
- package/src/modes/components/settings-selector.ts +29 -5
- package/src/modes/components/status-line/gh.ts +45 -1
- package/src/modes/components/status-line/segments.ts +19 -4
- package/src/modes/components/tool-status-header.ts +13 -3
- package/src/modes/controllers/command-controller.ts +20 -8
- package/src/modes/controllers/extension-ui-controller.ts +56 -257
- package/src/modes/controllers/input-controller.ts +10 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
- package/src/modes/controllers/selector-controller.ts +238 -77
- package/src/modes/controllers/stt-controller.ts +3 -3
- package/src/modes/interactive-mode.ts +88 -16
- package/src/modes/print-mode.ts +35 -6
- package/src/modes/runtime-init.ts +21 -68
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/theme/defaults/ouroboros.json +118 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/injected-user-submission.ts +25 -0
- package/src/modes/utils/terminal-bell.ts +15 -1
- package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/critic.md +1 -1
- package/src/prompts/system/rlm-research.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -2
- package/src/prompts/tools/bash.md +7 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/cron.md +6 -6
- package/src/prompts/tools/read.md +0 -2
- package/src/prompts/tools/skill-discovery.md +4 -2
- package/src/runtime-mcp/client.ts +427 -23
- package/src/runtime-mcp/config.ts +37 -5
- package/src/runtime-mcp/manager-pool.test.ts +56 -0
- package/src/runtime-mcp/manager.ts +52 -2
- package/src/runtime-mcp/oauth-discovery.ts +13 -0
- package/src/runtime-mcp/oauth-flow.ts +46 -1
- package/src/runtime-mcp/pool-key.ts +5 -1
- package/src/runtime-mcp/pool.test.ts +9 -0
- package/src/runtime-mcp/protocol.test.ts +353 -0
- package/src/runtime-mcp/protocol.ts +624 -0
- package/src/runtime-mcp/scope-config.ts +78 -0
- package/src/runtime-mcp/tool-bridge.ts +44 -10
- package/src/runtime-mcp/transports/http.ts +84 -13
- package/src/runtime-mcp/types.ts +123 -12
- package/src/sdk/acp/adapter.ts +328 -97
- package/src/sdk/broker/broker.ts +160 -741
- package/src/sdk/broker/ensure.ts +9 -1
- package/src/sdk/broker/lifecycle.ts +510 -116
- package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
- package/src/sdk/broker/process-incarnation.ts +19 -0
- package/src/sdk/broker/session-index.ts +516 -117
- package/src/sdk/broker/startup-failure.ts +28 -5
- package/src/sdk/broker/transport.ts +1 -13
- package/src/sdk/bus/chat-daemon-cli.ts +4 -2
- package/src/sdk/bus/chat-daemon-control.ts +67 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
- package/src/sdk/bus/config.ts +13 -0
- package/src/sdk/bus/conversation-store.ts +59 -171
- package/src/sdk/bus/daemon-paths.ts +20 -0
- package/src/sdk/bus/discord-conversation.ts +2 -0
- package/src/sdk/bus/discord-daemon.ts +524 -126
- package/src/sdk/bus/discord-live-provider.ts +206 -79
- package/src/sdk/bus/existing-thread-readiness.ts +4 -97
- package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
- package/src/sdk/bus/index.ts +2480 -456
- package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
- package/src/sdk/bus/lifecycle-commands.ts +2 -2
- package/src/sdk/bus/managed-daemon.ts +2 -145
- package/src/sdk/bus/notification-service.ts +196 -0
- package/src/sdk/bus/operator-runtime.ts +1 -1
- package/src/sdk/bus/prompt-reconciliation.ts +24 -15
- package/src/sdk/bus/reconciliation-store.ts +553 -24
- package/src/sdk/bus/session-control.ts +49 -3
- package/src/sdk/bus/slack-conversation.ts +4 -0
- package/src/sdk/bus/slack-daemon.ts +733 -134
- package/src/sdk/bus/slack-live-provider.ts +65 -15
- package/src/sdk/bus/slack-provider.ts +4 -0
- package/src/sdk/bus/slack-thread-binding.ts +9 -104
- package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
- package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
- package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
- package/src/sdk/bus/telegram-daemon-control.ts +3 -13
- package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
- package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
- package/src/sdk/bus/telegram-daemon.ts +2552 -2280
- package/src/sdk/bus/telegram-reference.ts +1 -226
- package/src/sdk/bus/telegram-setup.ts +2 -2
- package/src/sdk/bus/threaded-render.ts +1 -0
- package/src/sdk/bus/topic-registry.ts +279 -146
- package/src/sdk/cli/rows.ts +2 -35
- package/src/sdk/cli/session-cli.ts +776 -700
- package/src/sdk/client/client.ts +892 -1
- package/src/sdk/host/control/dispatch.ts +93 -58
- package/src/sdk/host/control/index.ts +1 -1
- package/src/sdk/host/control/operations.ts +18 -10
- package/src/sdk/host/control/runtime-gate.ts +40 -0
- package/src/sdk/host/host.ts +63 -12
- package/src/sdk/host/query/handlers.ts +66 -7
- package/src/sdk/host/reverse-leases.ts +2 -2
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +2787 -34
- package/src/sdk/host/session-runtime.ts +1489 -101
- package/src/sdk/host/surface-policy.ts +12 -8
- package/src/sdk/host/types.ts +1 -1
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
- package/src/sdk/host/websocket-transport.ts +85 -50
- package/src/sdk/index.ts +2 -1
- package/src/sdk/lifecycle/broker-client.ts +53 -0
- package/src/sdk/lifecycle/client.ts +234 -0
- package/src/sdk/lifecycle/index.ts +3 -0
- package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
- package/src/sdk/lifecycle/service.ts +720 -0
- package/src/sdk/mcp/server.ts +226 -188
- package/src/sdk/prompt-deadline-lease.ts +55 -0
- package/src/sdk/prompt-deadline-manager.ts +148 -0
- package/src/sdk/prompt-status.ts +11 -3
- package/src/sdk/protocol/operation-inventory.generated.json +272 -129
- package/src/sdk/protocol/operation-registry.ts +71 -8
- package/src/sdk/receipt-state.ts +43 -0
- package/src/sdk/reconciliation-extensions.ts +14 -0
- package/src/sdk/router/index.ts +1 -0
- package/src/sdk/router/session-router.ts +1658 -0
- package/src/sdk/session-list.ts +93 -0
- package/src/sdk/session-reconnect.ts +45 -1
- package/src/sdk/session.ts +716 -120
- package/src/sdk/transport/serve-cli.ts +18 -34
- package/src/session/account-inventory.ts +603 -0
- package/src/session/agent-session.ts +2873 -559
- package/src/session/agent-storage.ts +35 -0
- package/src/session/blob-store.ts +32 -1
- package/src/session/coordinator-tool-label.ts +40 -0
- package/src/session/internal/managed-session-scope.ts +4 -0
- package/src/session/internal/managed-session-storage.ts +199 -31
- package/src/session/messages.ts +1 -1
- package/src/session/secure-token-file.ts +80 -0
- package/src/session/session-manager.ts +757 -57
- package/src/session/session-storage.ts +25 -16
- package/src/session/startup-auth-config.ts +297 -0
- package/src/session/terminal-abort.ts +1253 -0
- package/src/session/yield-queue.ts +57 -15
- package/src/session-import/claude.ts +334 -0
- package/src/session-import/codex.ts +1033 -0
- package/src/session-import/command.ts +77 -0
- package/src/session-import/detect.ts +133 -0
- package/src/session-import/index.ts +7 -0
- package/src/session-import/provider-codex.ts +327 -0
- package/src/session-import/provider-service.ts +457 -0
- package/src/session-import/redact.ts +128 -0
- package/src/session-import/service.ts +1264 -0
- package/src/session-import/types.ts +148 -0
- package/src/setup/paseo/check.ts +213 -0
- package/src/setup/paseo/install-saga.ts +293 -0
- package/src/setup/paseo/json-publisher.ts +263 -0
- package/src/setup/paseo/orchestration-preferences.ts +86 -0
- package/src/setup/paseo/paseo-ownership.ts +224 -0
- package/src/setup/paseo/paseo-setup.ts +253 -0
- package/src/setup/paseo/provider-config.ts +139 -0
- package/src/setup/paseo/remove.ts +183 -0
- package/src/setup/paseo/result-types.ts +79 -0
- package/src/setup/paseo/setup-deps.ts +175 -0
- package/src/setup/paseo/skills-bridge.ts +260 -0
- package/src/skill-state/active-state.ts +21 -11
- package/src/slash-commands/acp-builtins.ts +9 -1
- package/src/slash-commands/builtin-registry.ts +117 -19
- package/src/slash-commands/helpers/credential-switch.ts +143 -0
- package/src/slash-commands/helpers/mcp.ts +9 -3
- package/src/slash-commands/helpers/usage-report.ts +61 -56
- package/src/slash-commands/types.ts +7 -2
- package/src/task/executor.ts +30 -11
- package/src/task/index.ts +66 -7
- package/src/task/render.ts +23 -9
- package/src/tools/ask-answer-registry.ts +23 -7
- package/src/tools/ask.ts +8 -4
- package/src/tools/bash-sleep-advisory.ts +115 -0
- package/src/tools/bash.ts +98 -18
- package/src/tools/browser/attach.ts +71 -3
- package/src/tools/browser/launch.ts +86 -13
- package/src/tools/browser/profile-discovery.ts +57 -7
- package/src/tools/browser/render.ts +8 -1
- package/src/tools/browser.ts +130 -21
- package/src/tools/computer.enforcement.test.ts +1 -1
- package/src/tools/computer.ts +3 -2
- package/src/tools/cron.ts +12 -1
- package/src/tools/image-gen.ts +349 -142
- package/src/tools/index.ts +8 -0
- package/src/tools/job.ts +33 -6
- package/src/tools/monitor.ts +41 -2
- package/src/tools/skill-discovery.ts +21 -29
- package/src/tools/subagent.ts +142 -24
- package/src/tools/todo-contract.ts +55 -3
- package/src/tools/todo-write.ts +4 -3
- package/src/tools/tool-catalog.generated.ts +6 -6
- package/src/tools/ultragoal-ask-guard.ts +10 -3
- package/src/utils/herdr-pane.ts +345 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/scrapers/index.ts +6 -4
- package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
- package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
- package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
- package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
- package/dist/types/runtime/lazy-service.test.d.ts +0 -1
- package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
- package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
- package/dist/types/runtime-mcp/config.test.d.ts +0 -1
- package/dist/types/runtime-mcp/http.test.d.ts +0 -1
- package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
- package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
- package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
- package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
- package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
- package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
- package/dist/types/sdk/elevation/audit.d.ts +0 -93
- package/dist/types/sdk/elevation/capability.d.ts +0 -3
- package/dist/types/sdk/elevation/digest.d.ts +0 -72
- package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
- package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
- package/dist/types/sdk/elevation/owner.d.ts +0 -48
- package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
- package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
- package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
- package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
- package/dist/types/session/auth-broker-config.d.ts +0 -13
- package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
- package/dist/types/skills/skills.test.d.ts +0 -1
- package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
- package/dist/types/task/token-log.test.d.ts +0 -1
- package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
- package/dist/types/tools/descriptors.test.d.ts +0 -1
- package/scripts/g005-daemon-smoke.ts +0 -180
- package/scripts/g011-daemon-path-smoke.ts +0 -153
- package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
- package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
- package/src/sdk/bus/telegram-cli.ts +0 -212
- package/src/sdk/elevation/allowlist.ts +0 -54
- package/src/sdk/elevation/audit.ts +0 -208
- package/src/sdk/elevation/capability.ts +0 -41
- package/src/sdk/elevation/digest.ts +0 -129
- package/src/sdk/elevation/dispatch-receipt.ts +0 -184
- package/src/sdk/elevation/grant-ledger.ts +0 -1142
- package/src/sdk/elevation/owner.ts +0 -109
- package/src/session/auth-broker-config.ts +0 -109
- /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
- /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
- /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
package/src/sdk/client/client.ts
CHANGED
|
@@ -1 +1,892 @@
|
|
|
1
|
-
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export type SdkErrorCode =
|
|
4
|
+
| "invalid_input"
|
|
5
|
+
| "unknown_operation"
|
|
6
|
+
| "not_found"
|
|
7
|
+
| "unavailable"
|
|
8
|
+
| "timeout"
|
|
9
|
+
| "connection_closed"
|
|
10
|
+
| "uncertain_after_send"
|
|
11
|
+
| "endpoint_credential_forbidden"
|
|
12
|
+
| (string & {});
|
|
13
|
+
|
|
14
|
+
export class SdkClientError extends Error {
|
|
15
|
+
readonly code: SdkErrorCode;
|
|
16
|
+
readonly details: unknown;
|
|
17
|
+
/**
|
|
18
|
+
* Reconnect-cycle diagnostics, separate from `details` because `details` is an
|
|
19
|
+
* established contract: callers read the terminating transport error straight off
|
|
20
|
+
* it (`session-cli.ts` matches `details.code` against `ENOENT`/`ECONNREFUSED`, and
|
|
21
|
+
* lifecycle callers cast it to a sent record). Wrapping that value would silently
|
|
22
|
+
* break every such reader, so the new attribution rides alongside it instead.
|
|
23
|
+
*/
|
|
24
|
+
readonly reconnect?: SdkReconnectExhaustedDetails;
|
|
25
|
+
constructor(code: SdkErrorCode, message: string, details?: unknown, reconnect?: SdkReconnectExhaustedDetails) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "SdkClientError";
|
|
28
|
+
this.code = code;
|
|
29
|
+
this.details = details;
|
|
30
|
+
if (reconnect) this.reconnect = reconnect;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type SdkReconnectTerminationReason = "attempts_exhausted" | "deadline" | "cancelled";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reconnect-cycle termination diagnostics. `attemptsConsumed` counts retry slots,
|
|
38
|
+
* not socket opens: the initial open is free, so the loop can open one more socket
|
|
39
|
+
* than `attemptBudget`. Both values are therefore directly comparable.
|
|
40
|
+
*
|
|
41
|
+
* `reason` is authoritative. `attemptsConsumed < attemptBudget` is corroborating
|
|
42
|
+
* evidence of truncation, not a classifier: a zero-attempt client that trips its
|
|
43
|
+
* deadline immediately reports `0 === 0` and is still deadline-terminated.
|
|
44
|
+
*/
|
|
45
|
+
export interface SdkReconnectExhaustedDetails {
|
|
46
|
+
readonly attemptsConsumed: number;
|
|
47
|
+
readonly attemptBudget: number;
|
|
48
|
+
readonly elapsedMs: number;
|
|
49
|
+
readonly reason: SdkReconnectTerminationReason;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Deadline a one-shot request client waits for a reply. Long-lived session
|
|
54
|
+
* clients override it per request; see `SESSION_REQUEST_TIMEOUT_MS`.
|
|
55
|
+
*/
|
|
56
|
+
export const DEFAULT_SDK_REQUEST_TIMEOUT_MS = 10_000;
|
|
57
|
+
|
|
58
|
+
export interface SdkClientOptions {
|
|
59
|
+
timeoutMs?: number;
|
|
60
|
+
/** Absolute wall-clock deadline shared by connect, hello, retry, and request work. */
|
|
61
|
+
deadline?: number;
|
|
62
|
+
|
|
63
|
+
reconnectAttempts?: number;
|
|
64
|
+
reconnectBackoffMs?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Per-attempt ceiling for the exponential reconnect backoff. A long reconnect
|
|
67
|
+
* budget must keep probing frequently instead of sleeping for tens of seconds
|
|
68
|
+
* on its last attempts. Defaults to 2s.
|
|
69
|
+
*/
|
|
70
|
+
reconnectMaxBackoffMs?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface SdkRequestOptions {
|
|
74
|
+
timeoutMs?: number;
|
|
75
|
+
idempotencyKey?: string;
|
|
76
|
+
confirm?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type SdkFrame = Record<string, unknown>;
|
|
80
|
+
|
|
81
|
+
/** Request identity retained after an uncertain send for lifecycle reconciliation. */
|
|
82
|
+
|
|
83
|
+
export interface SdkSentRecord {
|
|
84
|
+
readonly id: string;
|
|
85
|
+
readonly operation?: string;
|
|
86
|
+
readonly idempotencyKey?: string;
|
|
87
|
+
readonly fingerprint: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type SdkFrameHandler = (frame: SdkFrame) => void;
|
|
91
|
+
export type SdkReconnectHandler = () => void;
|
|
92
|
+
export type SdkReconnectFailedHandler = (error: SdkClientError) => void;
|
|
93
|
+
|
|
94
|
+
type Frame = SdkFrame;
|
|
95
|
+
type Cycle = {
|
|
96
|
+
readonly generation: number;
|
|
97
|
+
phase: "opening" | "backoff" | "complete" | "aborted";
|
|
98
|
+
candidate: Incarnation | null;
|
|
99
|
+
promise?: Promise<Incarnation>;
|
|
100
|
+
backoffTimer?: NodeJS.Timeout;
|
|
101
|
+
rejectBackoff?: (error: Error) => void;
|
|
102
|
+
};
|
|
103
|
+
type Incarnation = {
|
|
104
|
+
readonly generation: number;
|
|
105
|
+
readonly cycle: Cycle;
|
|
106
|
+
readonly socket: WebSocket;
|
|
107
|
+
phase: "opening" | "hello" | "active" | "retired";
|
|
108
|
+
tornDown: boolean;
|
|
109
|
+
openTimer?: NodeJS.Timeout;
|
|
110
|
+
failure?: Error;
|
|
111
|
+
helloTimer?: NodeJS.Timeout;
|
|
112
|
+
/** Hello frames that arrived before the open handler advanced phase to "hello". */
|
|
113
|
+
earlyHello?: Frame;
|
|
114
|
+
resolveOpen?: () => void;
|
|
115
|
+
rejectOpen?: (error: Error) => void;
|
|
116
|
+
resolveHello?: () => void;
|
|
117
|
+
rejectHello?: (error: Error) => void;
|
|
118
|
+
listeners: Array<["open" | "error" | "close" | "message", EventListener]>;
|
|
119
|
+
};
|
|
120
|
+
type Pending = {
|
|
121
|
+
readonly incarnation: Incarnation;
|
|
122
|
+
resolve: (value: unknown) => void;
|
|
123
|
+
reject: (error: Error) => void;
|
|
124
|
+
timer: NodeJS.Timeout;
|
|
125
|
+
sent: boolean;
|
|
126
|
+
onResponse?: () => void;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Transport facts attached to a request timeout before an outcome is known.
|
|
131
|
+
* A timeout after send is surfaced as `uncertain_after_send` with an
|
|
132
|
+
* {@link SdkSentRecord} in its details.
|
|
133
|
+
*/
|
|
134
|
+
export interface SdkRequestTimeoutDetails {
|
|
135
|
+
requestId: string;
|
|
136
|
+
requestSent: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function errorFrom(frame: Frame): SdkClientError {
|
|
140
|
+
const error = frame.error;
|
|
141
|
+
if (error && typeof error === "object") {
|
|
142
|
+
const detail = error as { code?: unknown; message?: unknown };
|
|
143
|
+
return new SdkClientError(
|
|
144
|
+
typeof detail.code === "string" ? detail.code : "unavailable",
|
|
145
|
+
typeof detail.message === "string" ? detail.message : "SDK request failed",
|
|
146
|
+
error,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return new SdkClientError("unavailable", "SDK request failed", error);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseFrame(value: unknown): Frame {
|
|
153
|
+
try {
|
|
154
|
+
const frame = JSON.parse(String(value));
|
|
155
|
+
if (frame && typeof frame === "object" && !Array.isArray(frame)) return frame as Frame;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw new SdkClientError("protocol_error", "SDK server sent malformed JSON.", error);
|
|
158
|
+
}
|
|
159
|
+
throw new SdkClientError("protocol_error", "SDK server sent a malformed frame.");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function lifecycleFingerprint(operation: string, input: unknown): string {
|
|
163
|
+
return createHash("sha256").update(JSON.stringify({ operation, input })).digest("hex");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function inputFingerprint(input: unknown): string {
|
|
167
|
+
return createHash("sha256").update(JSON.stringify(input)).digest("hex");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A transport-only v3 SDK WebSocket client with no host or session authority. */
|
|
171
|
+
export class SdkClient {
|
|
172
|
+
readonly #url: string;
|
|
173
|
+
readonly #token: string;
|
|
174
|
+
readonly #timeoutMs: number;
|
|
175
|
+
readonly #reconnectAttempts: number;
|
|
176
|
+
readonly #reconnectBackoffMs: number;
|
|
177
|
+
readonly #reconnectMaxBackoffMs: number;
|
|
178
|
+
/**
|
|
179
|
+
* Bounded grace for best-effort transport close, independent of the request
|
|
180
|
+
* deadline. Close teardown must never be gated by an already-elapsed operation
|
|
181
|
+
* deadline, or the socket leaks.
|
|
182
|
+
*/
|
|
183
|
+
readonly #closeGraceMs: number;
|
|
184
|
+
readonly #deadline?: number;
|
|
185
|
+
#currentSocketRecord: Incarnation | null = null;
|
|
186
|
+
#opening: Cycle | null = null;
|
|
187
|
+
#cycleGeneration = 0;
|
|
188
|
+
#incarnationGeneration = 0;
|
|
189
|
+
#pending = new Map<string, Pending>();
|
|
190
|
+
#sentRecords = new Map<string, SdkSentRecord>();
|
|
191
|
+
#frameHandlers = new Set<SdkFrameHandler>();
|
|
192
|
+
#reconnectHandlers = new Set<SdkReconnectHandler>();
|
|
193
|
+
#reconnectFailedHandlers = new Set<SdkReconnectFailedHandler>();
|
|
194
|
+
#closePromise: Promise<void> | undefined;
|
|
195
|
+
|
|
196
|
+
#closed = false;
|
|
197
|
+
connectionId?: string;
|
|
198
|
+
|
|
199
|
+
constructor(url: string, token: string, options: SdkClientOptions = {}) {
|
|
200
|
+
this.#url = url;
|
|
201
|
+
this.#token = token;
|
|
202
|
+
this.#timeoutMs = options.timeoutMs ?? DEFAULT_SDK_REQUEST_TIMEOUT_MS;
|
|
203
|
+
this.#closeGraceMs = Math.max(1, Math.min(this.#timeoutMs, 1_000));
|
|
204
|
+
this.#deadline =
|
|
205
|
+
typeof options.deadline === "number" && Number.isFinite(options.deadline) ? options.deadline : undefined;
|
|
206
|
+
|
|
207
|
+
this.#reconnectAttempts = options.reconnectAttempts ?? 3;
|
|
208
|
+
this.#reconnectBackoffMs = options.reconnectBackoffMs ?? 25;
|
|
209
|
+
this.#reconnectMaxBackoffMs = Math.max(this.#reconnectBackoffMs, options.reconnectMaxBackoffMs ?? 2_000);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
static async connect(url: string, token: string, options: SdkClientOptions = {}): Promise<SdkClient> {
|
|
213
|
+
const client = new SdkClient(url, token, options);
|
|
214
|
+
await client.connect();
|
|
215
|
+
return client;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async connect(): Promise<void> {
|
|
219
|
+
await this.#connect();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Resolves once the current WebSocket has received its server hello frame. */
|
|
223
|
+
async awaitHello(): Promise<void> {
|
|
224
|
+
await this.#connect();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
onFrame(handler: SdkFrameHandler): () => void {
|
|
228
|
+
this.#frameHandlers.add(handler);
|
|
229
|
+
return () => this.#frameHandlers.delete(handler);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
onReconnect(handler: SdkReconnectHandler): () => void {
|
|
233
|
+
this.#reconnectHandlers.add(handler);
|
|
234
|
+
return () => this.#reconnectHandlers.delete(handler);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
onReconnectFailed(handler: SdkReconnectFailedHandler): () => void {
|
|
238
|
+
this.#reconnectFailedHandlers.add(handler);
|
|
239
|
+
return () => this.#reconnectFailedHandlers.delete(handler);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
send(frame: SdkFrame): void {
|
|
243
|
+
if (this.#closed) throw new SdkClientError("connection_closed", "SDK client closed");
|
|
244
|
+
this.#throwIfDeadlineElapsed();
|
|
245
|
+
const current = this.#currentSocketRecord ?? this.#opening?.candidate;
|
|
246
|
+
const authoritative =
|
|
247
|
+
this.#isActive(current ?? null) ||
|
|
248
|
+
(!!current && current.phase === "hello" && this.#isCandidate(current.cycle, current));
|
|
249
|
+
if (!current || !authoritative || current.socket.readyState !== WebSocket.OPEN)
|
|
250
|
+
throw new SdkClientError("connection_closed", "SDK WebSocket is not connected");
|
|
251
|
+
try {
|
|
252
|
+
current.socket.send(JSON.stringify(frame));
|
|
253
|
+
} catch (error) {
|
|
254
|
+
throw new SdkClientError("unavailable", "SDK WebSocket send failed", error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
request(frame: SdkFrame, timeout?: number | { timeoutMs?: number; idempotencyKey?: string }): Promise<SdkFrame> {
|
|
259
|
+
const options = typeof timeout === "number" ? { timeoutMs: timeout } : (timeout ?? {});
|
|
260
|
+
return this.#request(frame, options) as Promise<SdkFrame>;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
close(): Promise<void> {
|
|
264
|
+
this.#closePromise ??= this.#close();
|
|
265
|
+
return this.#closePromise;
|
|
266
|
+
}
|
|
267
|
+
async #close(): Promise<void> {
|
|
268
|
+
this.#closed = true;
|
|
269
|
+
const transports = new Set<Incarnation>();
|
|
270
|
+
const cycle = this.#opening;
|
|
271
|
+
if (cycle) {
|
|
272
|
+
cycle.phase = "aborted";
|
|
273
|
+
if (cycle.backoffTimer) clearTimeout(cycle.backoffTimer);
|
|
274
|
+
if (cycle.candidate) {
|
|
275
|
+
transports.add(cycle.candidate);
|
|
276
|
+
this.#retire(cycle.candidate, new SdkClientError("connection_closed", "SDK client closed"), false);
|
|
277
|
+
}
|
|
278
|
+
cycle.rejectBackoff?.(new SdkClientError("connection_closed", "SDK client closed"));
|
|
279
|
+
cycle.rejectBackoff = undefined;
|
|
280
|
+
if (this.#opening === cycle) this.#opening = null;
|
|
281
|
+
}
|
|
282
|
+
const current = this.#currentSocketRecord;
|
|
283
|
+
if (current) {
|
|
284
|
+
transports.add(current);
|
|
285
|
+
this.#retire(current, new SdkClientError("connection_closed", "SDK client closed"), false, true);
|
|
286
|
+
}
|
|
287
|
+
for (const [id, pending] of this.#pending)
|
|
288
|
+
this.#settlePending(id, pending, new SdkClientError("connection_closed", "SDK client closed"), true);
|
|
289
|
+
try {
|
|
290
|
+
await Promise.all([...transports].map(incarnation => this.#closeTransport(incarnation)));
|
|
291
|
+
} finally {
|
|
292
|
+
this.#sentRecords.clear();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async control(
|
|
297
|
+
operation: string,
|
|
298
|
+
input: Record<string, unknown> = {},
|
|
299
|
+
options: SdkRequestOptions = {},
|
|
300
|
+
): Promise<unknown> {
|
|
301
|
+
return await this.#request(
|
|
302
|
+
{
|
|
303
|
+
type: "control_request",
|
|
304
|
+
operation,
|
|
305
|
+
input,
|
|
306
|
+
...(options.confirm === undefined ? {} : { confirm: options.confirm }),
|
|
307
|
+
},
|
|
308
|
+
options,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async query(
|
|
313
|
+
query: string,
|
|
314
|
+
input: Record<string, unknown> = {},
|
|
315
|
+
cursor?: string,
|
|
316
|
+
options: SdkRequestOptions = {},
|
|
317
|
+
): Promise<unknown> {
|
|
318
|
+
return await this.#request(
|
|
319
|
+
{ type: "query_request", query, input, ...(cursor === undefined ? {} : { cursor }) },
|
|
320
|
+
options,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async global(
|
|
325
|
+
operation: string,
|
|
326
|
+
input: Record<string, unknown> = {},
|
|
327
|
+
options: SdkRequestOptions = {},
|
|
328
|
+
): Promise<unknown> {
|
|
329
|
+
return await this.#request({ type: "broker_request", operation, input }, options);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
getSentRecord(id: string): SdkSentRecord | undefined {
|
|
333
|
+
return this.#sentRecords.get(id);
|
|
334
|
+
}
|
|
335
|
+
#rememberSentRecord(record: SdkSentRecord): void {
|
|
336
|
+
this.#sentRecords.set(record.id, record);
|
|
337
|
+
while (this.#sentRecords.size > 256) {
|
|
338
|
+
const oldest = this.#sentRecords.keys().next().value;
|
|
339
|
+
if (oldest === undefined) return;
|
|
340
|
+
this.#sentRecords.delete(oldest);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async lookupLifecycle(record: SdkSentRecord, timeoutMs?: number): Promise<unknown> {
|
|
344
|
+
if (!record.operation || !record.idempotencyKey || !record.fingerprint)
|
|
345
|
+
throw new SdkClientError(
|
|
346
|
+
"invalid_input",
|
|
347
|
+
"A lifecycle sent record requires operation, idempotencyKey, and fingerprint.",
|
|
348
|
+
);
|
|
349
|
+
return await this.#request(
|
|
350
|
+
{
|
|
351
|
+
type: "broker_request",
|
|
352
|
+
operation: "broker.lookup_lifecycle",
|
|
353
|
+
input: { operation: record.operation, fingerprint: record.fingerprint },
|
|
354
|
+
},
|
|
355
|
+
{ timeoutMs, idempotencyKey: record.idempotencyKey },
|
|
356
|
+
() => this.#sentRecords.delete(record.id),
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async #request(frame: Frame, options: SdkRequestOptions, onResponse?: () => void): Promise<unknown> {
|
|
361
|
+
if (this.#closed) throw new SdkClientError("connection_closed", "SDK client closed");
|
|
362
|
+
this.#throwIfDeadlineElapsed();
|
|
363
|
+
const incarnation = await this.#connect();
|
|
364
|
+
const timeoutMs = this.#remainingTimeout(options.timeoutMs ?? this.#timeoutMs);
|
|
365
|
+
if (timeoutMs <= 0) throw this.#deadlineError();
|
|
366
|
+
const id = randomUUID();
|
|
367
|
+
const requestFrame = {
|
|
368
|
+
...frame,
|
|
369
|
+
id,
|
|
370
|
+
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
371
|
+
};
|
|
372
|
+
const serializedRequest = JSON.stringify(requestFrame);
|
|
373
|
+
const serializedFrame = JSON.parse(serializedRequest) as Frame;
|
|
374
|
+
const deferred = Promise.withResolvers<unknown>();
|
|
375
|
+
const pending: Pending = {
|
|
376
|
+
incarnation,
|
|
377
|
+
resolve: deferred.resolve,
|
|
378
|
+
reject: deferred.reject,
|
|
379
|
+
sent: false,
|
|
380
|
+
onResponse,
|
|
381
|
+
timer: setTimeout(
|
|
382
|
+
() =>
|
|
383
|
+
this.#settlePending(
|
|
384
|
+
id,
|
|
385
|
+
pending,
|
|
386
|
+
new SdkClientError("timeout", `SDK request timed out after ${timeoutMs}ms`, {
|
|
387
|
+
requestId: id,
|
|
388
|
+
requestSent: pending.sent,
|
|
389
|
+
} satisfies SdkRequestTimeoutDetails),
|
|
390
|
+
true,
|
|
391
|
+
),
|
|
392
|
+
timeoutMs,
|
|
393
|
+
),
|
|
394
|
+
};
|
|
395
|
+
this.#pending.set(id, pending);
|
|
396
|
+
if (!this.#isActive(incarnation) || incarnation.socket.readyState !== WebSocket.OPEN) {
|
|
397
|
+
this.#settlePending(id, pending, new SdkClientError("unavailable", "SDK WebSocket is not connected"));
|
|
398
|
+
return await deferred.promise;
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
incarnation.socket.send(serializedRequest);
|
|
402
|
+
pending.sent = true;
|
|
403
|
+
this.#rememberSentRecord({
|
|
404
|
+
id,
|
|
405
|
+
operation: typeof serializedFrame.operation === "string" ? serializedFrame.operation : undefined,
|
|
406
|
+
idempotencyKey: options.idempotencyKey,
|
|
407
|
+
fingerprint:
|
|
408
|
+
typeof serializedFrame.operation === "string"
|
|
409
|
+
? lifecycleFingerprint(serializedFrame.operation, serializedFrame.input ?? {})
|
|
410
|
+
: inputFingerprint(serializedFrame.input ?? {}),
|
|
411
|
+
});
|
|
412
|
+
} catch (error) {
|
|
413
|
+
this.#settlePending(
|
|
414
|
+
id,
|
|
415
|
+
pending,
|
|
416
|
+
error instanceof SdkClientError
|
|
417
|
+
? error
|
|
418
|
+
: new SdkClientError("unavailable", "SDK WebSocket send failed", error),
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
return await deferred.promise;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
#deadlineError(reconnect?: SdkReconnectExhaustedDetails): SdkClientError {
|
|
425
|
+
return new SdkClientError("timeout", "SDK client deadline elapsed.", undefined, reconnect);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#remainingTimeout(limit = this.#timeoutMs): number {
|
|
429
|
+
if (this.#deadline === undefined) return limit;
|
|
430
|
+
return Math.min(limit, Math.max(0, this.#deadline - Date.now()));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
#throwIfDeadlineElapsed(): void {
|
|
434
|
+
if (this.#deadline !== undefined && Date.now() >= this.#deadline) throw this.#deadlineError();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async #connect(): Promise<Incarnation> {
|
|
438
|
+
this.#throwIfDeadlineElapsed();
|
|
439
|
+
const current = this.#currentSocketRecord;
|
|
440
|
+
if (current && this.#isActive(current) && current.socket.readyState === WebSocket.OPEN) return current;
|
|
441
|
+
if (current)
|
|
442
|
+
this.#retire(current, new SdkClientError("connection_closed", "SDK WebSocket connection closed"), true, true);
|
|
443
|
+
|
|
444
|
+
let cycle = this.#opening;
|
|
445
|
+
if (!cycle) {
|
|
446
|
+
cycle = { generation: ++this.#cycleGeneration, phase: "opening", candidate: null };
|
|
447
|
+
this.#opening = cycle;
|
|
448
|
+
cycle.promise = this.#openWithRetry(cycle);
|
|
449
|
+
}
|
|
450
|
+
return await cycle.promise!;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async #openWithRetry(cycle: Cycle): Promise<Incarnation> {
|
|
454
|
+
const startedAt = Date.now();
|
|
455
|
+
let attemptsConsumed = 0;
|
|
456
|
+
let lastError: unknown;
|
|
457
|
+
const diagnostics = (reason: SdkReconnectTerminationReason): SdkReconnectExhaustedDetails => ({
|
|
458
|
+
attemptsConsumed,
|
|
459
|
+
attemptBudget: this.#reconnectAttempts,
|
|
460
|
+
elapsedMs: Date.now() - startedAt,
|
|
461
|
+
reason,
|
|
462
|
+
});
|
|
463
|
+
const cancelled = (): SdkClientError =>
|
|
464
|
+
new SdkClientError("connection_closed", "SDK client closed", lastError, diagnostics("cancelled"));
|
|
465
|
+
/**
|
|
466
|
+
* A deadline and retry budget measure different failure axes. One-shot clients
|
|
467
|
+
* need the deadline to fast-fail their operation, while long-lived ACP sessions
|
|
468
|
+
* express their recovery window as retry slots. Honor both, but retain the
|
|
469
|
+
* terminating axis so a deadline-truncated session budget is not misdiagnosed
|
|
470
|
+
* as a host that consumed every retry.
|
|
471
|
+
*/
|
|
472
|
+
for (let attempt = 0; attempt <= this.#reconnectAttempts; attempt++) {
|
|
473
|
+
if (this.#deadline !== undefined && Date.now() >= this.#deadline) {
|
|
474
|
+
const error = this.#deadlineError(diagnostics("deadline"));
|
|
475
|
+
this.#completeCycle(cycle, error);
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
if (!this.#isOpening(cycle)) throw cancelled();
|
|
479
|
+
if (attempt > 0) attemptsConsumed++;
|
|
480
|
+
try {
|
|
481
|
+
const incarnation = await this.#open(cycle);
|
|
482
|
+
if (!this.#isActive(incarnation) && (!this.#isOpening(cycle) || cycle.candidate !== incarnation))
|
|
483
|
+
throw new SdkClientError("connection_closed", "SDK WebSocket is not connected");
|
|
484
|
+
await this.#waitForHello(incarnation);
|
|
485
|
+
if (this.#isActive(incarnation)) return incarnation;
|
|
486
|
+
throw new SdkClientError("connection_closed", "SDK WebSocket is not connected");
|
|
487
|
+
} catch (error) {
|
|
488
|
+
lastError = error;
|
|
489
|
+
if (!this.#isOpening(cycle)) throw cancelled();
|
|
490
|
+
const candidate = cycle.candidate;
|
|
491
|
+
if (candidate && candidate.phase !== "active")
|
|
492
|
+
this.#retire(
|
|
493
|
+
candidate,
|
|
494
|
+
error instanceof SdkClientError
|
|
495
|
+
? error
|
|
496
|
+
: new SdkClientError("unavailable", "SDK WebSocket connection failed", error),
|
|
497
|
+
true,
|
|
498
|
+
);
|
|
499
|
+
if (attempt < this.#reconnectAttempts) {
|
|
500
|
+
const backoffMs = this.#remainingTimeout(
|
|
501
|
+
Math.min(this.#reconnectBackoffMs * 2 ** attempt, this.#reconnectMaxBackoffMs),
|
|
502
|
+
);
|
|
503
|
+
if (backoffMs <= 0) break;
|
|
504
|
+
cycle.phase = "backoff";
|
|
505
|
+
// A close during the sleep rejects this promise. Left uncaught it escapes as
|
|
506
|
+
// the bare teardown error, so the one cancellation that is hardest to observe
|
|
507
|
+
// would be the only one carrying no attribution.
|
|
508
|
+
const deferred = Promise.withResolvers<void>();
|
|
509
|
+
cycle.rejectBackoff = deferred.reject;
|
|
510
|
+
cycle.backoffTimer = setTimeout(() => deferred.resolve(), backoffMs);
|
|
511
|
+
try {
|
|
512
|
+
await deferred.promise;
|
|
513
|
+
} catch (backoffRejection) {
|
|
514
|
+
lastError = backoffRejection;
|
|
515
|
+
throw cancelled();
|
|
516
|
+
} finally {
|
|
517
|
+
cycle.rejectBackoff = undefined;
|
|
518
|
+
cycle.backoffTimer = undefined;
|
|
519
|
+
}
|
|
520
|
+
if (!this.#isOpening(cycle)) throw cancelled();
|
|
521
|
+
cycle.phase = "opening";
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (!this.#isOpening(cycle)) throw cancelled();
|
|
526
|
+
if (this.#deadline !== undefined && Date.now() >= this.#deadline) {
|
|
527
|
+
const error = this.#deadlineError(diagnostics("deadline"));
|
|
528
|
+
this.#completeCycle(cycle, error);
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
cycle.phase = "complete";
|
|
532
|
+
if (this.#opening === cycle) this.#opening = null;
|
|
533
|
+
const error = new SdkClientError(
|
|
534
|
+
"reconnect_exhausted",
|
|
535
|
+
"SDK WebSocket reconnect attempts exhausted",
|
|
536
|
+
lastError,
|
|
537
|
+
diagnostics("attempts_exhausted"),
|
|
538
|
+
);
|
|
539
|
+
this.#notifyReconnectFailedHandlers(error);
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
#completeCycle(cycle: Cycle, error: SdkClientError): void {
|
|
544
|
+
if (cycle.backoffTimer) clearTimeout(cycle.backoffTimer);
|
|
545
|
+
cycle.rejectBackoff?.(error);
|
|
546
|
+
cycle.rejectBackoff = undefined;
|
|
547
|
+
cycle.backoffTimer = undefined;
|
|
548
|
+
const candidate = cycle.candidate;
|
|
549
|
+
if (candidate) this.#retire(candidate, error, true);
|
|
550
|
+
cycle.candidate = null;
|
|
551
|
+
cycle.phase = "complete";
|
|
552
|
+
if (this.#opening === cycle) this.#opening = null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
#open(cycle: Cycle): Promise<Incarnation> {
|
|
556
|
+
const timeoutMs = this.#remainingTimeout();
|
|
557
|
+
if (timeoutMs <= 0) return Promise.reject(this.#deadlineError());
|
|
558
|
+
const deferred = Promise.withResolvers<Incarnation>();
|
|
559
|
+
try {
|
|
560
|
+
const url = new URL(this.#url);
|
|
561
|
+
url.searchParams.set("token", this.#token);
|
|
562
|
+
const socket = new WebSocket(url);
|
|
563
|
+
const incarnation: Incarnation = {
|
|
564
|
+
generation: ++this.#incarnationGeneration,
|
|
565
|
+
cycle,
|
|
566
|
+
socket,
|
|
567
|
+
phase: "opening",
|
|
568
|
+
tornDown: false,
|
|
569
|
+
listeners: [],
|
|
570
|
+
resolveOpen: () => deferred.resolve(incarnation),
|
|
571
|
+
rejectOpen: deferred.reject,
|
|
572
|
+
};
|
|
573
|
+
cycle.candidate = incarnation;
|
|
574
|
+
const add = (type: "open" | "error" | "close" | "message", listener: EventListener, once = false) => {
|
|
575
|
+
incarnation.listeners.push([type, listener]);
|
|
576
|
+
socket.addEventListener(type, listener, once ? { once: true } : undefined);
|
|
577
|
+
};
|
|
578
|
+
add(
|
|
579
|
+
"open",
|
|
580
|
+
(() => {
|
|
581
|
+
if (!this.#isCandidate(cycle, incarnation) || incarnation.phase !== "opening") return;
|
|
582
|
+
if (incarnation.openTimer) clearTimeout(incarnation.openTimer);
|
|
583
|
+
incarnation.phase = "hello";
|
|
584
|
+
incarnation.resolveOpen?.();
|
|
585
|
+
incarnation.resolveOpen = undefined;
|
|
586
|
+
incarnation.rejectOpen = undefined;
|
|
587
|
+
this.#beginHello(incarnation);
|
|
588
|
+
const earlyHello = incarnation.earlyHello;
|
|
589
|
+
if (earlyHello) {
|
|
590
|
+
incarnation.earlyHello = undefined;
|
|
591
|
+
this.#acceptHello(incarnation, earlyHello);
|
|
592
|
+
if (this.#isActive(incarnation)) this.#notifyFrameHandlers(earlyHello);
|
|
593
|
+
}
|
|
594
|
+
}) as EventListener,
|
|
595
|
+
true,
|
|
596
|
+
);
|
|
597
|
+
add("error", ((event: Event) => this.#onSocketFailure(incarnation, event)) as EventListener);
|
|
598
|
+
add("close", (() => this.#onSocketFailure(incarnation)) as EventListener);
|
|
599
|
+
add("message", ((event: MessageEvent) => this.#onMessage(event.data, incarnation)) as EventListener);
|
|
600
|
+
incarnation.openTimer = setTimeout(() => this.#onOpenTimeout(incarnation, timeoutMs), timeoutMs);
|
|
601
|
+
incarnation.openTimer.unref?.();
|
|
602
|
+
} catch (error) {
|
|
603
|
+
deferred.reject(error);
|
|
604
|
+
}
|
|
605
|
+
return deferred.promise;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
#beginHello(incarnation: Incarnation): void {
|
|
609
|
+
const timeoutMs = this.#remainingTimeout();
|
|
610
|
+
if (timeoutMs <= 0) {
|
|
611
|
+
this.#retire(incarnation, this.#deadlineError(), true);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
incarnation.helloTimer = setTimeout(() => {
|
|
615
|
+
if (!this.#isCandidate(incarnation.cycle, incarnation) || incarnation.phase !== "hello") return;
|
|
616
|
+
const error =
|
|
617
|
+
this.#deadline !== undefined && Date.now() >= this.#deadline
|
|
618
|
+
? this.#deadlineError()
|
|
619
|
+
: new SdkClientError("protocol_error", "SDK server did not send a hello frame.");
|
|
620
|
+
incarnation.rejectHello?.(error);
|
|
621
|
+
this.#retire(incarnation, error, true);
|
|
622
|
+
}, timeoutMs);
|
|
623
|
+
incarnation.helloTimer.unref?.();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
#waitForHello(incarnation: Incarnation): Promise<void> {
|
|
627
|
+
if (incarnation.failure) return Promise.reject(incarnation.failure);
|
|
628
|
+
if (this.#isActive(incarnation)) return Promise.resolve();
|
|
629
|
+
if (!this.#isCandidate(incarnation.cycle, incarnation) || incarnation.phase !== "hello")
|
|
630
|
+
return Promise.reject(new SdkClientError("connection_closed", "SDK WebSocket is not connected"));
|
|
631
|
+
const deferred = Promise.withResolvers<void>();
|
|
632
|
+
incarnation.resolveHello = deferred.resolve;
|
|
633
|
+
incarnation.rejectHello = deferred.reject;
|
|
634
|
+
return deferred.promise;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
#onOpenTimeout(incarnation: Incarnation, timeoutMs: number): void {
|
|
638
|
+
if (!this.#isCandidate(incarnation.cycle, incarnation) || incarnation.phase !== "opening") return;
|
|
639
|
+
const error =
|
|
640
|
+
this.#deadline !== undefined && Date.now() >= this.#deadline
|
|
641
|
+
? this.#deadlineError()
|
|
642
|
+
: new SdkClientError("timeout", `SDK WebSocket connection timed out after ${timeoutMs}ms`);
|
|
643
|
+
incarnation.rejectOpen?.(error);
|
|
644
|
+
this.#retire(incarnation, error, true);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
#onSocketFailure(incarnation: Incarnation, event?: Event): void {
|
|
648
|
+
if (!this.#isCandidate(incarnation.cycle, incarnation) && !this.#isActive(incarnation)) return;
|
|
649
|
+
const detail = event as (Event & { error?: unknown; message?: unknown }) | undefined;
|
|
650
|
+
const error =
|
|
651
|
+
detail?.error instanceof Error
|
|
652
|
+
? detail.error
|
|
653
|
+
: new SdkClientError(
|
|
654
|
+
"connection_closed",
|
|
655
|
+
typeof detail?.message === "string" ? detail.message : "SDK WebSocket connection closed",
|
|
656
|
+
);
|
|
657
|
+
if (incarnation.phase === "opening") incarnation.rejectOpen?.(error);
|
|
658
|
+
if (incarnation.phase === "hello") incarnation.rejectHello?.(error);
|
|
659
|
+
this.#retire(
|
|
660
|
+
incarnation,
|
|
661
|
+
error instanceof SdkClientError
|
|
662
|
+
? error
|
|
663
|
+
: new SdkClientError("unavailable", "SDK WebSocket connection failed", error),
|
|
664
|
+
true,
|
|
665
|
+
true,
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
#onMessage(value: unknown, incarnation: Incarnation): void {
|
|
670
|
+
if (!this.#isCandidate(incarnation.cycle, incarnation) && !this.#isActive(incarnation)) return;
|
|
671
|
+
let frame: Frame;
|
|
672
|
+
try {
|
|
673
|
+
frame = parseFrame(value);
|
|
674
|
+
if (frame.type === "control_command_result" && typeof frame.message === "string")
|
|
675
|
+
frame = parseFrame(frame.message);
|
|
676
|
+
} catch (error) {
|
|
677
|
+
this.#rejectPendingFor(
|
|
678
|
+
incarnation,
|
|
679
|
+
error instanceof SdkClientError
|
|
680
|
+
? error
|
|
681
|
+
: new SdkClientError("protocol_error", "SDK server sent malformed frame.", error),
|
|
682
|
+
);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (frame.type === "hello" || frame.type === "server_hello" || frame.type === "broker_hello") {
|
|
686
|
+
if (incarnation.phase === "opening" && this.#isCandidate(incarnation.cycle, incarnation)) {
|
|
687
|
+
// Buffer until the open handler advances phase; do not drop.
|
|
688
|
+
incarnation.earlyHello = frame;
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (incarnation.phase === "hello" && this.#isCandidate(incarnation.cycle, incarnation)) {
|
|
692
|
+
this.#acceptHello(incarnation, frame);
|
|
693
|
+
if (this.#isActive(incarnation)) this.#notifyFrameHandlers(frame);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (!this.#isActive(incarnation)) return;
|
|
697
|
+
if (
|
|
698
|
+
typeof frame.connectionId !== "string" ||
|
|
699
|
+
frame.connectionId.length === 0 ||
|
|
700
|
+
frame.connectionId === this.connectionId
|
|
701
|
+
)
|
|
702
|
+
return;
|
|
703
|
+
this.connectionId = frame.connectionId;
|
|
704
|
+
this.#notifyReconnectHandlers();
|
|
705
|
+
}
|
|
706
|
+
if (!this.#isActive(incarnation)) return;
|
|
707
|
+
const id =
|
|
708
|
+
typeof frame.id === "string" ? frame.id : typeof frame.requestId === "string" ? frame.requestId : undefined;
|
|
709
|
+
if (id) {
|
|
710
|
+
const pending = this.#pending.get(id);
|
|
711
|
+
if (pending?.incarnation === incarnation) {
|
|
712
|
+
this.#settlePending(
|
|
713
|
+
id,
|
|
714
|
+
pending,
|
|
715
|
+
frame.ok === false || frame.status === "error" ? errorFrom(frame) : frame,
|
|
716
|
+
false,
|
|
717
|
+
true,
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
this.#notifyFrameHandlers(frame);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
#notifyFrameHandlers(frame: Frame): void {
|
|
725
|
+
for (const handler of [...this.#frameHandlers]) {
|
|
726
|
+
try {
|
|
727
|
+
handler(frame);
|
|
728
|
+
} catch {
|
|
729
|
+
// Observers cannot change transport settlement or prevent later observers.
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
#notifyReconnectHandlers(): void {
|
|
735
|
+
for (const handler of [...this.#reconnectHandlers]) {
|
|
736
|
+
try {
|
|
737
|
+
handler();
|
|
738
|
+
} catch {
|
|
739
|
+
// Reconnect observers cannot change transport state or prevent later observers.
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
#notifyReconnectFailedHandlers(error: SdkClientError): void {
|
|
745
|
+
for (const handler of [...this.#reconnectFailedHandlers]) {
|
|
746
|
+
try {
|
|
747
|
+
handler(error);
|
|
748
|
+
} catch {
|
|
749
|
+
// Failure observers cannot replace the typed transport error or prevent later observers.
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
#acceptHello(incarnation: Incarnation, frame: Frame): void {
|
|
755
|
+
if (!this.#isCandidate(incarnation.cycle, incarnation) || incarnation.phase !== "hello") return;
|
|
756
|
+
if (incarnation.helloTimer) clearTimeout(incarnation.helloTimer);
|
|
757
|
+
const reconnecting =
|
|
758
|
+
typeof frame.connectionId === "string" &&
|
|
759
|
+
frame.connectionId.length > 0 &&
|
|
760
|
+
this.connectionId !== undefined &&
|
|
761
|
+
this.connectionId !== frame.connectionId;
|
|
762
|
+
if (typeof frame.connectionId === "string" && frame.connectionId.length > 0)
|
|
763
|
+
this.connectionId = frame.connectionId;
|
|
764
|
+
incarnation.phase = "active";
|
|
765
|
+
this.#currentSocketRecord = incarnation;
|
|
766
|
+
incarnation.cycle.phase = "complete";
|
|
767
|
+
if (this.#opening === incarnation.cycle) this.#opening = null;
|
|
768
|
+
const resolveHello = incarnation.resolveHello;
|
|
769
|
+
incarnation.resolveHello = undefined;
|
|
770
|
+
incarnation.rejectHello = undefined;
|
|
771
|
+
resolveHello?.();
|
|
772
|
+
if (reconnecting) this.#notifyReconnectHandlers();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
#settlePending(
|
|
776
|
+
id: string,
|
|
777
|
+
pending: Pending,
|
|
778
|
+
result: unknown,
|
|
779
|
+
transportFailure = false,
|
|
780
|
+
responseReceived = false,
|
|
781
|
+
): void {
|
|
782
|
+
if (this.#pending.get(id) !== pending) return;
|
|
783
|
+
this.#pending.delete(id);
|
|
784
|
+
clearTimeout(pending.timer);
|
|
785
|
+
if (responseReceived) pending.onResponse?.();
|
|
786
|
+
if (result instanceof Error) {
|
|
787
|
+
if (
|
|
788
|
+
transportFailure &&
|
|
789
|
+
pending.sent &&
|
|
790
|
+
result instanceof SdkClientError &&
|
|
791
|
+
(result.code === "timeout" || result.code === "connection_closed")
|
|
792
|
+
)
|
|
793
|
+
pending.reject(
|
|
794
|
+
new SdkClientError(
|
|
795
|
+
"uncertain_after_send",
|
|
796
|
+
"SDK request outcome is uncertain after the frame was sent.",
|
|
797
|
+
this.#sentRecords.get(id),
|
|
798
|
+
),
|
|
799
|
+
);
|
|
800
|
+
else {
|
|
801
|
+
this.#sentRecords.delete(id);
|
|
802
|
+
pending.reject(result);
|
|
803
|
+
}
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
this.#sentRecords.delete(id);
|
|
807
|
+
pending.resolve(result);
|
|
808
|
+
}
|
|
809
|
+
#rejectPendingFor(incarnation: Incarnation, error: SdkClientError, transportFailure = false): void {
|
|
810
|
+
for (const [id, pending] of this.#pending)
|
|
811
|
+
if (pending.incarnation === incarnation) this.#settlePending(id, pending, error, transportFailure);
|
|
812
|
+
}
|
|
813
|
+
#retire(incarnation: Incarnation, error: SdkClientError, closeSocket: boolean, transportFailure = false): void {
|
|
814
|
+
if (incarnation.tornDown) return;
|
|
815
|
+
const phase = incarnation.phase;
|
|
816
|
+
incarnation.phase = "retired";
|
|
817
|
+
incarnation.failure = error;
|
|
818
|
+
if (phase === "opening") incarnation.rejectOpen?.(error);
|
|
819
|
+
if (phase === "hello") incarnation.rejectHello?.(error);
|
|
820
|
+
incarnation.resolveOpen = undefined;
|
|
821
|
+
incarnation.rejectOpen = undefined;
|
|
822
|
+
incarnation.resolveHello = undefined;
|
|
823
|
+
incarnation.rejectHello = undefined;
|
|
824
|
+
this.#rejectPendingFor(incarnation, error, transportFailure);
|
|
825
|
+
if (this.#currentSocketRecord === incarnation) this.#currentSocketRecord = null;
|
|
826
|
+
if (incarnation.cycle.candidate === incarnation) incarnation.cycle.candidate = null;
|
|
827
|
+
this.#teardown(incarnation, closeSocket);
|
|
828
|
+
}
|
|
829
|
+
#teardown(incarnation: Incarnation, closeSocket: boolean): void {
|
|
830
|
+
if (incarnation.tornDown) return;
|
|
831
|
+
incarnation.tornDown = true;
|
|
832
|
+
if (incarnation.openTimer) clearTimeout(incarnation.openTimer);
|
|
833
|
+
if (incarnation.helloTimer) clearTimeout(incarnation.helloTimer);
|
|
834
|
+
for (const [type, listener] of incarnation.listeners) incarnation.socket.removeEventListener(type, listener);
|
|
835
|
+
incarnation.listeners = [];
|
|
836
|
+
if (closeSocket)
|
|
837
|
+
try {
|
|
838
|
+
incarnation.socket.close();
|
|
839
|
+
} catch {}
|
|
840
|
+
}
|
|
841
|
+
async #closeTransport(incarnation: Incarnation): Promise<void> {
|
|
842
|
+
const socket = incarnation.socket;
|
|
843
|
+
if (socket.readyState === WebSocket.CLOSED) return;
|
|
844
|
+
// Close teardown must always issue socket.close() and be bounded by a
|
|
845
|
+
// dedicated close grace, never by the (possibly elapsed) request deadline —
|
|
846
|
+
// gating on an expired deadline would throw before close and leak the socket.
|
|
847
|
+
const timeoutMs = this.#closeGraceMs;
|
|
848
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
849
|
+
const onClose = (): void => resolve();
|
|
850
|
+
socket.addEventListener("close", onClose, { once: true });
|
|
851
|
+
const timer = setTimeout(
|
|
852
|
+
() => reject(new SdkClientError("timeout", `SDK WebSocket close timed out after ${timeoutMs}ms`)),
|
|
853
|
+
timeoutMs,
|
|
854
|
+
);
|
|
855
|
+
timer.unref?.();
|
|
856
|
+
try {
|
|
857
|
+
socket.close();
|
|
858
|
+
if (Number(socket.readyState) === WebSocket.CLOSED) resolve();
|
|
859
|
+
await promise;
|
|
860
|
+
} catch (error) {
|
|
861
|
+
if (error instanceof SdkClientError) throw error;
|
|
862
|
+
if (Number(socket.readyState) !== WebSocket.CLOSED)
|
|
863
|
+
throw new SdkClientError("connection_closed", "SDK WebSocket close failed", error);
|
|
864
|
+
} finally {
|
|
865
|
+
clearTimeout(timer);
|
|
866
|
+
socket.removeEventListener("close", onClose);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
#isCandidate(cycle: Cycle, incarnation: Incarnation): boolean {
|
|
870
|
+
return (
|
|
871
|
+
!this.#closed &&
|
|
872
|
+
this.#opening === cycle &&
|
|
873
|
+
cycle.candidate === incarnation &&
|
|
874
|
+
cycle.generation > 0 &&
|
|
875
|
+
incarnation.generation > 0 &&
|
|
876
|
+
incarnation.cycle === cycle &&
|
|
877
|
+
(cycle.phase === "opening" || cycle.phase === "backoff")
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
#isOpening(cycle: Cycle): boolean {
|
|
881
|
+
return !this.#closed && this.#opening === cycle && (cycle.phase === "opening" || cycle.phase === "backoff");
|
|
882
|
+
}
|
|
883
|
+
#isActive(incarnation: Incarnation | null): boolean {
|
|
884
|
+
return (
|
|
885
|
+
!!incarnation &&
|
|
886
|
+
incarnation.generation > 0 &&
|
|
887
|
+
!this.#closed &&
|
|
888
|
+
this.#currentSocketRecord === incarnation &&
|
|
889
|
+
incarnation.phase === "active"
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
}
|