@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
|
@@ -1,32 +1,30 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { logger } from "@gajae-code/utils";
|
|
5
|
-
import { type IndexedSession, SessionIndex } from "../broker/session-index";
|
|
6
|
-
import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
|
|
7
|
-
import { SdkClient, SdkClientError } from "../client/client";
|
|
8
|
-
import { readSdkBrokerDiscovery, readSdkSessionEndpoint, type SdkSessionEndpoint } from "../client/discovery";
|
|
2
|
+
import { SdkClientError } from "../client/client";
|
|
9
3
|
import { SESSION_PREPARED_EVENT } from "../host/host";
|
|
10
|
-
|
|
4
|
+
|
|
5
|
+
import type { SessionRouterDeps } from "../router";
|
|
6
|
+
|
|
7
|
+
import { type SessionAttachment, SessionRouter, SessionRouterError, type SessionRouterFrame } from "../router";
|
|
11
8
|
|
|
12
9
|
import { createDiscordAdapter, createSlackAdapter } from "./chat-adapters";
|
|
13
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type ChatOperationRequest,
|
|
12
|
+
type ChatTransport,
|
|
13
|
+
projectChatCommandOutcome,
|
|
14
|
+
sendAuthorizedChatOperation,
|
|
15
|
+
} from "./chat-command-policy";
|
|
14
16
|
import type { ChatDaemonCommandBindInput, ChatDaemonCommandOutcome } from "./chat-daemon-command-channel";
|
|
15
17
|
import type { ChatDaemonKind } from "./chat-daemon-control";
|
|
16
18
|
import { isControlPlaneFrameType } from "./control-plane-frames";
|
|
17
|
-
import {
|
|
19
|
+
import { DiscordNotificationDaemon } from "./discord-daemon";
|
|
20
|
+
|
|
18
21
|
import { DiscordLiveProvider } from "./discord-live-provider";
|
|
19
22
|
import type { DiscordProvider } from "./discord-provider";
|
|
20
23
|
import { type NotificationEvent, NotificationPresentationEngine } from "./engine";
|
|
21
|
-
import {
|
|
22
|
-
type SlackBindingAuthority,
|
|
23
|
-
type SlackEndpoint,
|
|
24
|
-
SlackEndpointBindingError,
|
|
25
|
-
SlackNotificationDaemon,
|
|
26
|
-
} from "./slack-daemon";
|
|
24
|
+
import { SlackNotificationDaemon } from "./slack-daemon";
|
|
27
25
|
import { SlackLiveProvider } from "./slack-live-provider";
|
|
28
26
|
import { SlackProvider, type SlackProviderClient } from "./slack-provider";
|
|
29
|
-
import {
|
|
27
|
+
import { SlackThreadBindingError } from "./slack-thread-binding";
|
|
30
28
|
|
|
31
29
|
export interface ChatDaemonRuntimeConfig {
|
|
32
30
|
identity: string;
|
|
@@ -37,21 +35,6 @@ export interface ChatDaemonRuntimeConfig {
|
|
|
37
35
|
presentation?: { redact: boolean; verbosity: "lean" | "verbose" };
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
export interface ChatDaemonSdkClient {
|
|
41
|
-
onFrame(handler: (frame: Record<string, unknown>) => void): () => void;
|
|
42
|
-
/**
|
|
43
|
-
* Announces that a replacement socket is live. Optional because command-scoped
|
|
44
|
-
* clients (the broker client, one-shot doubles) never outlive one socket, while
|
|
45
|
-
* every long-lived attachment's client carries it.
|
|
46
|
-
*/
|
|
47
|
-
onReconnect?(handler: () => void): () => void;
|
|
48
|
-
/** Re-dials a retired socket; a no-op on a live one. */
|
|
49
|
-
connect?(): Promise<void>;
|
|
50
|
-
request(frame: Record<string, unknown>, options?: { timeoutMs?: number }): Promise<Record<string, unknown>>;
|
|
51
|
-
close(): Promise<void>;
|
|
52
|
-
send(frame: Record<string, unknown>): void;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
38
|
export type ChatDeliveryPhase = "pre_send" | "ambiguous";
|
|
56
39
|
|
|
57
40
|
/** An authorized SDK command could not be conclusively delivered. */
|
|
@@ -63,17 +46,20 @@ export class ChatDeliveryError extends Error {
|
|
|
63
46
|
}
|
|
64
47
|
|
|
65
48
|
function chatDeliveryPhase(error: unknown): ChatDeliveryPhase | undefined {
|
|
49
|
+
if (error instanceof SessionRouterError) return error.phase;
|
|
50
|
+
|
|
66
51
|
if (error instanceof ChatDeliveryError) return error.phase;
|
|
67
52
|
if (!(error instanceof SdkClientError)) return undefined;
|
|
68
|
-
// `
|
|
69
|
-
//
|
|
53
|
+
// `uncertain_after_send` is the one code that states outright that the frame
|
|
54
|
+
// reached the host: reporting it as a definite failure would tell the operator a
|
|
55
|
+
// prompt was rejected while the session may already be running it.
|
|
70
56
|
return [
|
|
71
57
|
"connection_closed",
|
|
72
58
|
"unavailable",
|
|
73
59
|
"timeout",
|
|
74
|
-
"uncertain_after_send",
|
|
75
60
|
"reconnect_exhausted",
|
|
76
61
|
"protocol_error",
|
|
62
|
+
"uncertain_after_send",
|
|
77
63
|
].includes(error.code)
|
|
78
64
|
? "ambiguous"
|
|
79
65
|
: undefined;
|
|
@@ -83,125 +69,13 @@ export interface ChatDaemonRuntimeDeps {
|
|
|
83
69
|
createDiscordProvider?: (
|
|
84
70
|
config: NonNullable<ChatDaemonRuntimeConfig["notifications"]["discord"]>,
|
|
85
71
|
) => DiscordProvider;
|
|
86
|
-
|
|
87
72
|
createSlackProvider?: (
|
|
88
73
|
config: NonNullable<ChatDaemonRuntimeConfig["notifications"]["slack"]>,
|
|
89
74
|
) => SlackProviderClient;
|
|
90
|
-
|
|
91
|
-
createIndex?: (agentDir: string) => SessionIndex;
|
|
92
|
-
createBrokerClient?: (endpoint: { url: string; token: string }) => Promise<ChatDaemonSdkClient>;
|
|
93
|
-
onReconciled?: () => void;
|
|
94
|
-
setInterval?: typeof setInterval;
|
|
95
|
-
clearInterval?: typeof clearInterval;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** One live frame held behind a replay barrier, kept with the sequence that orders it. */
|
|
99
|
-
type HeldFrame = Readonly<{ seq: number; frame: Record<string, unknown> }>;
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Which producer handed a frame to delivery.
|
|
103
|
-
*
|
|
104
|
-
* A reconnect has two at once — the replacement socket, whose subscription never
|
|
105
|
-
* stopped, and the replay answer — and only the socket's frames may be held: the
|
|
106
|
-
* barrier drains by re-entering delivery with the frames it holds, and holding those
|
|
107
|
-
* a second time would never publish anything.
|
|
108
|
-
*/
|
|
109
|
-
type FrameOrigin = "live" | "ordered";
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Ingress fence one attachment applies while its replay is outstanding.
|
|
113
|
-
*
|
|
114
|
-
* `held` is non-undefined exactly while a replay round owns this attachment's live
|
|
115
|
-
* ingress, and the array identity is that round's ticket: a later round or a disposal
|
|
116
|
-
* installs or revokes its own, so an earlier round can tell it no longer owns the
|
|
117
|
-
* buffer instead of stranding — or publishing — someone else's frames.
|
|
118
|
-
*/
|
|
119
|
-
type ReplayBarrier = {
|
|
120
|
-
held: HeldFrame[] | undefined;
|
|
121
|
-
detached: boolean;
|
|
122
|
-
/**
|
|
123
|
-
* Set when a round could not close the gap it fenced — the replay went
|
|
124
|
-
* unanswered, or the hold buffer overflowed. The attachment stops delivering
|
|
125
|
-
* from that point on, because lifting the fence instead would let the next live
|
|
126
|
-
* frame drag the cursor over sequences nobody ever delivered.
|
|
127
|
-
*/
|
|
128
|
-
failed: boolean;
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* How many live frames one attachment holds while its replay is outstanding.
|
|
133
|
-
*
|
|
134
|
-
* The window is a single round trip on an open socket, so the bound is only there
|
|
135
|
-
* to keep a flooding session from growing the buffer without limit. Overflow is
|
|
136
|
-
* barrier failure rather than an eviction: the oldest held frame is the one the
|
|
137
|
-
* cursor needs next, and dropping the newest still leaves a hole no drain can
|
|
138
|
-
* close, so the attachment is rebuilt from its cursor and its replay re-fetches
|
|
139
|
-
* the whole gap instead.
|
|
140
|
-
*/
|
|
141
|
-
const REPLAY_BARRIER_LIMIT = 1_024;
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* How many times one round re-issues a refused replay, and the backoff it doubles
|
|
145
|
-
* from.
|
|
146
|
-
*
|
|
147
|
-
* A socket that never dropped sends no hello, so nothing but this round can
|
|
148
|
-
* re-issue the replay it owes — `client.onReconnect` fires only for a new
|
|
149
|
-
* connection id. The retry therefore rides the same live socket, and the budget is
|
|
150
|
-
* bounded so a host that never serves the gap ends in a rebuild rather than a
|
|
151
|
-
* barrier that fences ingress forever.
|
|
152
|
-
*/
|
|
153
|
-
const REPLAY_RETRY_ATTEMPTS = 3;
|
|
154
|
-
const REPLAY_RETRY_BACKOFF_MS = 100;
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* How many rounds re-serve one sequence whose publication failed before it is conceded.
|
|
158
|
-
*
|
|
159
|
-
* A refused publication holds the cursor below its frame, and the rebuild that follows
|
|
160
|
-
* re-serves exactly that frame. A surface that is down for good would repeat that
|
|
161
|
-
* forever and deliver nothing else, so the rounds are bounded: past this count the frame
|
|
162
|
-
* is conceded the way a retention gap is, and the stream resumes above it.
|
|
163
|
-
*/
|
|
164
|
-
const DELIVERY_ATTEMPT_LIMIT = 3;
|
|
165
|
-
|
|
166
|
-
type AttachedSession = Readonly<{
|
|
167
|
-
id: string;
|
|
168
|
-
sessionId: string;
|
|
169
|
-
endpoint: SdkSessionEndpoint;
|
|
170
|
-
generation: number;
|
|
171
|
-
client: ChatDaemonSdkClient;
|
|
172
|
-
/**
|
|
173
|
-
* How far this attachment has consumed its session's event stream. The record is
|
|
174
|
-
* frozen; this cursor is the one thing that must keep moving with delivery,
|
|
175
|
-
* because a reconnect resumes from exactly here.
|
|
176
|
-
*/
|
|
177
|
-
cursor: { seq: number };
|
|
178
|
-
/**
|
|
179
|
-
* Ordering fence between the two producers a reconnect creates. Frozen with the
|
|
180
|
-
* record like the cursor; only its own fields move.
|
|
181
|
-
*/
|
|
182
|
-
barrier: ReplayBarrier;
|
|
183
|
-
dispose: () => void;
|
|
184
|
-
}>;
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Default transport for one attached session.
|
|
188
|
-
*
|
|
189
|
-
* The attachment is long-lived — its frame subscription survives until an
|
|
190
|
-
* explicit detach or endpoint roll — and it sits under the host's liveness
|
|
191
|
-
* reaper, which drops any session that has not ponged within HEARTBEAT_TTL_MS.
|
|
192
|
-
* The transport's one-shot defaults give up after 175ms, so every stall the host
|
|
193
|
-
* reaps would be unrecoverable; the shared session budget outlives that TTL.
|
|
194
|
-
*/
|
|
195
|
-
async function connectAttachedSession(endpoint: SdkSessionEndpoint): Promise<ChatDaemonSdkClient> {
|
|
196
|
-
return await SdkClient.connect(endpoint.url, endpoint.token, { ...ACP_SESSION_RECONNECT });
|
|
75
|
+
routerDeps?: SessionRouterDeps;
|
|
197
76
|
}
|
|
198
77
|
|
|
199
|
-
/**
|
|
200
|
-
* The lifecycle signals that decide whether a chat root exists at all. They are
|
|
201
|
-
* the only event names that close, resume, or withhold a session's publication,
|
|
202
|
-
* so their identity may never be assembled from two disagreeing representations
|
|
203
|
-
* of one frame.
|
|
204
|
-
*/
|
|
78
|
+
/** The lifecycle signals that decide whether a chat root exists at all. */
|
|
205
79
|
const LIFECYCLE_EVENT_NAMES: ReadonlySet<string> = new Set([
|
|
206
80
|
SESSION_PREPARED_EVENT,
|
|
207
81
|
"session_ready",
|
|
@@ -213,25 +87,12 @@ function isLifecycleEvent(name: string | undefined): boolean {
|
|
|
213
87
|
return name !== undefined && LIFECYCLE_EVENT_NAMES.has(name);
|
|
214
88
|
}
|
|
215
89
|
|
|
216
|
-
/**
|
|
217
|
-
* Names whose presence obliges a frame to mean exactly one thing. Lifecycle
|
|
218
|
-
* signals decide whether a chat root exists at all; control-plane
|
|
219
|
-
* discriminants may never be presented at all. Either identity appearing on
|
|
220
|
-
* only one of a frame's two representations is a smuggling attempt, not an
|
|
221
|
-
* event.
|
|
222
|
-
*/
|
|
223
90
|
function isReservedIdentity(name: string | undefined): boolean {
|
|
224
91
|
return isLifecycleEvent(name) || isControlPlaneFrameType(name);
|
|
225
92
|
}
|
|
226
93
|
|
|
227
94
|
/** One delivered frame reduced to a single event identity. */
|
|
228
|
-
type CorrelatedFrame =
|
|
229
|
-
/** The event body a notification is projected from. */
|
|
230
|
-
body: Record<string, unknown>;
|
|
231
|
-
name: string | undefined;
|
|
232
|
-
sessionId: string | undefined;
|
|
233
|
-
generation: number | undefined;
|
|
234
|
-
}>;
|
|
95
|
+
type CorrelatedFrame = SessionRouterFrame;
|
|
235
96
|
|
|
236
97
|
function eventPayload(frame: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
237
98
|
if (frame.type !== "event") return undefined;
|
|
@@ -253,38 +114,6 @@ function readGeneration(value: unknown): number | undefined {
|
|
|
253
114
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
254
115
|
}
|
|
255
116
|
|
|
256
|
-
function readSequence(value: unknown): number | undefined {
|
|
257
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 ? value : undefined;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* What one `event_replay_result` states about the range it could not answer.
|
|
262
|
-
*
|
|
263
|
-
* A gap is the host's own admission, not an inference this side draws:
|
|
264
|
-
* `sequence_gap` names sequences its ring has already evicted, and
|
|
265
|
-
* `generation_reset` says the stream this cursor belongs to no longer exists.
|
|
266
|
-
* Reading it whole — rather than treating every truthy `gap` alike — is what
|
|
267
|
-
* keeps a bounded, nameable loss apart from an answer nothing can be concluded
|
|
268
|
-
* from, and the two owe opposite responses.
|
|
269
|
-
*/
|
|
270
|
-
type ReplayGap =
|
|
271
|
-
| Readonly<{ kind: "generation_reset"; toGeneration: number }>
|
|
272
|
-
| Readonly<{ kind: "sequence_gap"; fromSeq: number; toSeq: number }>;
|
|
273
|
-
|
|
274
|
-
function readReplayGap(value: unknown): ReplayGap | undefined {
|
|
275
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
276
|
-
const gap = value as Record<string, unknown>;
|
|
277
|
-
if (gap.kind === "generation_reset") {
|
|
278
|
-
const toGeneration = readGeneration(gap.toGeneration);
|
|
279
|
-
return toGeneration === undefined ? undefined : { kind: "generation_reset", toGeneration };
|
|
280
|
-
}
|
|
281
|
-
if (gap.kind !== "sequence_gap") return undefined;
|
|
282
|
-
const fromSeq = readSequence(gap.fromSeq);
|
|
283
|
-
const toSeq = readSequence(gap.toSeq);
|
|
284
|
-
if (fromSeq === undefined || toSeq === undefined || toSeq < fromSeq) return undefined;
|
|
285
|
-
return { kind: "sequence_gap", fromSeq, toSeq };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
117
|
/**
|
|
289
118
|
* What one representation of a frame states about an identity it owns.
|
|
290
119
|
*
|
|
@@ -413,43 +242,61 @@ function correlateFrame(frame: Record<string, unknown>): CorrelatedFrame | undef
|
|
|
413
242
|
|
|
414
243
|
/**
|
|
415
244
|
* Worker-owned session discovery and event fanout. It connects only through the
|
|
416
|
-
* public SDK transport and retains
|
|
245
|
+
* public SDK transport and retains SDK credentials solely in live client objects.
|
|
417
246
|
*/
|
|
418
247
|
export class ChatDaemonRuntime {
|
|
419
|
-
readonly #
|
|
420
|
-
readonly #
|
|
421
|
-
#
|
|
422
|
-
readonly #
|
|
423
|
-
readonly #frameTails = new Map<string, Promise<void>>();
|
|
424
|
-
/**
|
|
425
|
-
* The frame an attachment failed to publish, with how many rounds have tried it.
|
|
426
|
-
*
|
|
427
|
-
* Keyed by session rather than by attachment because the retire-and-rebuild a refused
|
|
428
|
-
* publication triggers replaces the attachment: the count has to outlive it, or a
|
|
429
|
-
* surface that is down for good would rebuild from the same sequence forever. It does
|
|
430
|
-
* not outlive the stream, though: a sequence names a frame only within one endpoint
|
|
431
|
-
* generation, so a rolled endpoint reopens a sequence space whose first frame nothing
|
|
432
|
-
* has ever been offered. Charging it for what an earlier generation's frame cost would
|
|
433
|
-
* concede it after a single refusal.
|
|
434
|
-
*/
|
|
435
|
-
readonly #undelivered = new Map<string, { generation: number; seq: number; attempts: number }>();
|
|
436
|
-
readonly #reviving = new Set<string>();
|
|
437
|
-
#reconcileTail: Promise<void> = Promise.resolve();
|
|
438
|
-
|
|
248
|
+
readonly #router: SessionRouter;
|
|
249
|
+
readonly #attachments = new Map<string, SessionAttachment>();
|
|
250
|
+
readonly #cleanupWork = new Map<string, Promise<void>>();
|
|
251
|
+
readonly #attachmentBarriers = new Map<string, Promise<void>>();
|
|
439
252
|
#discord: DiscordNotificationDaemon | undefined;
|
|
440
253
|
#slack: SlackNotificationDaemon | undefined;
|
|
441
254
|
#presentation: NotificationPresentationEngine | undefined;
|
|
442
255
|
#transportHealthy: (() => boolean) | undefined;
|
|
443
|
-
#reconcileReady = false;
|
|
444
256
|
|
|
445
257
|
constructor(
|
|
446
258
|
private readonly input: { kind: ChatDaemonKind; agentDir: string; config: ChatDaemonRuntimeConfig },
|
|
447
259
|
private readonly deps: ChatDaemonRuntimeDeps = {},
|
|
448
260
|
) {
|
|
449
|
-
this.#
|
|
261
|
+
this.#router = new SessionRouter({
|
|
262
|
+
agentDir: input.agentDir,
|
|
263
|
+
correlateFrame,
|
|
264
|
+
deps: {
|
|
265
|
+
...deps.routerDeps,
|
|
266
|
+
onFrame: async (attachment, frame) => await this.#handleFrame(attachment, frame),
|
|
267
|
+
onAttachment: async attachment => this.#onAttachment(attachment),
|
|
268
|
+
onSessionRemoved: async (attachment, reason) => await this.#onSessionRemoved(attachment, reason),
|
|
269
|
+
},
|
|
270
|
+
});
|
|
450
271
|
}
|
|
451
272
|
|
|
452
273
|
async start(): Promise<void> {
|
|
274
|
+
if (this.#cleanupWork.size > 0) {
|
|
275
|
+
const settled = await Promise.race([
|
|
276
|
+
Promise.allSettled([...this.#cleanupWork.values()]).then(() => true),
|
|
277
|
+
Bun.sleep(5_000).then(() => false),
|
|
278
|
+
]);
|
|
279
|
+
if (!settled) throw new Error("Prior provider cleanup did not settle before chat daemon restart.");
|
|
280
|
+
}
|
|
281
|
+
this.#attachments.clear();
|
|
282
|
+
const retainedProvider =
|
|
283
|
+
this.input.kind === "discord"
|
|
284
|
+
? this.#discord?.restartBlocked()
|
|
285
|
+
? this.#discord
|
|
286
|
+
: undefined
|
|
287
|
+
: this.#slack?.restartBlocked()
|
|
288
|
+
? this.#slack
|
|
289
|
+
: undefined;
|
|
290
|
+
if (retainedProvider) {
|
|
291
|
+
try {
|
|
292
|
+
await this.#router.start();
|
|
293
|
+
await retainedProvider.start();
|
|
294
|
+
return;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
await this.#router.stop().catch(() => undefined);
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
453
300
|
if (this.input.kind === "discord") {
|
|
454
301
|
const config = this.input.config.notifications.discord;
|
|
455
302
|
if (!config) throw new Error("Discord chat daemon provider configuration is unavailable.");
|
|
@@ -458,7 +305,7 @@ export class ChatDaemonRuntime {
|
|
|
458
305
|
((value: NonNullable<ChatDaemonRuntimeConfig["notifications"]["discord"]>) =>
|
|
459
306
|
new DiscordLiveProvider(value))
|
|
460
307
|
)(config);
|
|
461
|
-
this.#transportHealthy = () => this.#
|
|
308
|
+
this.#transportHealthy = () => this.#router.isReady() && (provider.transportHealthy ?? true);
|
|
462
309
|
this.#presentation = new NotificationPresentationEngine(
|
|
463
310
|
[createDiscordAdapter({ channelId: config.parentChannelId })],
|
|
464
311
|
{
|
|
@@ -468,17 +315,13 @@ export class ChatDaemonRuntime {
|
|
|
468
315
|
);
|
|
469
316
|
this.#discord = new DiscordNotificationDaemon({
|
|
470
317
|
agentDir: this.input.agentDir,
|
|
471
|
-
repo: "",
|
|
472
318
|
guildId: config.guildId,
|
|
473
319
|
parentChannelId: config.parentChannelId,
|
|
474
320
|
provider,
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
throw new DiscordEndpointBindingError("Discord session endpoint changed before command dispatch.");
|
|
480
|
-
return await this.#runChatCommand("discord", sessionId, content, attached.client, idempotencyKey);
|
|
481
|
-
},
|
|
321
|
+
resolveAttachment: (sessionId, expectedGeneration) =>
|
|
322
|
+
this.#router.attachment(sessionId, expectedGeneration),
|
|
323
|
+
onCommand: async (sessionId, content, attachment, idempotencyKey) =>
|
|
324
|
+
await this.#runChatCommand("discord", sessionId, content, attachment, idempotencyKey),
|
|
482
325
|
});
|
|
483
326
|
} else {
|
|
484
327
|
const config = this.input.config.notifications.slack;
|
|
@@ -487,7 +330,7 @@ export class ChatDaemonRuntime {
|
|
|
487
330
|
this.deps.createSlackProvider ??
|
|
488
331
|
((value: NonNullable<ChatDaemonRuntimeConfig["notifications"]["slack"]>) => new SlackLiveProvider(value))
|
|
489
332
|
)(config);
|
|
490
|
-
this.#transportHealthy = () => this.#
|
|
333
|
+
this.#transportHealthy = () => this.#router.isReady() && (provider.transportHealthy ?? true);
|
|
491
334
|
this.#presentation = new NotificationPresentationEngine(
|
|
492
335
|
[createSlackAdapter({ channelId: config.channelId })],
|
|
493
336
|
{
|
|
@@ -502,45 +345,16 @@ export class ChatDaemonRuntime {
|
|
|
502
345
|
channelId: config.channelId,
|
|
503
346
|
provider: new SlackProvider(provider),
|
|
504
347
|
authorizeActor: async actorId => config.authorizedUserId === actorId,
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
attached.generation !== endpoint.generation ||
|
|
510
|
-
attached.endpoint.url !== endpoint.url ||
|
|
511
|
-
attached.endpoint.token !== endpoint.token
|
|
512
|
-
)
|
|
513
|
-
throw new SlackEndpointBindingError();
|
|
514
|
-
return {
|
|
515
|
-
send: frame => {
|
|
516
|
-
if (this.#sessions.get(endpoint.sessionId) !== attached) throw new SlackEndpointBindingError();
|
|
517
|
-
attached.client.send(frame);
|
|
518
|
-
},
|
|
519
|
-
};
|
|
520
|
-
},
|
|
521
|
-
resolveEndpoint: async sessionId => await this.resolveEndpoint(sessionId),
|
|
522
|
-
resolveBindingAuthority: async sessionId => await this.#slackBindingAuthority(sessionId),
|
|
523
|
-
onCommand: async (sessionId, content, endpoint, idempotencyKey) => {
|
|
524
|
-
const attached = this.#sessions.get(sessionId);
|
|
525
|
-
if (
|
|
526
|
-
!attached ||
|
|
527
|
-
attached.generation !== endpoint.generation ||
|
|
528
|
-
attached.endpoint.url !== endpoint.url ||
|
|
529
|
-
attached.endpoint.token !== endpoint.token
|
|
530
|
-
)
|
|
531
|
-
throw new SlackEndpointBindingError("Slack session endpoint changed before command dispatch.");
|
|
532
|
-
return await this.#runChatCommand("slack", sessionId, content, attached.client, idempotencyKey);
|
|
533
|
-
},
|
|
348
|
+
resolveAttachment: async sessionId => this.#router.attachment(sessionId),
|
|
349
|
+
resolveBindingAuthority: async sessionId => await this.#router.bindingAuthority(sessionId),
|
|
350
|
+
onCommand: async (sessionId, content, attachment, idempotencyKey) =>
|
|
351
|
+
await this.#runChatCommand("slack", sessionId, content, attachment, idempotencyKey),
|
|
534
352
|
});
|
|
535
353
|
}
|
|
536
354
|
try {
|
|
537
|
-
await this.#
|
|
355
|
+
await this.#router.start();
|
|
538
356
|
if (this.#discord) await this.#discord.start();
|
|
539
357
|
if (this.#slack) await this.#slack.start();
|
|
540
|
-
const timer = (this.deps.setInterval ?? setInterval)(() => {
|
|
541
|
-
this.schedule(this.#serialReconcile());
|
|
542
|
-
}, 2_000);
|
|
543
|
-
this.#stopTimer = () => (this.deps.clearInterval ?? clearInterval)(timer);
|
|
544
358
|
} catch (error) {
|
|
545
359
|
await this.stop();
|
|
546
360
|
throw error;
|
|
@@ -552,441 +366,27 @@ export class ChatDaemonRuntime {
|
|
|
552
366
|
}
|
|
553
367
|
|
|
554
368
|
async stop(): Promise<void> {
|
|
555
|
-
|
|
556
|
-
this.#
|
|
557
|
-
|
|
558
|
-
this.#discord = undefined;
|
|
559
|
-
this.#slack = undefined;
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
await Promise.allSettled([...this.#pending]);
|
|
564
|
-
for (const [sessionId, attached] of this.#sessions) {
|
|
565
|
-
this.#sessions.delete(sessionId);
|
|
566
|
-
attached.dispose();
|
|
567
|
-
await attached.client.close();
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
#serialReconcile(): Promise<void> {
|
|
572
|
-
const task = this.#reconcileTail
|
|
573
|
-
.catch(() => undefined)
|
|
574
|
-
.then(async () => {
|
|
575
|
-
try {
|
|
576
|
-
await this.reconcile();
|
|
577
|
-
this.#reconcileReady = true;
|
|
578
|
-
this.deps.onReconciled?.();
|
|
579
|
-
} catch (error) {
|
|
580
|
-
this.#reconcileReady = false;
|
|
581
|
-
throw error;
|
|
582
|
-
}
|
|
583
|
-
});
|
|
584
|
-
this.#reconcileTail = task;
|
|
585
|
-
return task;
|
|
586
|
-
}
|
|
587
|
-
private async reconcile(): Promise<void> {
|
|
588
|
-
await this.#index.open();
|
|
589
|
-
await this.#index.refresh();
|
|
590
|
-
const live = this.#index.listSessions().sessions.filter(session => session.live);
|
|
591
|
-
const ids = new Set(live.map(session => session.sessionId));
|
|
592
|
-
for (const session of live) await this.attach(session);
|
|
593
|
-
for (const [sessionId, attached] of this.#sessions) {
|
|
594
|
-
if (ids.has(sessionId)) continue;
|
|
595
|
-
this.#sessions.delete(sessionId);
|
|
596
|
-
attached.dispose();
|
|
597
|
-
await attached.client.close();
|
|
598
|
-
await this.close(sessionId);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
private async attach(indexed: IndexedSession): Promise<void> {
|
|
603
|
-
const repo = path.resolve(indexed.locator.repo);
|
|
604
|
-
const defaultStateRoot = path.join(repo, ".gjc", "state");
|
|
605
|
-
const indexedStateRoot = path.resolve(indexed.locator.stateRoot);
|
|
606
|
-
const scope =
|
|
607
|
-
indexedStateRoot === defaultStateRoot
|
|
608
|
-
? "default"
|
|
609
|
-
: indexedStateRoot === path.join(defaultStateRoot, "chat")
|
|
610
|
-
? "chat"
|
|
611
|
-
: undefined;
|
|
612
|
-
if (!scope || indexed.endpointMtimeMs === undefined) return;
|
|
613
|
-
const endpoint = await readSdkSessionEndpoint(repo, indexed.sessionId, scope);
|
|
614
|
-
if (!endpoint || endpoint.stale) return;
|
|
615
|
-
const endpointStat = await fs.stat(endpoint.path).catch(() => undefined);
|
|
616
|
-
if (!endpointStat || endpointStat.mtimeMs !== indexed.endpointMtimeMs) return;
|
|
617
|
-
const existing = this.#sessions.get(indexed.sessionId);
|
|
618
|
-
const resumable =
|
|
619
|
-
existing !== undefined &&
|
|
620
|
-
existing.endpoint.url === endpoint.url &&
|
|
621
|
-
existing.endpoint.token === endpoint.token &&
|
|
622
|
-
existing.generation === indexed.endpointGeneration;
|
|
623
|
-
if (existing && resumable && !existing.barrier.failed) {
|
|
624
|
-
// The attachment is current, so reconcile's remaining job is keeping its socket
|
|
625
|
-
// dialed: `SdkClient` retires a closed socket and re-dials only on the next
|
|
626
|
-
// request, and a passive subscription issues none.
|
|
627
|
-
this.#reviveTransport(existing);
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
// A failed barrier is rebuilt rather than revived, and its cursor travels into the
|
|
631
|
-
// replacement: same endpoint, same generation, same stream, so the replay this
|
|
632
|
-
// attach issues asks for exactly the gap the failed round left open, and every
|
|
633
|
-
// sequence already published stays below the cursor that drops it. A rolled
|
|
634
|
-
// generation is a different stream, so that fence resets the cursor to zero.
|
|
635
|
-
const resumeSeq = existing && resumable ? existing.cursor.seq : 0;
|
|
636
|
-
if (existing) {
|
|
637
|
-
this.#sessions.delete(indexed.sessionId);
|
|
638
|
-
existing.dispose();
|
|
639
|
-
await existing.client.close();
|
|
369
|
+
const providerResults = await Promise.allSettled([this.#discord?.stop(), this.#slack?.stop()]);
|
|
370
|
+
const discordRestartBlocked = this.#discord?.restartBlocked() ?? false;
|
|
371
|
+
const slackRestartBlocked = this.#slack?.restartBlocked() ?? false;
|
|
372
|
+
if (!discordRestartBlocked) this.#discord = undefined;
|
|
373
|
+
if (!slackRestartBlocked) this.#slack = undefined;
|
|
374
|
+
if (!discordRestartBlocked && !slackRestartBlocked) {
|
|
375
|
+
this.#presentation = undefined;
|
|
376
|
+
this.#transportHealthy = undefined;
|
|
640
377
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
const disposeFrames = client.onFrame(frame => {
|
|
645
|
-
if (attached) this.schedule(this.enqueueFrame(attached, frame, "live"));
|
|
646
|
-
});
|
|
647
|
-
// The frame subscription is client-scoped and survives the socket, but a
|
|
648
|
-
// replacement socket resumes delivery where the stream stands then. Only a replay
|
|
649
|
-
// from this attachment's cursor closes the gap the drop left behind, and that cursor
|
|
650
|
-
// is read here — synchronously with the hello — so the replay asks for exactly the
|
|
651
|
-
// gap the barrier it opens is holding the replacement socket's frames ahead of.
|
|
652
|
-
//
|
|
653
|
-
// Deliberately outside `#pending`, like `#reviveTransport`: a replay that never
|
|
654
|
-
// answers must not make `stop()` wait for it. Disposal revokes the barrier and
|
|
655
|
-
// closing the client settles the request, so the round ends without publishing.
|
|
656
|
-
const disposeReconnect = client.onReconnect?.(() => {
|
|
657
|
-
if (attached) void this.#replayAttachment(attached, attached.cursor.seq).catch(() => undefined);
|
|
658
|
-
});
|
|
659
|
-
attached = Object.freeze({
|
|
660
|
-
id: randomUUID(),
|
|
661
|
-
sessionId: indexed.sessionId,
|
|
662
|
-
endpoint,
|
|
663
|
-
generation: indexed.endpointGeneration,
|
|
664
|
-
client,
|
|
665
|
-
cursor: { seq: resumeSeq },
|
|
666
|
-
barrier,
|
|
667
|
-
dispose: () => {
|
|
668
|
-
disposeFrames();
|
|
669
|
-
disposeReconnect?.();
|
|
670
|
-
// A detached attachment's outstanding replay is dead work: revoking the
|
|
671
|
-
// barrier here is what keeps its held frames off the newer attachment.
|
|
672
|
-
barrier.detached = true;
|
|
673
|
-
barrier.held = undefined;
|
|
674
|
-
},
|
|
675
|
-
});
|
|
676
|
-
this.#sessions.set(indexed.sessionId, attached);
|
|
677
|
-
this.#presentation?.connectSession(indexed.sessionId, {
|
|
678
|
-
sendReply: route => {
|
|
679
|
-
if (this.#sessions.get(indexed.sessionId) !== attached)
|
|
680
|
-
throw new Error("Session endpoint changed before reply.");
|
|
681
|
-
attached.client.send({ type: "reply", id: route.actionId, answer: route.answer });
|
|
682
|
-
},
|
|
683
|
-
});
|
|
684
|
-
await this.#replayAttachment(attached, resumeSeq);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/**
|
|
688
|
-
* Keep an established attachment's socket dialed.
|
|
689
|
-
*
|
|
690
|
-
* `SdkClient` never opens a replacement socket on its own: it retires the closed
|
|
691
|
-
* incarnation and re-dials on the next `connect`/`request`. A chat attachment is
|
|
692
|
-
* purely passive, so without this probe a transient drop would silently end
|
|
693
|
-
* delivery for good. `connect()` resolves immediately on a live socket, spends the
|
|
694
|
-
* session reconnect budget on a dead one, and — because reconcile only reaches
|
|
695
|
-
* here for an endpoint it has just re-read as current at this generation — never
|
|
696
|
-
* revives an attachment the index has moved on from.
|
|
697
|
-
*/
|
|
698
|
-
#reviveTransport(attached: AttachedSession): void {
|
|
699
|
-
const connect = attached.client.connect?.bind(attached.client);
|
|
700
|
-
if (!connect || this.#reviving.has(attached.id)) return;
|
|
701
|
-
this.#reviving.add(attached.id);
|
|
702
|
-
// Deliberately outside `#pending`: the reconnect budget outlives the heartbeat
|
|
703
|
-
// TTL, and `stop()` must not wait for it. Closing the client aborts it instead.
|
|
704
|
-
void connect()
|
|
705
|
-
.catch(() => undefined)
|
|
706
|
-
.finally(() => this.#reviving.delete(attached.id));
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
/**
|
|
710
|
-
* Issue one replay for `attached` and publish its answer ahead of the live frames
|
|
711
|
-
* that raced it.
|
|
712
|
-
*
|
|
713
|
-
* A reconnect leaves two producers writing one stream: the replacement socket, whose
|
|
714
|
-
* subscription never stopped, and the replay answer. Ingress is therefore fenced for
|
|
715
|
-
* the whole round trip — frames the socket delivers after the hello are held, the
|
|
716
|
-
* replayed events are published first, and the held frames are drained afterwards in
|
|
717
|
-
* sequence order — so delivery follows sequence rather than arrival. Both paths
|
|
718
|
-
* publish through the same cursor, so an event both producers carried is published
|
|
719
|
-
* exactly once. Frames carrying no sequence of this attachment's own cannot be
|
|
720
|
-
* ordered against a replay at all, so they are never held and never delayed.
|
|
721
|
-
*
|
|
722
|
-
* The replay is fenced exactly like `attach()`: it is issued for the attachment's
|
|
723
|
-
* own endpoint generation and from its own cursor, and it is dropped whole if the
|
|
724
|
-
* runtime has since detached, rolled, or superseded this attachment — so a stale
|
|
725
|
-
* incarnation can never resurrect its events onto a newer one's root.
|
|
726
|
-
*
|
|
727
|
-
* A refused replay loses nothing either: the cursor stays where the request was
|
|
728
|
-
* issued from, the held frames stay held, and the round re-issues on the socket
|
|
729
|
-
* it already has. When that budget runs out the barrier fails whole and the
|
|
730
|
-
* attachment is rebuilt from its cursor. A replay that reports a retention gap
|
|
731
|
-
* is not a round to retry: the sequences it names are evicted at the host, so no
|
|
732
|
-
* rebuild can re-fetch them. That round concedes them instead — loudly — carries
|
|
733
|
-
* the cursor over them, and publishes the suffix the host did keep, so the gap
|
|
734
|
-
* costs exactly the sequences the host lost and nothing that follows them. The
|
|
735
|
-
* concession is what moves the cursor, not the suffix: a host that kept every
|
|
736
|
-
* sequence above the gap can still answer with none of them, and a cursor left
|
|
737
|
-
* below the lost range would concede it again on every later replay. A gap is
|
|
738
|
-
* conceded only where it answers this round's own request: one that opens above
|
|
739
|
-
* the cursor, or that the same answer returns retained events from, states no
|
|
740
|
-
* bounded loss and is fenced like any other answer nothing can be concluded from.
|
|
741
|
-
* What the host evicted is not what delivery lost: a sequence in the conceded
|
|
742
|
-
* range that the replacement socket already carried is held by this round's
|
|
743
|
-
* barrier, so it is published before the cursor steps over the rest.
|
|
744
|
-
*/
|
|
745
|
-
async #replayAttachment(attached: AttachedSession, sinceSeq: number): Promise<void> {
|
|
746
|
-
if (!this.#attachmentLive(attached)) return;
|
|
747
|
-
const held: HeldFrame[] = [];
|
|
748
|
-
attached.barrier.held = held;
|
|
749
|
-
try {
|
|
750
|
-
let replay: Record<string, unknown>;
|
|
751
|
-
for (let attempt = 0; ; attempt++) {
|
|
752
|
-
try {
|
|
753
|
-
replay = await attached.client.request({
|
|
754
|
-
type: "event_replay",
|
|
755
|
-
sinceGeneration: attached.generation,
|
|
756
|
-
sinceSeq,
|
|
757
|
-
});
|
|
758
|
-
break;
|
|
759
|
-
} catch {
|
|
760
|
-
// An unanswered replay is a transport failure, not a delivery decision, and
|
|
761
|
-
// the gap it owed is still owed: the cursor stays at `sinceSeq` and the held
|
|
762
|
-
// frames stay held, so the retry asks for exactly the same gap. It is issued
|
|
763
|
-
// on this attachment's own live socket, which is the only producer that can
|
|
764
|
-
// close the gap when no hello is coming.
|
|
765
|
-
if (attempt >= REPLAY_RETRY_ATTEMPTS) {
|
|
766
|
-
this.#failBarrier(attached, "replay went unanswered");
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
await Bun.sleep(REPLAY_RETRY_BACKOFF_MS * 2 ** attempt);
|
|
770
|
-
// A later round or a disposal may have taken this attachment's ingress over
|
|
771
|
-
// while the retry waited. That round owes the same gap from the same cursor,
|
|
772
|
-
// so this one stands down instead of asking for it twice.
|
|
773
|
-
if (attached.barrier.held !== held) return;
|
|
774
|
-
if (!this.#attachmentLive(attached)) return;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
if (attached.barrier.held !== held) return;
|
|
778
|
-
if (!this.#attachmentLive(attached)) return;
|
|
779
|
-
// The answer rides the same socket as the live frames that preceded it, but
|
|
780
|
-
// ingress is a queue: a frame this socket already delivered can still be in
|
|
781
|
-
// flight when the answer resolves, and the hold buffer does not carry it until
|
|
782
|
-
// it lands. What live delivery already carried is therefore only readable once
|
|
783
|
-
// that queue has caught up, so the round joins it before reading its own
|
|
784
|
-
// answer — otherwise a conceded range would step the cursor over a sequence
|
|
785
|
-
// still on its way into the buffer, and that frame would be dropped as already
|
|
786
|
-
// delivered by a cursor no producer ever published it past.
|
|
787
|
-
await this.#frameTails.get(attached.sessionId)?.catch(() => undefined);
|
|
788
|
-
if (attached.barrier.held !== held) return;
|
|
789
|
-
if (!this.#attachmentLive(attached)) return;
|
|
790
|
-
// The answer's own events are read before anything is concluded from its gap: a
|
|
791
|
-
// concession is only readable against the suffix it arrived with.
|
|
792
|
-
const events = Array.isArray(replay.events)
|
|
793
|
-
? replay.events.filter(
|
|
794
|
-
(event): event is Record<string, unknown> =>
|
|
795
|
-
!!event && typeof event === "object" && !Array.isArray(event),
|
|
796
|
-
)
|
|
797
|
-
: [];
|
|
798
|
-
if (replay.gap !== undefined) {
|
|
799
|
-
// The host answered, and its answer says part of what this cursor asked for
|
|
800
|
-
// is gone for good. Neither a retry nor a rebuild can recover an evicted
|
|
801
|
-
// sequence, so refusing to publish would trade a bounded loss for a total
|
|
802
|
-
// outage on a healthy socket, and the 2-second reconcile would rebuild into
|
|
803
|
-
// the same gap forever. Concede it instead — loudly — and publish the suffix
|
|
804
|
-
// the host did keep, so the gap costs exactly the sequences the host lost and
|
|
805
|
-
// nothing that follows them.
|
|
806
|
-
const gap = readReplayGap(replay.gap);
|
|
807
|
-
if (!gap) {
|
|
808
|
-
this.#failBarrier(attached, "replay reported a gap it did not state");
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
if (gap.kind === "generation_reset") {
|
|
812
|
-
// A reset stream shares no sequence space with this cursor, so nothing in
|
|
813
|
-
// the answer can be ordered against it. Reconcile rebuilds against the
|
|
814
|
-
// generation the index publishes now, which is a different fence entirely.
|
|
815
|
-
this.#failBarrier(attached, `replay reported a generation reset to ${gap.toGeneration}`);
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
// A gap is a statement about this round's own request, so it is conceded only
|
|
819
|
-
// where it answers one: it must open at the first sequence this round asked
|
|
820
|
-
// for, and the suffix the host kept must sit entirely above it. A range this
|
|
821
|
-
// cursor never asked about, or one the same answer returns events from, states
|
|
822
|
-
// no bounded loss at all, and conceding it would fence off sequences the host
|
|
823
|
-
// still holds — the one thing a retention gap must never cost.
|
|
824
|
-
if (gap.fromSeq !== sinceSeq + 1) {
|
|
825
|
-
this.#failBarrier(
|
|
826
|
-
attached,
|
|
827
|
-
`replay conceded sequences ${gap.fromSeq}-${gap.toSeq} for a request that resumed from seq ${sinceSeq}`,
|
|
828
|
-
);
|
|
829
|
-
return;
|
|
830
|
-
}
|
|
831
|
-
const retained = events
|
|
832
|
-
.map(event => readSequence(event.seq))
|
|
833
|
-
.find(seq => seq !== undefined && seq <= gap.toSeq);
|
|
834
|
-
if (retained !== undefined) {
|
|
835
|
-
this.#failBarrier(
|
|
836
|
-
attached,
|
|
837
|
-
`replay conceded sequences ${gap.fromSeq}-${gap.toSeq} while returning seq ${retained}`,
|
|
838
|
-
);
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
// A sequence the host lost from its ring can still have arrived over the
|
|
842
|
-
// replacement socket and be sitting in this round's hold buffer: the two
|
|
843
|
-
// producers fail independently, so eviction at the host says nothing about
|
|
844
|
-
// what live delivery already carried. Those frames are published here — in
|
|
845
|
-
// sequence order, ahead of the retained suffix that sits above them — and
|
|
846
|
-
// leave the buffer, so the concession costs only the sequences neither
|
|
847
|
-
// producer holds and the drain cannot re-offer one the cursor just passed.
|
|
848
|
-
const recovered = held.filter(entry => entry.seq <= gap.toSeq).sort((left, right) => left.seq - right.seq);
|
|
849
|
-
const carried = held.filter(entry => entry.seq > gap.toSeq);
|
|
850
|
-
held.splice(0, held.length, ...carried);
|
|
851
|
-
const recoveredNote =
|
|
852
|
-
recovered.length > 0 ? `, ${recovered.length} of them recovered from live delivery` : "";
|
|
853
|
-
logger.warn(
|
|
854
|
-
`chat daemon replay conceded a retention gap (sequences ${gap.fromSeq}-${gap.toSeq} are gone from the host${recoveredNote}); session ${attached.sessionId} generation ${attached.generation} resumes at seq ${gap.toSeq + 1}.`,
|
|
855
|
-
);
|
|
856
|
-
for (const entry of recovered) await this.enqueueFrame(attached, entry.frame, "ordered");
|
|
857
|
-
// The concession itself carries the cursor over the rest of the lost range,
|
|
858
|
-
// because the retained suffix cannot be trusted to do it: `SessionSdkHost`
|
|
859
|
-
// drops capability-gated kinds from every replay answer, so a host that still
|
|
860
|
-
// holds every sequence above the gap can legitimately answer with none of
|
|
861
|
-
// them. A cursor left below a range no host can re-serve would resume the
|
|
862
|
-
// next replay from the same evicted sequence and concede the same permanent
|
|
863
|
-
// loss on every reconnect, forever.
|
|
864
|
-
if (gap.toSeq > attached.cursor.seq) attached.cursor.seq = gap.toSeq;
|
|
865
|
-
}
|
|
866
|
-
for (const event of events) await this.enqueueFrame(attached, event, "ordered");
|
|
867
|
-
await this.#drainHeldFrames(attached, held);
|
|
868
|
-
} finally {
|
|
869
|
-
// Only this round's own buffer is revoked: a later round or a disposal may have
|
|
870
|
-
// installed its own, and clearing that one would strand the frames it holds.
|
|
871
|
-
if (attached.barrier.held === held) attached.barrier.held = undefined;
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
/**
|
|
876
|
-
* Whether `attached` is still the incarnation this runtime publishes through.
|
|
877
|
-
*
|
|
878
|
-
* Detachment, supersession, and a barrier that could not close its gap all retire
|
|
879
|
-
* an attachment the same way: whatever it still carries is dead work, and
|
|
880
|
-
* publishing any of it would either resurrect a stale incarnation or step the
|
|
881
|
-
* cursor over a sequence nobody delivered.
|
|
882
|
-
*/
|
|
883
|
-
#attachmentLive(attached: AttachedSession): boolean {
|
|
884
|
-
return (
|
|
885
|
-
!attached.barrier.detached && !attached.barrier.failed && this.#sessions.get(attached.sessionId) === attached
|
|
886
|
-
);
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
/**
|
|
890
|
-
* Retire one attachment whose barrier could not close the gap it fenced.
|
|
891
|
-
*
|
|
892
|
-
* The cursor is left at the last contiguously published sequence and delivery
|
|
893
|
-
* stops there, so nothing can carry it over the gap. Reconcile then rebuilds the
|
|
894
|
-
* attachment against the same endpoint generation and hands that cursor to the
|
|
895
|
-
* replacement, whose replay re-fetches exactly what this round could not close —
|
|
896
|
-
* held frames are dropped only because that replay re-issues every one of them.
|
|
897
|
-
* Only an inconclusive round retires this way: a host that names what it lost is
|
|
898
|
-
* conceded where the answer is read, because no rebuild could recover it.
|
|
899
|
-
*
|
|
900
|
-
* Loud, because a barrier that cannot close is a transport fault rather than a
|
|
901
|
-
* delivery decision.
|
|
902
|
-
*/
|
|
903
|
-
#failBarrier(attached: AttachedSession, reason: string): void {
|
|
904
|
-
if (attached.barrier.detached || attached.barrier.failed) return;
|
|
905
|
-
attached.barrier.failed = true;
|
|
906
|
-
attached.barrier.held = undefined;
|
|
907
|
-
logger.warn(
|
|
908
|
-
`chat daemon replay barrier failed (${reason}); rebuilding session ${attached.sessionId} at generation ${attached.generation} from seq ${attached.cursor.seq}.`,
|
|
378
|
+
await this.#router.stop();
|
|
379
|
+
const failures = providerResults.filter(
|
|
380
|
+
(result): result is PromiseRejectedResult => result.status === "rejected",
|
|
909
381
|
);
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
*
|
|
915
|
-
* The cursor is delivery's record, so a frame no surface took must stay above it. The
|
|
916
|
-
* attachment is retired exactly like one whose barrier failed, and the rebuild replays
|
|
917
|
-
* from a cursor that still sits below the frame. Later frames cannot drag the cursor
|
|
918
|
-
* over it either, because a retired attachment publishes nothing.
|
|
919
|
-
*
|
|
920
|
-
* The rounds are bounded, because a surface that is down for good would re-serve that
|
|
921
|
-
* same sequence forever and never deliver another. On the last one the frame is
|
|
922
|
-
* conceded — loudly, like a retention gap — and the cursor steps over it, since a
|
|
923
|
-
* stream that never advances again loses every later frame instead of just this one.
|
|
924
|
-
*/
|
|
925
|
-
#failDelivery(attached: AttachedSession, seq: number, error: unknown): void {
|
|
926
|
-
const previous = this.#undelivered.get(attached.sessionId);
|
|
927
|
-
const attempts = previous?.generation === attached.generation && previous.seq === seq ? previous.attempts + 1 : 1;
|
|
928
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
929
|
-
if (attempts >= DELIVERY_ATTEMPT_LIMIT) {
|
|
930
|
-
this.#undelivered.delete(attached.sessionId);
|
|
931
|
-
attached.cursor.seq = seq;
|
|
932
|
-
logger.warn(
|
|
933
|
-
`chat daemon conceded seq ${seq} of session ${attached.sessionId} at generation ${attached.generation} after ${attempts} refused publications (${reason}); delivery resumes above it.`,
|
|
382
|
+
if (failures.length > 0)
|
|
383
|
+
throw new AggregateError(
|
|
384
|
+
failures.map(result => result.reason),
|
|
385
|
+
"Provider shutdown failed after Router authority revocation.",
|
|
934
386
|
);
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
this.#undelivered.set(attached.sessionId, { generation: attached.generation, seq, attempts });
|
|
938
|
-
this.#failBarrier(attached, `publication failed at seq ${seq} (${reason})`);
|
|
939
387
|
}
|
|
940
388
|
|
|
941
|
-
/**
|
|
942
|
-
* Publish what this round held, lowest sequence first, then reopen live ingress.
|
|
943
|
-
*
|
|
944
|
-
* Frames keep arriving while the drain runs, so the barrier is only lifted by a pass
|
|
945
|
-
* that finds nothing left to publish: lifting it earlier would let a frame arriving
|
|
946
|
-
* mid-drain overtake one still held. That lift is synchronous with the emptiness
|
|
947
|
-
* check, because anything queued behind an await could otherwise land in a buffer
|
|
948
|
-
* nobody owns any more.
|
|
949
|
-
*/
|
|
950
|
-
async #drainHeldFrames(attached: AttachedSession, held: HeldFrame[]): Promise<void> {
|
|
951
|
-
for (;;) {
|
|
952
|
-
if (attached.barrier.held !== held) return;
|
|
953
|
-
if (!this.#attachmentLive(attached)) return;
|
|
954
|
-
if (held.length === 0) {
|
|
955
|
-
attached.barrier.held = undefined;
|
|
956
|
-
return;
|
|
957
|
-
}
|
|
958
|
-
const batch = held.splice(0, held.length).sort((left, right) => left.seq - right.seq);
|
|
959
|
-
for (const entry of batch) await this.enqueueFrame(attached, entry.frame, "ordered");
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
private async resolveEndpoint(sessionId: string): Promise<SlackEndpoint | null> {
|
|
964
|
-
const attached = this.#sessions.get(sessionId);
|
|
965
|
-
return attached ? { ...attached.endpoint, generation: attached.generation } : null;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
/**
|
|
969
|
-
* Exact authority for adopting an existing root: the runtime must currently
|
|
970
|
-
* hold this session's attachment, the index must still list it as live and
|
|
971
|
-
* non-terminal with an intact replay, and its discovery endpoint must be
|
|
972
|
-
* readable, non-stale, and owned by the indexed host pid at the same
|
|
973
|
-
* generation. Re-reading the attachment afterwards rejects a session that
|
|
974
|
-
* detached or rolled while the index and endpoint were being consulted.
|
|
975
|
-
*/
|
|
976
|
-
async #slackBindingAuthority(sessionId: string): Promise<SlackBindingAuthority | undefined> {
|
|
977
|
-
const attached = this.#sessions.get(sessionId);
|
|
978
|
-
if (!attached) return undefined;
|
|
979
|
-
const authority = await resolveSessionBindingAuthority({ sessionIndex: this.#index, sessionId });
|
|
980
|
-
if (!authority || authority.endpointGeneration !== attached.generation) return undefined;
|
|
981
|
-
if (this.#sessions.get(sessionId) !== attached) return undefined;
|
|
982
|
-
return { sessionId, endpointGeneration: attached.generation };
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
/**
|
|
986
|
-
* Adopt an operator-supplied Slack root for one attached session. Returns a
|
|
987
|
-
* machine-readable rejection instead of throwing so the daemon control plane
|
|
988
|
-
* can answer without exposing internal failure detail.
|
|
989
|
-
*/
|
|
389
|
+
/** Adopt an operator-supplied Slack root through the provider's presentation store. */
|
|
990
390
|
async bindExistingRoot(request: ChatDaemonCommandBindInput): Promise<ChatDaemonCommandOutcome> {
|
|
991
391
|
const slack = this.#slack;
|
|
992
392
|
if (!slack) return { ok: false, certainty: "rejected", code: "target_not_configured" };
|
|
@@ -1003,156 +403,74 @@ export class ChatDaemonRuntime {
|
|
|
1003
403
|
rootTs: bound.rootTs,
|
|
1004
404
|
};
|
|
1005
405
|
} catch (error) {
|
|
1006
|
-
// `binding_outcome_unknown` is the store's typed statement that the
|
|
1007
|
-
// mapping may already be applied. It must travel as an indeterminate
|
|
1008
|
-
// outcome, never as a rejection the operator could act on.
|
|
1009
406
|
const code = error instanceof SlackThreadBindingError ? error.code : "binding_failed";
|
|
1010
407
|
return code === "binding_outcome_unknown"
|
|
1011
408
|
? { ok: false, certainty: "unknown", code }
|
|
1012
409
|
: { ok: false, certainty: "rejected", code };
|
|
1013
410
|
}
|
|
1014
411
|
}
|
|
1015
|
-
#discordEndpoint(sessionId: string): DiscordEndpointBinding | null {
|
|
1016
|
-
const attached = this.#sessions.get(sessionId);
|
|
1017
|
-
if (!attached) return null;
|
|
1018
|
-
return {
|
|
1019
|
-
generation: attached.generation,
|
|
1020
|
-
isCurrent: () => this.#sessions.get(sessionId) === attached,
|
|
1021
|
-
send: frame => {
|
|
1022
|
-
if (this.#sessions.get(sessionId) !== attached) throw new DiscordEndpointBindingError();
|
|
1023
|
-
attached.client.send(frame);
|
|
1024
|
-
},
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
412
|
|
|
1028
|
-
|
|
1029
|
-
this.#
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
this.#frameTails.set(attached.sessionId, current);
|
|
1039
|
-
void current.then(
|
|
1040
|
-
() => {
|
|
1041
|
-
if (this.#frameTails.get(attached.sessionId) === current) this.#frameTails.delete(attached.sessionId);
|
|
1042
|
-
},
|
|
1043
|
-
() => {
|
|
1044
|
-
if (this.#frameTails.get(attached.sessionId) === current) this.#frameTails.delete(attached.sessionId);
|
|
1045
|
-
},
|
|
1046
|
-
);
|
|
1047
|
-
return current;
|
|
1048
|
-
}
|
|
1049
|
-
private async handleFrame(
|
|
1050
|
-
attached: AttachedSession,
|
|
1051
|
-
frame: Record<string, unknown>,
|
|
1052
|
-
origin: FrameOrigin,
|
|
1053
|
-
): Promise<void> {
|
|
1054
|
-
if (!this.#attachmentLive(attached)) return;
|
|
1055
|
-
// Correlate before anything is acted on. A frame whose envelope and payload
|
|
1056
|
-
// disagree on session, lifecycle identity, or lifecycle generation is not a
|
|
1057
|
-
// usable event, so it is dropped whole — no close, resume, notify, resolve,
|
|
1058
|
-
// root, or mapping mutation — on the replay path and the live path alike.
|
|
1059
|
-
const correlated = correlateFrame(frame);
|
|
1060
|
-
if (!correlated) return;
|
|
1061
|
-
// One event delivered under this attachment's identity is one sequence, and a
|
|
1062
|
-
// reconnect leaves two producers carrying it: the replacement socket, whose
|
|
1063
|
-
// subscription never stopped, and the replay answer. So the cursor both orders
|
|
1064
|
-
// delivery and settles it. A sequence at or below the cursor was already
|
|
1065
|
-
// published and is rejected outright rather than merely declining to advance,
|
|
1066
|
-
// or the frame both producers carried is published twice; and every sequence
|
|
1067
|
-
// above it advances the cursor, including the ones the filters below drop,
|
|
1068
|
-
// because a cursor left behind by dropped-but-delivered frames would re-deliver
|
|
1069
|
-
// every notification interleaved with them. The fences are the two `attach()`
|
|
1070
|
-
// already applies — this session, this endpoint generation — so a foreign or
|
|
1071
|
-
// superseded frame neither moves the cursor nor enters the barrier.
|
|
1072
|
-
const seq = typeof frame.seq === "number" && Number.isSafeInteger(frame.seq) ? frame.seq : undefined;
|
|
1073
|
-
const ownsSequence =
|
|
1074
|
-
correlated.generation === attached.generation &&
|
|
1075
|
-
(correlated.sessionId === undefined || correlated.sessionId === attached.sessionId);
|
|
1076
|
-
if (seq !== undefined && ownsSequence) {
|
|
1077
|
-
if (seq <= attached.cursor.seq) return;
|
|
1078
|
-
const held = attached.barrier.held;
|
|
1079
|
-
if (held && origin === "live") {
|
|
1080
|
-
// A replay is outstanding, so where this frame sits in the stream is not
|
|
1081
|
-
// settled yet: hold it, and let the drain place it after the answer.
|
|
1082
|
-
if (held.length >= REPLAY_BARRIER_LIMIT) {
|
|
1083
|
-
// Overflow is not a choice between frames: evicting the oldest silently
|
|
1084
|
-
// skips the sequence the cursor needs next, and evicting the newest leaves
|
|
1085
|
-
// a hole no drain can close. So the barrier fails whole instead, and the
|
|
1086
|
-
// rebuild re-fetches the gap from this attachment's own cursor — the host
|
|
1087
|
-
// answers with whatever of it survived, and names the rest as lost.
|
|
1088
|
-
this.#failBarrier(attached, `hold buffer overflowed at ${REPLAY_BARRIER_LIMIT} frames`);
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
held.push({ seq, frame });
|
|
1092
|
-
return;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
// Publication is the delivery boundary. The cursor records what was delivered, so
|
|
1096
|
-
// it may pass this sequence only once every configured surface has taken the
|
|
1097
|
-
// frame: moving it first turns one refused publication into permanent loss,
|
|
1098
|
-
// because the next replay would resume above a frame nobody ever published.
|
|
1099
|
-
// Rebuilds and replays retain this stream identity, so an acknowledgement that
|
|
1100
|
-
// disappears after provider acceptance is reconciled as the same publication.
|
|
1101
|
-
const publicationId =
|
|
1102
|
-
seq !== undefined && ownsSequence ? `${attached.sessionId}:${attached.generation}:${seq}` : undefined;
|
|
413
|
+
async #onAttachment(attachment: SessionAttachment): Promise<void> {
|
|
414
|
+
this.#attachments.set(attachment.sessionId, attachment);
|
|
415
|
+
const discord = this.#discord;
|
|
416
|
+
const slack = this.#slack;
|
|
417
|
+
const barrier = (async () => {
|
|
418
|
+
await this.#cleanupWork.get(attachment.sessionId)?.catch(() => undefined);
|
|
419
|
+
await discord?.recoverCleanup(attachment.sessionId, attachment.generation);
|
|
420
|
+
await slack?.recoverCleanup(attachment.sessionId, attachment.generation);
|
|
421
|
+
})();
|
|
422
|
+
this.#attachmentBarriers.set(attachment.sessionId, barrier);
|
|
1103
423
|
try {
|
|
1104
|
-
await
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
424
|
+
await barrier;
|
|
425
|
+
if (this.#attachments.get(attachment.sessionId) !== attachment) return;
|
|
426
|
+
this.#presentation?.connectSession(attachment.sessionId, {
|
|
427
|
+
sendReply: route => attachment.send({ type: "reply", id: route.actionId, answer: route.answer }),
|
|
428
|
+
});
|
|
429
|
+
} finally {
|
|
430
|
+
if (this.#attachmentBarriers.get(attachment.sessionId) === barrier)
|
|
431
|
+
this.#attachmentBarriers.delete(attachment.sessionId);
|
|
1111
432
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async #onSessionRemoved(
|
|
436
|
+
attachment: SessionAttachment,
|
|
437
|
+
reason: "removed" | "replaced" | "replaced_same_generation" = "removed",
|
|
438
|
+
): Promise<void> {
|
|
439
|
+
if (this.#attachments.get(attachment.sessionId) !== attachment) return;
|
|
440
|
+
if (reason === "replaced_same_generation") {
|
|
441
|
+
await this.#discord?.retireAttachment(attachment.sessionId, attachment.generation);
|
|
442
|
+
await this.#slack?.retireAttachment(attachment.sessionId, attachment.generation);
|
|
1117
443
|
}
|
|
444
|
+
this.#attachments.delete(attachment.sessionId);
|
|
445
|
+
if (reason === "removed") await this.#trackCleanup(attachment);
|
|
1118
446
|
}
|
|
1119
447
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
* nothing to publish — and throwing means no surface took it. Only the caller moves
|
|
1126
|
-
* the cursor, and only on the first.
|
|
1127
|
-
*/
|
|
1128
|
-
async #publishFrame(attached: AttachedSession, correlated: CorrelatedFrame, publicationId?: string): Promise<void> {
|
|
448
|
+
async #handleFrame(attachment: SessionAttachment, correlated: CorrelatedFrame): Promise<void> {
|
|
449
|
+
if (this.#attachments.get(attachment.sessionId) !== attachment) return;
|
|
450
|
+
await this.#attachmentBarriers.get(attachment.sessionId);
|
|
451
|
+
if (this.#attachments.get(attachment.sessionId) !== attachment) return;
|
|
452
|
+
const publicationId = correlated.publicationId;
|
|
1129
453
|
const normalizedFrame = correlated.body;
|
|
1130
|
-
// The SDK's own request/response traffic arrives on this same observer:
|
|
1131
|
-
// `SdkClient` settles a pending request and still forwards that frame to
|
|
1132
|
-
// every handler. A protocol answer carries no user-visible content, so it
|
|
1133
|
-
// is dropped here — ahead of presentation fanout and of every root,
|
|
1134
|
-
// mapping, resume, close, and action mutation — on both delivery paths.
|
|
1135
|
-
// Its nested payload is discarded with it and never reprojected.
|
|
1136
454
|
const bodyType = typeof normalizedFrame.type === "string" ? normalizedFrame.type : undefined;
|
|
1137
455
|
if (isControlPlaneFrameType(correlated.name) || isControlPlaneFrameType(bodyType)) return;
|
|
1138
456
|
if (normalizedFrame.type === "turn_stream" && normalizedFrame.phase === "live") return;
|
|
1139
|
-
if (correlated.sessionId !== undefined && correlated.sessionId !==
|
|
1140
|
-
const sessionId =
|
|
457
|
+
if (correlated.sessionId !== undefined && correlated.sessionId !== attachment.sessionId) return;
|
|
458
|
+
const sessionId = attachment.sessionId;
|
|
1141
459
|
const name = correlated.name;
|
|
1142
460
|
if (name === "session_closed" || name === "session_terminated") {
|
|
1143
|
-
await this
|
|
461
|
+
await this.#trackCleanup(attachment);
|
|
1144
462
|
return;
|
|
1145
463
|
}
|
|
1146
|
-
// `session_prepared` is control-plane evidence only: the session holds
|
|
1147
|
-
// endpoint authority while deliberately withholding readiness. It carries no
|
|
1148
|
-
// user-visible content and must never create or adopt a root, notify, or
|
|
1149
|
-
// resume — at this attachment's generation or any other. Foreign session ids
|
|
1150
|
-
// are already rejected above, so every prepared frame is inert here, and a
|
|
1151
|
-
// stale or foreign one cannot mutate state either.
|
|
1152
464
|
if (name === SESSION_PREPARED_EVENT || bodyType === SESSION_PREPARED_EVENT) return;
|
|
1153
465
|
if (name === "session_ready") {
|
|
1154
|
-
if (correlated.generation !==
|
|
1155
|
-
await this
|
|
466
|
+
if (correlated.generation !== attachment.generation) return;
|
|
467
|
+
await this.#resume(
|
|
468
|
+
sessionId,
|
|
469
|
+
attachment.generation,
|
|
470
|
+
attachment.authorityId,
|
|
471
|
+
"GJC session ready.",
|
|
472
|
+
publicationId,
|
|
473
|
+
);
|
|
1156
474
|
return;
|
|
1157
475
|
}
|
|
1158
476
|
const notification = this.#notificationEvent(sessionId, normalizedFrame);
|
|
@@ -1176,7 +494,8 @@ export class ChatDaemonRuntime {
|
|
|
1176
494
|
if (this.#discord)
|
|
1177
495
|
await this.#discord.notify({
|
|
1178
496
|
sessionId,
|
|
1179
|
-
endpointGeneration:
|
|
497
|
+
endpointGeneration: attachment.generation,
|
|
498
|
+
attachmentAuthorityId: attachment.authorityId,
|
|
1180
499
|
content,
|
|
1181
500
|
...(publicationId === undefined ? {} : { publicationId }),
|
|
1182
501
|
...(notification.type === "action_needed"
|
|
@@ -1188,39 +507,54 @@ export class ChatDaemonRuntime {
|
|
|
1188
507
|
sessionId,
|
|
1189
508
|
content,
|
|
1190
509
|
notification.type === "action_needed" ? notification.id : undefined,
|
|
1191
|
-
|
|
510
|
+
attachment.generation,
|
|
1192
511
|
publicationId,
|
|
1193
512
|
);
|
|
1194
513
|
}
|
|
1195
514
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
515
|
+
#trackCleanup(attachment: SessionAttachment): Promise<void> {
|
|
516
|
+
const sessionId = attachment.sessionId;
|
|
517
|
+
const previous = this.#cleanupWork.get(sessionId);
|
|
518
|
+
const work = (previous ? previous.catch(() => undefined) : Promise.resolve()).then(async () => {
|
|
519
|
+
await this.#closeProviders(attachment);
|
|
520
|
+
});
|
|
521
|
+
this.#cleanupWork.set(sessionId, work);
|
|
522
|
+
return work.finally(() => {
|
|
523
|
+
if (this.#cleanupWork.get(sessionId) === work) this.#cleanupWork.delete(sessionId);
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
async #closeProviders(attachment: SessionAttachment): Promise<void> {
|
|
527
|
+
const discord = this.#discord;
|
|
528
|
+
const slack = this.#slack;
|
|
529
|
+
await discord?.close(attachment.sessionId, attachment.generation);
|
|
530
|
+
await slack?.close(attachment.sessionId, undefined, attachment.generation);
|
|
1200
531
|
}
|
|
1201
532
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
533
|
+
async #resume(
|
|
534
|
+
sessionId: string,
|
|
535
|
+
generation: number,
|
|
536
|
+
attachmentAuthorityId: string | undefined,
|
|
537
|
+
content: string,
|
|
538
|
+
publicationId?: string,
|
|
539
|
+
): Promise<void> {
|
|
1208
540
|
if (this.#discord) {
|
|
1209
|
-
await this.#discord.resume(sessionId, generation);
|
|
541
|
+
await this.#discord.resume(sessionId, generation, attachmentAuthorityId);
|
|
1210
542
|
await this.#discord.notify({
|
|
1211
543
|
sessionId,
|
|
1212
544
|
endpointGeneration: generation,
|
|
545
|
+
attachmentAuthorityId,
|
|
1213
546
|
content,
|
|
1214
547
|
...(publicationId === undefined ? {} : { publicationId }),
|
|
1215
548
|
});
|
|
1216
549
|
}
|
|
1217
550
|
if (this.#slack) await this.#slack.resume(sessionId, content, generation, publicationId);
|
|
1218
551
|
}
|
|
552
|
+
|
|
1219
553
|
async #runChatCommand(
|
|
1220
554
|
transport: ChatTransport,
|
|
1221
555
|
sessionId: string,
|
|
1222
556
|
content: string,
|
|
1223
|
-
|
|
557
|
+
expectedAttachment: SessionAttachment,
|
|
1224
558
|
idempotencyKey: string = randomUUID(),
|
|
1225
559
|
): Promise<boolean> {
|
|
1226
560
|
const match = /^\/sdk\s+(control|query|global)\s+([^\s]+)(?:\s+(.+))?\s*$/.exec(content);
|
|
@@ -1241,12 +575,14 @@ export class ChatDaemonRuntime {
|
|
|
1241
575
|
outcome = await sendAuthorizedChatOperation(transport, { kind, operation, input }, async () => {
|
|
1242
576
|
if (kind === "global")
|
|
1243
577
|
return await this.#runGlobalCommand(operation, input as Record<string, unknown>, idempotencyKey);
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
578
|
+
if (!expectedAttachment.isCurrent()) throw new ChatDeliveryError("pre_send");
|
|
579
|
+
return await this.#router.request(
|
|
580
|
+
sessionId,
|
|
1247
581
|
kind === "control"
|
|
1248
582
|
? { type: "control_request", operation, input, confirm: true, idempotencyKey }
|
|
1249
583
|
: { type: "query_request", query: operation, input, idempotencyKey },
|
|
584
|
+
expectedAttachment.generation,
|
|
585
|
+
expectedAttachment,
|
|
1250
586
|
);
|
|
1251
587
|
});
|
|
1252
588
|
} catch (error) {
|
|
@@ -1255,50 +591,38 @@ export class ChatDaemonRuntime {
|
|
|
1255
591
|
if (!(error instanceof SdkClientError)) throw new ChatDeliveryError("ambiguous");
|
|
1256
592
|
outcome = {
|
|
1257
593
|
ok: false,
|
|
1258
|
-
error: {
|
|
1259
|
-
code: error.code,
|
|
1260
|
-
message: error.message,
|
|
1261
|
-
},
|
|
594
|
+
error: { code: error.code, message: error.message },
|
|
1262
595
|
};
|
|
1263
596
|
}
|
|
1264
597
|
await this.#postCommandOutcome(transport, sessionId, { kind, operation }, outcome);
|
|
1265
598
|
return outcome.ok;
|
|
1266
599
|
}
|
|
600
|
+
|
|
1267
601
|
async #runGlobalCommand(
|
|
1268
602
|
operation: string,
|
|
1269
603
|
input: Record<string, unknown>,
|
|
1270
604
|
idempotencyKey: string,
|
|
1271
605
|
): Promise<Record<string, unknown>> {
|
|
1272
|
-
|
|
1273
|
-
if (!discovery) throw new ChatDeliveryError("pre_send");
|
|
1274
|
-
let client: ChatDaemonSdkClient;
|
|
1275
|
-
try {
|
|
1276
|
-
client = await (
|
|
1277
|
-
this.deps.createBrokerClient ?? (async endpoint => await SdkClient.connect(endpoint.url, endpoint.token))
|
|
1278
|
-
)({ url: discovery.url, token: discovery.token });
|
|
1279
|
-
} catch {
|
|
1280
|
-
throw new ChatDeliveryError("pre_send");
|
|
1281
|
-
}
|
|
606
|
+
if (operation !== "session.list") throw new ChatDeliveryError("pre_send");
|
|
1282
607
|
try {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
);
|
|
1288
|
-
} finally {
|
|
1289
|
-
await client.close();
|
|
608
|
+
return await this.#router.listBrokerSessions(input, idempotencyKey);
|
|
609
|
+
} catch (error) {
|
|
610
|
+
if (error instanceof SessionRouterError) throw new ChatDeliveryError(error.phase);
|
|
611
|
+
throw error;
|
|
1290
612
|
}
|
|
1291
613
|
}
|
|
614
|
+
|
|
1292
615
|
async #postCommandOutcome(
|
|
1293
616
|
transport: ChatTransport,
|
|
1294
617
|
sessionId: string,
|
|
1295
|
-
request: Pick<
|
|
618
|
+
request: Pick<ChatOperationRequest, "kind" | "operation">,
|
|
1296
619
|
outcome: { ok: true; result: unknown } | { ok: false; error: { code: string; message: string } },
|
|
1297
620
|
): Promise<void> {
|
|
1298
621
|
const content = JSON.stringify(projectChatCommandOutcome(request, outcome));
|
|
1299
622
|
if (transport === "discord") await this.#discord?.postCommandResult(sessionId, content);
|
|
1300
623
|
else await this.#slack?.postCommandResult(sessionId, content);
|
|
1301
624
|
}
|
|
625
|
+
|
|
1302
626
|
#notificationEvent(sessionId: string, frame: Record<string, unknown>): NotificationEvent {
|
|
1303
627
|
if (frame.type === "action_needed" && typeof frame.id === "string" && typeof frame.kind === "string") {
|
|
1304
628
|
return {
|