@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,5 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { logger } from "@gajae-code/utils";
|
|
2
3
|
import { SdkClientError } from "../client/client";
|
|
4
|
+
import { type SessionAttachment, SessionRouterError } from "../router";
|
|
3
5
|
import type { ChatDeliveryError } from "./chat-daemon-runtime";
|
|
4
6
|
|
|
5
7
|
import {
|
|
@@ -20,10 +22,10 @@ import type { DiscordInboundEvent, DiscordMessageComponent, DiscordProvider, Dis
|
|
|
20
22
|
|
|
21
23
|
const FAILURE = "This conversation is no longer available.";
|
|
22
24
|
|
|
23
|
-
export class
|
|
24
|
-
constructor(message = "Discord session
|
|
25
|
+
export class DiscordAttachmentBindingError extends Error {
|
|
26
|
+
constructor(message = "Discord session attachment changed before outbound publication.") {
|
|
25
27
|
super(message);
|
|
26
|
-
this.name = "
|
|
28
|
+
this.name = "DiscordAttachmentBindingError";
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -71,13 +73,6 @@ function withoutClosingIntent(record: DiscordConversation, closedAt: number): Di
|
|
|
71
73
|
};
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
/** A captured live SDK transport. `isCurrent` must fail closed after replacement or removal. */
|
|
75
|
-
export interface DiscordEndpointBinding {
|
|
76
|
-
generation: number;
|
|
77
|
-
isCurrent(): boolean;
|
|
78
|
-
send(frame: Record<string, unknown>): void | Promise<void>;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
76
|
export interface DiscordLeaseRecoveryScheduler {
|
|
82
77
|
setTimeout(callback: () => void | Promise<void>, delayMs: number): unknown;
|
|
83
78
|
clearTimeout(handle: unknown): void;
|
|
@@ -85,17 +80,19 @@ export interface DiscordLeaseRecoveryScheduler {
|
|
|
85
80
|
|
|
86
81
|
export interface DiscordNotificationDaemonOptions {
|
|
87
82
|
agentDir: string;
|
|
88
|
-
repo: string;
|
|
89
83
|
guildId: string;
|
|
90
84
|
parentChannelId: string;
|
|
91
85
|
provider: DiscordProvider;
|
|
92
86
|
now?: () => number;
|
|
93
87
|
leaseRecoveryScheduler?: DiscordLeaseRecoveryScheduler;
|
|
94
|
-
|
|
88
|
+
resolveAttachment: (
|
|
89
|
+
sessionId: string,
|
|
90
|
+
expectedGeneration?: number,
|
|
91
|
+
) => SessionAttachment | null | Promise<SessionAttachment | null>;
|
|
95
92
|
onCommand?: (
|
|
96
93
|
sessionId: string,
|
|
97
94
|
content: string,
|
|
98
|
-
|
|
95
|
+
attachment: SessionAttachment,
|
|
99
96
|
idempotencyKey: string,
|
|
100
97
|
) => Promise<boolean>;
|
|
101
98
|
}
|
|
@@ -103,6 +100,7 @@ export interface DiscordNotificationDaemonOptions {
|
|
|
103
100
|
export interface DiscordNotificationInput {
|
|
104
101
|
sessionId: string;
|
|
105
102
|
endpointGeneration: number;
|
|
103
|
+
attachmentAuthorityId?: string;
|
|
106
104
|
content: string;
|
|
107
105
|
threadName?: string;
|
|
108
106
|
actionId?: string;
|
|
@@ -130,6 +128,7 @@ type DiscordInboundRouting = {
|
|
|
130
128
|
parentId: string;
|
|
131
129
|
threadId: string;
|
|
132
130
|
eventId: string;
|
|
131
|
+
attachmentAuthorityId?: string;
|
|
133
132
|
interactionId?: string;
|
|
134
133
|
kind: "command" | "action";
|
|
135
134
|
actionId?: string;
|
|
@@ -162,18 +161,23 @@ export class DiscordNotificationDaemon {
|
|
|
162
161
|
readonly #creates = new Map<string, Promise<DiscordConversation>>();
|
|
163
162
|
readonly #resumes = new Map<string, Promise<DiscordConversation | undefined>>();
|
|
164
163
|
readonly #archives = new Map<string, Promise<void>>();
|
|
165
|
-
readonly #
|
|
164
|
+
readonly #resolveAttachment: (
|
|
166
165
|
sessionId: string,
|
|
167
166
|
expectedGeneration?: number,
|
|
168
|
-
) => Promise<
|
|
167
|
+
) => SessionAttachment | null | Promise<SessionAttachment | null>;
|
|
169
168
|
readonly #effects: ChatEffectJournal;
|
|
170
169
|
readonly #activeWork = new Set<Promise<unknown>>();
|
|
170
|
+
readonly #workInvalidators = new Set<() => Promise<void>>();
|
|
171
171
|
readonly #inflightInbound = new Set<string>();
|
|
172
172
|
#started = false;
|
|
173
173
|
#lifecycleGeneration = 0;
|
|
174
|
+
#workGeneration = 0;
|
|
174
175
|
#startTask: Promise<void> | undefined;
|
|
175
176
|
#stopTask: Promise<void> | undefined;
|
|
176
177
|
#providerStarting = false;
|
|
178
|
+
#providerLifecycleTail: Promise<void> | undefined;
|
|
179
|
+
#providerLifecycleError: unknown;
|
|
180
|
+
#providerLifecycleErrorSet = false;
|
|
177
181
|
|
|
178
182
|
#leaseRecoveryTimer: unknown;
|
|
179
183
|
#leaseRecoveryAt: number | undefined;
|
|
@@ -191,11 +195,30 @@ export class DiscordNotificationDaemon {
|
|
|
191
195
|
setTimeout: (callback, delayMs) => setTimeout(() => void callback(), delayMs),
|
|
192
196
|
clearTimeout: handle => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
193
197
|
};
|
|
194
|
-
this.#
|
|
198
|
+
this.#resolveAttachment = options.resolveAttachment;
|
|
199
|
+
}
|
|
200
|
+
restartBlocked(): boolean {
|
|
201
|
+
return this.#providerLifecycleTail !== undefined || this.#providerLifecycleErrorSet;
|
|
195
202
|
}
|
|
196
203
|
|
|
197
204
|
async start(): Promise<void> {
|
|
198
205
|
if (this.#started && !this.#stopTask) return;
|
|
206
|
+
if (this.#providerLifecycleErrorSet) throw this.#providerLifecycleError;
|
|
207
|
+
if (this.#providerLifecycleTail) {
|
|
208
|
+
const tail = this.#providerLifecycleTail;
|
|
209
|
+
const settled = await Promise.race([
|
|
210
|
+
Promise.allSettled([tail]).then(() => true),
|
|
211
|
+
Bun.sleep(5_000).then(() => false),
|
|
212
|
+
]);
|
|
213
|
+
if (!settled) throw new Error("Prior Discord provider shutdown did not settle before restart.");
|
|
214
|
+
try {
|
|
215
|
+
await tail;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
this.#recordProviderLifecycleError(error);
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
if (this.#providerLifecycleTail === tail) this.#providerLifecycleTail = undefined;
|
|
221
|
+
}
|
|
199
222
|
if (this.#stopTask) {
|
|
200
223
|
await this.#stopTask;
|
|
201
224
|
return await this.start();
|
|
@@ -239,7 +262,10 @@ export class DiscordNotificationDaemon {
|
|
|
239
262
|
this.#providerStarting = true;
|
|
240
263
|
try {
|
|
241
264
|
await this.options.provider.start(
|
|
242
|
-
event =>
|
|
265
|
+
async event => {
|
|
266
|
+
if (lifecycleGeneration !== this.#lifecycleGeneration || !this.#started) return;
|
|
267
|
+
await this.#track(this.handleInbound(event));
|
|
268
|
+
},
|
|
243
269
|
() => {},
|
|
244
270
|
);
|
|
245
271
|
} finally {
|
|
@@ -247,7 +273,12 @@ export class DiscordNotificationDaemon {
|
|
|
247
273
|
}
|
|
248
274
|
if (lifecycleGeneration !== this.#lifecycleGeneration) {
|
|
249
275
|
this.#started = false;
|
|
250
|
-
|
|
276
|
+
try {
|
|
277
|
+
await this.options.provider.stop();
|
|
278
|
+
} catch (error) {
|
|
279
|
+
this.#recordProviderLifecycleError(error);
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
251
282
|
}
|
|
252
283
|
} catch (error) {
|
|
253
284
|
if (lifecycleGeneration === this.#lifecycleGeneration) {
|
|
@@ -282,36 +313,127 @@ export class DiscordNotificationDaemon {
|
|
|
282
313
|
}
|
|
283
314
|
|
|
284
315
|
async #stop(starting: Promise<void> | undefined, stopProvider: boolean): Promise<void> {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
316
|
+
const drainDeadline = this.#now() + 5_000;
|
|
317
|
+
let providerStopError: unknown;
|
|
318
|
+
let providerStopRejected = false;
|
|
319
|
+
const providerLifecycle = (async () => {
|
|
320
|
+
// Calling provider.stop() before awaiting start lets a provider cancel a
|
|
321
|
+
// Gateway open that is already in flight. #start rechecks the generation
|
|
322
|
+
// after it resolves and closes any late open.
|
|
323
|
+
if (stopProvider) {
|
|
324
|
+
try {
|
|
325
|
+
await this.options.provider.stop();
|
|
326
|
+
} catch (error) {
|
|
327
|
+
providerStopRejected = true;
|
|
328
|
+
providerStopError = error;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
await starting;
|
|
333
|
+
} catch {
|
|
334
|
+
// The caller of start() owns its recovery/start error; stop still drains it.
|
|
335
|
+
}
|
|
336
|
+
if (this.#providerStarting || this.#started) {
|
|
337
|
+
this.#started = false;
|
|
338
|
+
try {
|
|
339
|
+
await this.options.provider.stop();
|
|
340
|
+
} catch (error) {
|
|
341
|
+
providerStopRejected = true;
|
|
342
|
+
providerStopError ??= error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (providerStopRejected) throw providerStopError;
|
|
346
|
+
})();
|
|
347
|
+
const providerOutcome = await Promise.race([
|
|
348
|
+
providerLifecycle.then(
|
|
349
|
+
() => ({ kind: "settled" as const }),
|
|
350
|
+
error => ({ kind: "rejected" as const, error }),
|
|
351
|
+
),
|
|
352
|
+
Bun.sleep(Math.max(0, drainDeadline - this.#now())).then(() => ({ kind: "timeout" as const })),
|
|
353
|
+
]);
|
|
354
|
+
if (providerOutcome.kind === "rejected") {
|
|
355
|
+
this.#recordProviderLifecycleError(providerOutcome.error);
|
|
356
|
+
await this.#invalidateActiveWork();
|
|
357
|
+
await this.#drainActiveWork(drainDeadline);
|
|
358
|
+
throw providerOutcome.error;
|
|
293
359
|
}
|
|
294
|
-
if (
|
|
295
|
-
this.#
|
|
296
|
-
await this
|
|
360
|
+
if (providerOutcome.kind === "timeout") {
|
|
361
|
+
this.#retainProviderLifecycle(providerLifecycle);
|
|
362
|
+
await this.#invalidateActiveWork();
|
|
363
|
+
await this.#drainActiveWork(drainDeadline);
|
|
364
|
+
logger.warn(
|
|
365
|
+
"Discord provider lifecycle exceeded the 5000ms shutdown drain; continuing with Router revocation.",
|
|
366
|
+
);
|
|
367
|
+
return;
|
|
297
368
|
}
|
|
298
|
-
// Drain until quiescent
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
369
|
+
// Drain until quiescent, but never let a hung provider REST request prevent
|
|
370
|
+
// SessionRouter authority revocation and daemon ownership release.
|
|
371
|
+
await this.#drainActiveWork(drainDeadline);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async #invalidateActiveWork(): Promise<void> {
|
|
375
|
+
this.#workGeneration += 1;
|
|
376
|
+
await Promise.allSettled([...this.#workInvalidators].map(invalidate => invalidate()));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async #drainActiveWork(deadline: number): Promise<void> {
|
|
380
|
+
while (this.#activeWork.size > 0) {
|
|
381
|
+
const remaining = deadline - this.#now();
|
|
382
|
+
if (remaining <= 0) {
|
|
383
|
+
await this.#invalidateActiveWork();
|
|
384
|
+
logger.warn("Discord provider work exceeded the 5000ms shutdown drain; continuing with Router revocation.");
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const settled = await Promise.race([
|
|
388
|
+
Promise.allSettled([...this.#activeWork]).then(() => true),
|
|
389
|
+
Bun.sleep(remaining).then(() => false),
|
|
390
|
+
]);
|
|
391
|
+
if (!settled) {
|
|
392
|
+
await this.#invalidateActiveWork();
|
|
393
|
+
logger.warn("Discord provider work exceeded the 5000ms shutdown drain; continuing with Router revocation.");
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#recordProviderLifecycleError(error: unknown): void {
|
|
400
|
+
this.#providerLifecycleError = error;
|
|
401
|
+
this.#providerLifecycleErrorSet = true;
|
|
303
402
|
}
|
|
304
403
|
|
|
404
|
+
#retainProviderLifecycle(tail: Promise<void>): void {
|
|
405
|
+
this.#providerLifecycleTail = tail;
|
|
406
|
+
void tail.then(
|
|
407
|
+
() => {
|
|
408
|
+
if (this.#providerLifecycleTail === tail) this.#providerLifecycleTail = undefined;
|
|
409
|
+
},
|
|
410
|
+
error => {
|
|
411
|
+
this.#recordProviderLifecycleError(error);
|
|
412
|
+
if (this.#providerLifecycleTail === tail) this.#providerLifecycleTail = undefined;
|
|
413
|
+
},
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Frame-driven publication work is tracked like inbound events so that stop()
|
|
419
|
+
* drains it while the Router still holds attachment authority. A session
|
|
420
|
+
* replayed during teardown otherwise reaches its post-provider binding
|
|
421
|
+
* revalidation only after Router.stop() has revoked the attachment, turning a
|
|
422
|
+
* legitimate create into a spurious teardown rejection.
|
|
423
|
+
*/
|
|
305
424
|
async notify(input: DiscordNotificationInput): Promise<DiscordConversation> {
|
|
306
|
-
await this.#
|
|
425
|
+
return await this.#track(this.#notify(input));
|
|
426
|
+
}
|
|
427
|
+
async #notify(input: DiscordNotificationInput): Promise<DiscordConversation> {
|
|
428
|
+
await this.#requireLiveBinding(input.sessionId, input.endpointGeneration, input.attachmentAuthorityId);
|
|
307
429
|
const conversation = await this.#ensureConversation(input);
|
|
308
|
-
await this.#requireLiveBinding(input.sessionId, input.endpointGeneration);
|
|
430
|
+
await this.#requireLiveBinding(input.sessionId, input.endpointGeneration, input.attachmentAuthorityId);
|
|
309
431
|
if (conversation.state !== "active" || !conversation.threadId) throw new Error("Discord thread is unavailable");
|
|
310
432
|
const pendingActionId = input.actionId;
|
|
311
433
|
const authoritative = pendingActionId
|
|
312
434
|
? await this.#ensureActionPublication(conversation, pendingActionId)
|
|
313
435
|
: conversation;
|
|
314
|
-
await this.#requireLiveBinding(input.sessionId, input.endpointGeneration);
|
|
436
|
+
await this.#requireLiveBinding(input.sessionId, input.endpointGeneration, input.attachmentAuthorityId);
|
|
315
437
|
const components =
|
|
316
438
|
pendingActionId && authoritative.pendingActionNonce && input.options && input.options.length > 0
|
|
317
439
|
? actionComponents(
|
|
@@ -335,17 +457,64 @@ export class DiscordNotificationDaemon {
|
|
|
335
457
|
|
|
336
458
|
/** Posts a safe command outcome to the active mapped conversation. */
|
|
337
459
|
async postCommandResult(sessionId: string, content: string): Promise<boolean> {
|
|
460
|
+
return await this.#track(this.#postCommandResult(sessionId, content));
|
|
461
|
+
}
|
|
462
|
+
async #postCommandResult(sessionId: string, content: string): Promise<boolean> {
|
|
338
463
|
const record = await this.#bySession(sessionId);
|
|
339
464
|
if (record?.state !== "active" || !record.threadId) return false;
|
|
340
465
|
await this.#postEffect(`command-result:${record.threadId}:${randomUUID()}`, record, content);
|
|
341
466
|
return true;
|
|
342
467
|
}
|
|
343
468
|
|
|
344
|
-
async close(sessionId: string): Promise<void> {
|
|
345
|
-
const closing = await this.#markClosing(sessionId);
|
|
469
|
+
async close(sessionId: string, endpointGeneration?: number): Promise<void> {
|
|
470
|
+
const closing = await this.#markClosing(sessionId, endpointGeneration);
|
|
346
471
|
if (closing) await this.#driveClose(closing);
|
|
347
472
|
}
|
|
348
473
|
|
|
474
|
+
async recoverCleanup(sessionId: string, endpointGeneration: number): Promise<void> {
|
|
475
|
+
const record = await this.#bySession(sessionId);
|
|
476
|
+
if (record?.endpointGeneration !== endpointGeneration || !closingIntent(record)) return;
|
|
477
|
+
await this.#driveClose(record);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async retireAttachment(sessionId: string, endpointGeneration: number): Promise<void> {
|
|
481
|
+
const intentKey = this.#intentKey(sessionId);
|
|
482
|
+
const now = this.#now();
|
|
483
|
+
await this.#store.transact(intentKey, current => {
|
|
484
|
+
if (
|
|
485
|
+
current?.state !== "creating" ||
|
|
486
|
+
current.sessionId !== sessionId ||
|
|
487
|
+
current.endpointGeneration !== endpointGeneration
|
|
488
|
+
)
|
|
489
|
+
return current;
|
|
490
|
+
return normalizeDiscordConversation({
|
|
491
|
+
...current,
|
|
492
|
+
generation: current.generation + 1,
|
|
493
|
+
state: "closed",
|
|
494
|
+
createNonce: undefined,
|
|
495
|
+
createOwner: undefined,
|
|
496
|
+
createLeaseExpiresAt: undefined,
|
|
497
|
+
closedAt: now,
|
|
498
|
+
updatedAt: now,
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
const record = await this.#bySession(sessionId);
|
|
502
|
+
if (!record?.threadId || record.endpointGeneration !== endpointGeneration) return;
|
|
503
|
+
for (const receipt of record.inboundDispatches ?? [])
|
|
504
|
+
await this.#terminalizeInbound(record, receipt, "stale_binding");
|
|
505
|
+
const key = discordConversationKey({
|
|
506
|
+
appId: record.appId,
|
|
507
|
+
guildId: record.guildId,
|
|
508
|
+
parentChannelId: record.parentChannelId,
|
|
509
|
+
threadId: record.threadId,
|
|
510
|
+
});
|
|
511
|
+
await this.#store.transact(key, current =>
|
|
512
|
+
current?.sessionId === sessionId && current.endpointGeneration === endpointGeneration
|
|
513
|
+
? withoutClosingIntent(current, now)
|
|
514
|
+
: current,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
349
518
|
async archive(sessionId: string): Promise<void> {
|
|
350
519
|
const running = this.#archives.get(sessionId);
|
|
351
520
|
if (running) return await running;
|
|
@@ -361,19 +530,34 @@ export class DiscordNotificationDaemon {
|
|
|
361
530
|
async #archive(sessionId: string): Promise<void> {
|
|
362
531
|
const record = await this.#bySession(sessionId);
|
|
363
532
|
if (!record?.threadId || record.state !== "active") return;
|
|
364
|
-
await this.#requireLiveBinding(record.sessionId!, record.endpointGeneration
|
|
533
|
+
await this.#requireLiveBinding(record.sessionId!, record.endpointGeneration!, record.attachmentAuthorityId);
|
|
365
534
|
const archiving = await this.#beginArchive(record);
|
|
366
535
|
const occurrenceId = archiving.archiveOccurrenceId;
|
|
367
536
|
if (!occurrenceId || !archiving.archiveEffectId) throw new Error("Discord archive occurrence is unavailable");
|
|
368
537
|
await this.#threadEffect(archiving.archiveEffectId, archiving, "archive", false, false, occurrenceId);
|
|
369
|
-
await this.#requireLiveBinding(
|
|
538
|
+
await this.#requireLiveBinding(
|
|
539
|
+
archiving.sessionId!,
|
|
540
|
+
archiving.endpointGeneration!,
|
|
541
|
+
archiving.attachmentAuthorityId,
|
|
542
|
+
);
|
|
370
543
|
await this.#completeArchive(archiving, occurrenceId);
|
|
371
544
|
}
|
|
372
545
|
|
|
373
|
-
async resume(
|
|
546
|
+
async resume(
|
|
547
|
+
sessionId: string,
|
|
548
|
+
endpointGeneration: number,
|
|
549
|
+
attachmentAuthorityId?: string,
|
|
550
|
+
): Promise<DiscordConversation | undefined> {
|
|
551
|
+
return await this.#track(this.#resumeAdmission(sessionId, endpointGeneration, attachmentAuthorityId));
|
|
552
|
+
}
|
|
553
|
+
async #resumeAdmission(
|
|
554
|
+
sessionId: string,
|
|
555
|
+
endpointGeneration: number,
|
|
556
|
+
attachmentAuthorityId?: string,
|
|
557
|
+
): Promise<DiscordConversation | undefined> {
|
|
374
558
|
const running = this.#resumes.get(sessionId);
|
|
375
559
|
if (running) return await running;
|
|
376
|
-
const task = this.#resume(sessionId, endpointGeneration);
|
|
560
|
+
const task = this.#resume(sessionId, endpointGeneration, attachmentAuthorityId);
|
|
377
561
|
this.#resumes.set(sessionId, task);
|
|
378
562
|
try {
|
|
379
563
|
return await task;
|
|
@@ -382,20 +566,29 @@ export class DiscordNotificationDaemon {
|
|
|
382
566
|
}
|
|
383
567
|
}
|
|
384
568
|
|
|
385
|
-
async #resume(
|
|
569
|
+
async #resume(
|
|
570
|
+
sessionId: string,
|
|
571
|
+
endpointGeneration: number,
|
|
572
|
+
attachmentAuthorityId?: string,
|
|
573
|
+
): Promise<DiscordConversation | undefined> {
|
|
386
574
|
const record = await this.#bySession(sessionId);
|
|
387
575
|
if (!record?.threadId || record.state === "closed") return undefined;
|
|
576
|
+
if (attachmentAuthorityId !== undefined && record.attachmentAuthorityId !== attachmentAuthorityId) {
|
|
577
|
+
await this.retireAttachment(sessionId, record.endpointGeneration ?? endpointGeneration);
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
388
580
|
if (closingIntent(record)) {
|
|
389
581
|
await this.#driveClose(record);
|
|
390
582
|
return undefined;
|
|
391
583
|
}
|
|
392
|
-
await this.#requireLiveBinding(sessionId, endpointGeneration);
|
|
584
|
+
await this.#requireLiveBinding(sessionId, endpointGeneration, attachmentAuthorityId);
|
|
393
585
|
const resumeOccurrenceId = randomUUID();
|
|
394
586
|
const effectIncarnationId = backfilledEffectIncarnationId(record);
|
|
395
587
|
const resuming = await this.#replace(record, {
|
|
396
588
|
...record,
|
|
397
589
|
state: "resuming",
|
|
398
590
|
endpointGeneration,
|
|
591
|
+
attachmentAuthorityId,
|
|
399
592
|
effectIncarnationId,
|
|
400
593
|
resumeOccurrenceId,
|
|
401
594
|
resumeEffectId: `unarchive:${record.threadId}:${effectIncarnationId}:${resumeOccurrenceId}`,
|
|
@@ -407,10 +600,10 @@ export class DiscordNotificationDaemon {
|
|
|
407
600
|
const occurrenceId = resuming.resumeOccurrenceId;
|
|
408
601
|
if (!occurrenceId || !resuming.resumeEffectId) throw new Error("Discord resume occurrence is unavailable");
|
|
409
602
|
await this.#threadEffect(resuming.resumeEffectId, resuming, "unarchive", false, false, occurrenceId);
|
|
410
|
-
await this.#requireLiveBinding(sessionId, endpointGeneration);
|
|
603
|
+
await this.#requireLiveBinding(sessionId, endpointGeneration, attachmentAuthorityId);
|
|
411
604
|
return await this.#completeResume(resuming, occurrenceId);
|
|
412
605
|
} catch {
|
|
413
|
-
await this.#requireLiveBinding(sessionId, endpointGeneration);
|
|
606
|
+
await this.#requireLiveBinding(sessionId, endpointGeneration, attachmentAuthorityId);
|
|
414
607
|
const superseded = await this.#replace(resuming, {
|
|
415
608
|
...resuming,
|
|
416
609
|
state: "archived",
|
|
@@ -421,8 +614,14 @@ export class DiscordNotificationDaemon {
|
|
|
421
614
|
pendingActionEffectId: undefined,
|
|
422
615
|
});
|
|
423
616
|
|
|
424
|
-
const replacement = await this.#create(
|
|
425
|
-
|
|
617
|
+
const replacement = await this.#create(
|
|
618
|
+
sessionId,
|
|
619
|
+
endpointGeneration,
|
|
620
|
+
`resume-${randomUUID()}`,
|
|
621
|
+
undefined,
|
|
622
|
+
attachmentAuthorityId,
|
|
623
|
+
);
|
|
624
|
+
await this.#requireLiveBinding(sessionId, endpointGeneration, attachmentAuthorityId);
|
|
426
625
|
await this.#replace(superseded, { ...superseded, supersededByThreadId: replacement.threadId });
|
|
427
626
|
return replacement;
|
|
428
627
|
}
|
|
@@ -451,15 +650,15 @@ export class DiscordNotificationDaemon {
|
|
|
451
650
|
this.#inflightInbound.add(claim.receipt.effectId);
|
|
452
651
|
try {
|
|
453
652
|
// Interaction callbacks have a short provider deadline. The mapping and
|
|
454
|
-
// journal claim are sufficient to acknowledge;
|
|
455
|
-
const
|
|
653
|
+
// journal claim are sufficient to acknowledge; attachment discovery waits.
|
|
654
|
+
const attachment = event.interaction
|
|
456
655
|
? null
|
|
457
|
-
: await this.#
|
|
458
|
-
if (!event.interaction && !this.#matches(record,
|
|
656
|
+
: await this.#resolveAttachment(record.sessionId, record.endpointGeneration);
|
|
657
|
+
if (!event.interaction && !this.#matches(record, attachment)) {
|
|
459
658
|
await this.#fail(event.threadId);
|
|
460
659
|
return;
|
|
461
660
|
}
|
|
462
|
-
await this.#dispatchInbound(record,
|
|
661
|
+
await this.#dispatchInbound(record, attachment, claim.receipt, event.interaction, claim.liveCallbackEffect);
|
|
463
662
|
} finally {
|
|
464
663
|
this.#inflightInbound.delete(claim.receipt.effectId);
|
|
465
664
|
await this.#scheduleLeaseRecovery();
|
|
@@ -495,6 +694,7 @@ export class DiscordNotificationDaemon {
|
|
|
495
694
|
parentId: event.parentId,
|
|
496
695
|
threadId: event.threadId,
|
|
497
696
|
eventId: event.id,
|
|
697
|
+
...(record.attachmentAuthorityId === undefined ? {} : { attachmentAuthorityId: record.attachmentAuthorityId }),
|
|
498
698
|
...(event.interaction ? { interactionId: event.interaction.id } : {}),
|
|
499
699
|
kind: command ? "command" : "action",
|
|
500
700
|
...(!command ? { actionId: route!.actionId, actionNonce: route!.actionNonce } : {}),
|
|
@@ -570,6 +770,9 @@ export class DiscordNotificationDaemon {
|
|
|
570
770
|
eventId: event.id,
|
|
571
771
|
kind: "command",
|
|
572
772
|
endpointGeneration: record.endpointGeneration!,
|
|
773
|
+
...(record.attachmentAuthorityId === undefined
|
|
774
|
+
? {}
|
|
775
|
+
: { attachmentAuthorityId: record.attachmentAuthorityId }),
|
|
573
776
|
effectId,
|
|
574
777
|
idempotencyKey,
|
|
575
778
|
}
|
|
@@ -581,6 +784,9 @@ export class DiscordNotificationDaemon {
|
|
|
581
784
|
actionId: route!.actionId,
|
|
582
785
|
actionNonce: route!.actionNonce,
|
|
583
786
|
endpointGeneration: record.endpointGeneration!,
|
|
787
|
+
...(record.attachmentAuthorityId === undefined
|
|
788
|
+
? {}
|
|
789
|
+
: { attachmentAuthorityId: record.attachmentAuthorityId }),
|
|
584
790
|
effectId,
|
|
585
791
|
idempotencyKey,
|
|
586
792
|
};
|
|
@@ -609,7 +815,7 @@ export class DiscordNotificationDaemon {
|
|
|
609
815
|
}
|
|
610
816
|
async #dispatchInbound(
|
|
611
817
|
record: DiscordConversation,
|
|
612
|
-
|
|
818
|
+
initialAttachment: SessionAttachment | null,
|
|
613
819
|
receipt: DiscordInboundDispatchReceipt,
|
|
614
820
|
interaction?: DiscordInboundEvent["interaction"],
|
|
615
821
|
liveCallbackEffect?: ChatEffect<DiscordInboundEffectPayload>,
|
|
@@ -711,8 +917,9 @@ export class DiscordNotificationDaemon {
|
|
|
711
917
|
return;
|
|
712
918
|
}
|
|
713
919
|
record = dispatchable;
|
|
714
|
-
const
|
|
715
|
-
|
|
920
|
+
const attachment =
|
|
921
|
+
initialAttachment ?? (await this.#resolveAttachment(record.sessionId!, receipt.endpointGeneration));
|
|
922
|
+
if (!this.#matches(record, attachment) || receipt.endpointGeneration !== attachment.generation) {
|
|
716
923
|
await this.#rescheduleAfterEffectTransition(
|
|
717
924
|
this.#effects.record(effect.id, lease, "accepted", {
|
|
718
925
|
status: this.#inboundAcceptedStatus(effect, "pre_send_binding_changed"),
|
|
@@ -750,7 +957,7 @@ export class DiscordNotificationDaemon {
|
|
|
750
957
|
);
|
|
751
958
|
try {
|
|
752
959
|
if (!(await renew())) return;
|
|
753
|
-
if (!this.#matches(record,
|
|
960
|
+
if (!this.#matches(record, attachment) || receipt.endpointGeneration !== attachment.generation) {
|
|
754
961
|
await this.#rescheduleAfterEffectTransition(
|
|
755
962
|
this.#effects.record(effect.id, lease, "accepted", {
|
|
756
963
|
status: this.#inboundAcceptedStatus(effect, "pre_send_binding_changed"),
|
|
@@ -767,7 +974,7 @@ export class DiscordNotificationDaemon {
|
|
|
767
974
|
return;
|
|
768
975
|
}
|
|
769
976
|
record = beforeSend;
|
|
770
|
-
if (!this.#matches(record,
|
|
977
|
+
if (!this.#matches(record, attachment) || receipt.endpointGeneration !== attachment.generation) {
|
|
771
978
|
await this.#rescheduleAfterEffectTransition(
|
|
772
979
|
this.#effects.record(effect.id, lease, "accepted", {
|
|
773
980
|
status: this.#inboundAcceptedStatus(effect, "pre_send_binding_changed"),
|
|
@@ -780,10 +987,10 @@ export class DiscordNotificationDaemon {
|
|
|
780
987
|
await this.options.onCommand?.(
|
|
781
988
|
record.sessionId!,
|
|
782
989
|
effect.payload.content,
|
|
783
|
-
|
|
990
|
+
attachment,
|
|
784
991
|
effect.payload.idempotencyKey,
|
|
785
992
|
);
|
|
786
|
-
else await
|
|
993
|
+
else await attachment.send(effect.payload);
|
|
787
994
|
if (!leaseLost && (await renew()) && (await this.#effects.record(effect.id, lease, "terminal")))
|
|
788
995
|
await this.#finishInbound(record, receipt);
|
|
789
996
|
} catch (error) {
|
|
@@ -824,8 +1031,8 @@ export class DiscordNotificationDaemon {
|
|
|
824
1031
|
const dispatched = new Set<string>();
|
|
825
1032
|
for (const record of Object.values((await this.#store.load()).conversations)) {
|
|
826
1033
|
if (!record.threadId || !record.sessionId || record.state !== "active") continue;
|
|
827
|
-
let
|
|
828
|
-
let
|
|
1034
|
+
let attachment: SessionAttachment | null = null;
|
|
1035
|
+
let attachmentResolved = false;
|
|
829
1036
|
for (const [index, receipt] of (record.inboundDispatches ?? []).entries()) {
|
|
830
1037
|
// Never race a foreground handleInbound() dispatch: while an effect is
|
|
831
1038
|
// in-flight it briefly unowns itself (record "accepted"/"deferred" before
|
|
@@ -864,16 +1071,16 @@ export class DiscordNotificationDaemon {
|
|
|
864
1071
|
dispatched.add(receipt.effectId);
|
|
865
1072
|
continue;
|
|
866
1073
|
}
|
|
867
|
-
if (!
|
|
868
|
-
|
|
869
|
-
|
|
1074
|
+
if (!attachmentResolved) {
|
|
1075
|
+
attachment = await this.#resolveAttachment(record.sessionId, receipt.endpointGeneration);
|
|
1076
|
+
attachmentResolved = true;
|
|
870
1077
|
}
|
|
871
|
-
if (!this.#matches(record,
|
|
1078
|
+
if (!this.#matches(record, attachment) || attachment.generation !== receipt.endpointGeneration) {
|
|
872
1079
|
await this.#terminalizeInbound(record, receipt, "stale_binding");
|
|
873
1080
|
continue;
|
|
874
1081
|
}
|
|
875
1082
|
dispatched.add(receipt.effectId);
|
|
876
|
-
await this.#dispatchInbound(record,
|
|
1083
|
+
await this.#dispatchInbound(record, attachment, receipt);
|
|
877
1084
|
}
|
|
878
1085
|
}
|
|
879
1086
|
|
|
@@ -901,12 +1108,12 @@ export class DiscordNotificationDaemon {
|
|
|
901
1108
|
routing,
|
|
902
1109
|
);
|
|
903
1110
|
if (!adopted) continue;
|
|
904
|
-
const
|
|
905
|
-
if (!this.#matches(adopted.record,
|
|
1111
|
+
const attachment = await this.#resolveAttachment(adopted.record.sessionId!, effect.endpointGeneration);
|
|
1112
|
+
if (!this.#matches(adopted.record, attachment) || attachment.generation !== effect.endpointGeneration) {
|
|
906
1113
|
await this.#terminalizeInbound(adopted.record, adopted.receipt, "stale_binding");
|
|
907
1114
|
continue;
|
|
908
1115
|
}
|
|
909
|
-
await this.#dispatchInbound(adopted.record,
|
|
1116
|
+
await this.#dispatchInbound(adopted.record, attachment, adopted.receipt);
|
|
910
1117
|
}
|
|
911
1118
|
return recoveryFailed;
|
|
912
1119
|
}
|
|
@@ -1003,6 +1210,7 @@ export class DiscordNotificationDaemon {
|
|
|
1003
1210
|
left.actionId === right.actionId &&
|
|
1004
1211
|
left.actionNonce === right.actionNonce &&
|
|
1005
1212
|
left.endpointGeneration === right.endpointGeneration &&
|
|
1213
|
+
left.attachmentAuthorityId === right.attachmentAuthorityId &&
|
|
1006
1214
|
left.effectId === right.effectId &&
|
|
1007
1215
|
left.idempotencyKey === right.idempotencyKey
|
|
1008
1216
|
);
|
|
@@ -1083,6 +1291,7 @@ export class DiscordNotificationDaemon {
|
|
|
1083
1291
|
current?.state !== "active" ||
|
|
1084
1292
|
current.sessionId !== record.sessionId ||
|
|
1085
1293
|
current.endpointGeneration !== receipt.endpointGeneration ||
|
|
1294
|
+
current.attachmentAuthorityId !== receipt.attachmentAuthorityId ||
|
|
1086
1295
|
!claimed
|
|
1087
1296
|
)
|
|
1088
1297
|
return undefined;
|
|
@@ -1106,6 +1315,9 @@ export class DiscordNotificationDaemon {
|
|
|
1106
1315
|
eventId: routing.eventId,
|
|
1107
1316
|
kind: "command",
|
|
1108
1317
|
endpointGeneration,
|
|
1318
|
+
...(routing.attachmentAuthorityId === undefined
|
|
1319
|
+
? {}
|
|
1320
|
+
: { attachmentAuthorityId: routing.attachmentAuthorityId }),
|
|
1109
1321
|
effectId,
|
|
1110
1322
|
idempotencyKey,
|
|
1111
1323
|
}
|
|
@@ -1117,6 +1329,9 @@ export class DiscordNotificationDaemon {
|
|
|
1117
1329
|
actionId: routing.actionId!,
|
|
1118
1330
|
actionNonce: routing.actionNonce!,
|
|
1119
1331
|
endpointGeneration,
|
|
1332
|
+
...(routing.attachmentAuthorityId === undefined
|
|
1333
|
+
? {}
|
|
1334
|
+
: { attachmentAuthorityId: routing.attachmentAuthorityId }),
|
|
1120
1335
|
effectId,
|
|
1121
1336
|
idempotencyKey,
|
|
1122
1337
|
};
|
|
@@ -1176,23 +1391,42 @@ export class DiscordNotificationDaemon {
|
|
|
1176
1391
|
}
|
|
1177
1392
|
}
|
|
1178
1393
|
|
|
1179
|
-
#matches(record: DiscordConversation,
|
|
1180
|
-
if (!
|
|
1181
|
-
return
|
|
1394
|
+
#matches(record: DiscordConversation, attachment: SessionAttachment | null): attachment is SessionAttachment {
|
|
1395
|
+
if (!attachment?.isCurrent()) return false;
|
|
1396
|
+
return (
|
|
1397
|
+
record.state === "active" &&
|
|
1398
|
+
record.endpointGeneration === attachment.generation &&
|
|
1399
|
+
record.attachmentAuthorityId === attachment.authorityId
|
|
1400
|
+
);
|
|
1182
1401
|
}
|
|
1183
|
-
async #bindingCurrent(
|
|
1402
|
+
async #bindingCurrent(
|
|
1403
|
+
sessionId: string,
|
|
1404
|
+
endpointGeneration: number,
|
|
1405
|
+
attachmentAuthorityId?: string,
|
|
1406
|
+
): Promise<boolean> {
|
|
1184
1407
|
try {
|
|
1185
|
-
const
|
|
1186
|
-
return
|
|
1408
|
+
const attachment = await this.#resolveAttachment(sessionId, endpointGeneration);
|
|
1409
|
+
return (
|
|
1410
|
+
!!attachment &&
|
|
1411
|
+
attachment.isCurrent() &&
|
|
1412
|
+
attachment.generation === endpointGeneration &&
|
|
1413
|
+
attachment.authorityId === attachmentAuthorityId
|
|
1414
|
+
);
|
|
1187
1415
|
} catch {
|
|
1188
1416
|
return false;
|
|
1189
1417
|
}
|
|
1190
1418
|
}
|
|
1191
|
-
async #requireLiveBinding(
|
|
1192
|
-
|
|
1419
|
+
async #requireLiveBinding(
|
|
1420
|
+
sessionId: string,
|
|
1421
|
+
endpointGeneration: number,
|
|
1422
|
+
attachmentAuthorityId?: string,
|
|
1423
|
+
): Promise<void> {
|
|
1424
|
+
if (!(await this.#bindingCurrent(sessionId, endpointGeneration, attachmentAuthorityId)))
|
|
1425
|
+
throw new DiscordAttachmentBindingError();
|
|
1193
1426
|
}
|
|
1194
1427
|
#isDefiniteSdkPreSendFailure(error: unknown): boolean {
|
|
1195
|
-
if (error instanceof
|
|
1428
|
+
if (error instanceof DiscordAttachmentBindingError) return true;
|
|
1429
|
+
if (error instanceof SessionRouterError) return error.phase === "pre_send";
|
|
1196
1430
|
if (error instanceof SdkClientError) return error.code === "connection_closed";
|
|
1197
1431
|
return (
|
|
1198
1432
|
error instanceof Error &&
|
|
@@ -1215,9 +1449,26 @@ export class DiscordNotificationDaemon {
|
|
|
1215
1449
|
throw new Error("Discord thread is closing");
|
|
1216
1450
|
}
|
|
1217
1451
|
if (existing?.state === "active" && existing.threadId) {
|
|
1218
|
-
if (
|
|
1219
|
-
|
|
1220
|
-
|
|
1452
|
+
if (
|
|
1453
|
+
existing.endpointGeneration === input.endpointGeneration &&
|
|
1454
|
+
(input.attachmentAuthorityId === undefined ||
|
|
1455
|
+
existing.attachmentAuthorityId === input.attachmentAuthorityId)
|
|
1456
|
+
)
|
|
1457
|
+
return existing;
|
|
1458
|
+
if (
|
|
1459
|
+
existing.endpointGeneration === input.endpointGeneration &&
|
|
1460
|
+
input.attachmentAuthorityId !== undefined &&
|
|
1461
|
+
existing.attachmentAuthorityId !== input.attachmentAuthorityId
|
|
1462
|
+
) {
|
|
1463
|
+
await this.retireAttachment(input.sessionId, input.endpointGeneration);
|
|
1464
|
+
return await this.#ensureConversation(input);
|
|
1465
|
+
}
|
|
1466
|
+
await this.#requireLiveBinding(input.sessionId, input.endpointGeneration, input.attachmentAuthorityId);
|
|
1467
|
+
return await this.#replace(existing, {
|
|
1468
|
+
...existing,
|
|
1469
|
+
endpointGeneration: input.endpointGeneration,
|
|
1470
|
+
attachmentAuthorityId: input.attachmentAuthorityId,
|
|
1471
|
+
});
|
|
1221
1472
|
}
|
|
1222
1473
|
const inFlight = this.#creates.get(input.sessionId);
|
|
1223
1474
|
if (inFlight) {
|
|
@@ -1225,14 +1476,28 @@ export class DiscordNotificationDaemon {
|
|
|
1225
1476
|
try {
|
|
1226
1477
|
created = await inFlight;
|
|
1227
1478
|
} catch {
|
|
1228
|
-
await this.#requireLiveBinding(input.sessionId, input.endpointGeneration);
|
|
1479
|
+
await this.#requireLiveBinding(input.sessionId, input.endpointGeneration, input.attachmentAuthorityId);
|
|
1229
1480
|
return await this.#ensureConversation(input);
|
|
1230
1481
|
}
|
|
1231
|
-
if (
|
|
1232
|
-
|
|
1233
|
-
|
|
1482
|
+
if (
|
|
1483
|
+
created.endpointGeneration === input.endpointGeneration &&
|
|
1484
|
+
(input.attachmentAuthorityId === undefined || created.attachmentAuthorityId === input.attachmentAuthorityId)
|
|
1485
|
+
)
|
|
1486
|
+
return created;
|
|
1487
|
+
await this.#requireLiveBinding(input.sessionId, input.endpointGeneration, input.attachmentAuthorityId);
|
|
1488
|
+
return await this.#replace(created, {
|
|
1489
|
+
...created,
|
|
1490
|
+
endpointGeneration: input.endpointGeneration,
|
|
1491
|
+
attachmentAuthorityId: input.attachmentAuthorityId,
|
|
1492
|
+
});
|
|
1234
1493
|
}
|
|
1235
|
-
const pending = this.#create(
|
|
1494
|
+
const pending = this.#create(
|
|
1495
|
+
input.sessionId,
|
|
1496
|
+
input.endpointGeneration,
|
|
1497
|
+
randomUUID(),
|
|
1498
|
+
input.threadName,
|
|
1499
|
+
input.attachmentAuthorityId,
|
|
1500
|
+
);
|
|
1236
1501
|
this.#creates.set(input.sessionId, pending);
|
|
1237
1502
|
try {
|
|
1238
1503
|
return await pending;
|
|
@@ -1246,6 +1511,7 @@ export class DiscordNotificationDaemon {
|
|
|
1246
1511
|
endpointGeneration: number,
|
|
1247
1512
|
nonce: string,
|
|
1248
1513
|
name = "GJC session",
|
|
1514
|
+
attachmentAuthorityId?: string,
|
|
1249
1515
|
): Promise<DiscordConversation> {
|
|
1250
1516
|
const intentKey = this.#intentKey(sessionId);
|
|
1251
1517
|
const owner = randomUUID();
|
|
@@ -1257,12 +1523,24 @@ export class DiscordNotificationDaemon {
|
|
|
1257
1523
|
throw new Error("Discord thread is closing");
|
|
1258
1524
|
}
|
|
1259
1525
|
if (active?.state === "active" && active.threadId) {
|
|
1260
|
-
if (
|
|
1261
|
-
|
|
1262
|
-
|
|
1526
|
+
if (
|
|
1527
|
+
active.endpointGeneration === endpointGeneration &&
|
|
1528
|
+
(attachmentAuthorityId === undefined || active.attachmentAuthorityId === attachmentAuthorityId)
|
|
1529
|
+
)
|
|
1530
|
+
return active;
|
|
1531
|
+
if (
|
|
1532
|
+
active.endpointGeneration === endpointGeneration &&
|
|
1533
|
+
attachmentAuthorityId !== undefined &&
|
|
1534
|
+
active.attachmentAuthorityId !== attachmentAuthorityId
|
|
1535
|
+
) {
|
|
1536
|
+
await this.retireAttachment(sessionId, endpointGeneration);
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
await this.#requireLiveBinding(sessionId, endpointGeneration, attachmentAuthorityId);
|
|
1540
|
+
return await this.#replace(active, { ...active, endpointGeneration, attachmentAuthorityId });
|
|
1263
1541
|
}
|
|
1264
1542
|
const now = this.#now();
|
|
1265
|
-
await this.#requireLiveBinding(sessionId, endpointGeneration);
|
|
1543
|
+
await this.#requireLiveBinding(sessionId, endpointGeneration, attachmentAuthorityId);
|
|
1266
1544
|
intent = await this.#store.transact(intentKey, old => {
|
|
1267
1545
|
if (old?.state === "creating" && old.createOwner && (old.createLeaseExpiresAt ?? 0) > now) return old;
|
|
1268
1546
|
return {
|
|
@@ -1273,6 +1551,7 @@ export class DiscordNotificationDaemon {
|
|
|
1273
1551
|
parentChannelId: this.options.parentChannelId,
|
|
1274
1552
|
sessionId,
|
|
1275
1553
|
endpointGeneration,
|
|
1554
|
+
attachmentAuthorityId,
|
|
1276
1555
|
createNonce: old?.createNonce ?? nonce,
|
|
1277
1556
|
createOwner: owner,
|
|
1278
1557
|
createLeaseExpiresAt: now + 60_000,
|
|
@@ -1288,13 +1567,13 @@ export class DiscordNotificationDaemon {
|
|
|
1288
1567
|
const active = await this.#bySession(sessionId);
|
|
1289
1568
|
if (active?.state === "active" && active.threadId) {
|
|
1290
1569
|
if (active.endpointGeneration === endpointGeneration) return active;
|
|
1291
|
-
await this.#requireLiveBinding(sessionId, endpointGeneration);
|
|
1570
|
+
await this.#requireLiveBinding(sessionId, endpointGeneration, active.attachmentAuthorityId);
|
|
1292
1571
|
return await this.#replace(active, { ...active, endpointGeneration });
|
|
1293
1572
|
}
|
|
1294
1573
|
let thread: DiscordThread | null;
|
|
1295
1574
|
try {
|
|
1296
1575
|
thread = await this.#withCreateIntentLease(intent, () => this.#createThreadEffect(intent, name));
|
|
1297
|
-
await this.#requireLiveBinding(sessionId, endpointGeneration);
|
|
1576
|
+
await this.#requireLiveBinding(sessionId, endpointGeneration, intent.attachmentAuthorityId);
|
|
1298
1577
|
} catch (error) {
|
|
1299
1578
|
await this.#abandonCreator(intentKey, intent);
|
|
1300
1579
|
throw error;
|
|
@@ -1304,19 +1583,31 @@ export class DiscordNotificationDaemon {
|
|
|
1304
1583
|
currentIntent?.state !== "creating" ||
|
|
1305
1584
|
currentIntent.createOwner !== intent.createOwner ||
|
|
1306
1585
|
currentIntent.generation !== intent.generation ||
|
|
1586
|
+
currentIntent.attachmentAuthorityId !== intent.attachmentAuthorityId ||
|
|
1307
1587
|
(currentIntent.createLeaseExpiresAt ?? 0) <= this.#now()
|
|
1308
1588
|
) {
|
|
1309
1589
|
throw new Error("Discord create intent lost its fence before mapping commit");
|
|
1310
1590
|
}
|
|
1311
|
-
await this.#requireLiveBinding(sessionId, endpointGeneration);
|
|
1591
|
+
await this.#requireLiveBinding(sessionId, endpointGeneration, intent.attachmentAuthorityId);
|
|
1312
1592
|
const key = discordConversationKey({
|
|
1313
1593
|
appId: intent.appId,
|
|
1314
1594
|
guildId: intent.guildId,
|
|
1315
1595
|
parentChannelId: intent.parentChannelId,
|
|
1316
1596
|
threadId: thread.id,
|
|
1317
1597
|
});
|
|
1318
|
-
|
|
1319
|
-
|
|
1598
|
+
let committed = false;
|
|
1599
|
+
const record = await this.#store.transactWithSnapshot(key, (old, conversations) => {
|
|
1600
|
+
const current = conversations[intentKey];
|
|
1601
|
+
if (
|
|
1602
|
+
current?.state !== "creating" ||
|
|
1603
|
+
current.createOwner !== intent.createOwner ||
|
|
1604
|
+
current.generation !== intent.generation ||
|
|
1605
|
+
current.attachmentAuthorityId !== intent.attachmentAuthorityId ||
|
|
1606
|
+
(current.createLeaseExpiresAt ?? 0) <= this.#now()
|
|
1607
|
+
)
|
|
1608
|
+
return old;
|
|
1609
|
+
committed = true;
|
|
1610
|
+
return normalizeDiscordConversation({
|
|
1320
1611
|
generation: (old?.generation ?? 0) + 1,
|
|
1321
1612
|
state: "active",
|
|
1322
1613
|
appId: intent.appId,
|
|
@@ -1325,15 +1616,18 @@ export class DiscordNotificationDaemon {
|
|
|
1325
1616
|
threadId: thread.id,
|
|
1326
1617
|
sessionId,
|
|
1327
1618
|
endpointGeneration,
|
|
1619
|
+
attachmentAuthorityId: intent.attachmentAuthorityId,
|
|
1328
1620
|
createNonce: intent.createNonce,
|
|
1329
1621
|
effectIncarnationId: old?.effectIncarnationId ?? intent.effectIncarnationId ?? randomUUID(),
|
|
1330
1622
|
updatedAt: this.#now(),
|
|
1331
|
-
seenEventIds: old?.seenEventIds ?? [],
|
|
1332
|
-
seenInteractionIds:
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1623
|
+
seenEventIds: old?.attachmentAuthorityId === intent.attachmentAuthorityId ? (old?.seenEventIds ?? []) : [],
|
|
1624
|
+
seenInteractionIds:
|
|
1625
|
+
old?.attachmentAuthorityId === intent.attachmentAuthorityId ? (old?.seenInteractionIds ?? []) : [],
|
|
1626
|
+
inboundDispatches:
|
|
1627
|
+
old?.attachmentAuthorityId === intent.attachmentAuthorityId ? old?.inboundDispatches : undefined,
|
|
1628
|
+
});
|
|
1629
|
+
});
|
|
1630
|
+
if (!committed || !record) throw new Error("Discord create intent lost its fence before mapping commit");
|
|
1337
1631
|
await this.#store.delete(intentKey, intent.generation);
|
|
1338
1632
|
return record;
|
|
1339
1633
|
}
|
|
@@ -1373,9 +1667,10 @@ export class DiscordNotificationDaemon {
|
|
|
1373
1667
|
if (!record.threadId || !intent) throw new Error("Discord close intent is unavailable.");
|
|
1374
1668
|
return `close-archive:${record.threadId}:${intent.nonce}`;
|
|
1375
1669
|
}
|
|
1376
|
-
async #markClosing(sessionId: string): Promise<DiscordConversation | undefined> {
|
|
1670
|
+
async #markClosing(sessionId: string, endpointGeneration?: number): Promise<DiscordConversation | undefined> {
|
|
1377
1671
|
const record = await this.#bySession(sessionId);
|
|
1378
1672
|
if (!record?.threadId || record.state === "closed") return undefined;
|
|
1673
|
+
if (endpointGeneration !== undefined && record.endpointGeneration !== endpointGeneration) return undefined;
|
|
1379
1674
|
const key = discordConversationKey({
|
|
1380
1675
|
appId: record.appId,
|
|
1381
1676
|
guildId: record.guildId,
|
|
@@ -1384,7 +1679,13 @@ export class DiscordNotificationDaemon {
|
|
|
1384
1679
|
});
|
|
1385
1680
|
let closing: DiscordConversation | undefined;
|
|
1386
1681
|
await this.#store.transact(key, current => {
|
|
1387
|
-
if (
|
|
1682
|
+
if (
|
|
1683
|
+
!current ||
|
|
1684
|
+
current.sessionId !== sessionId ||
|
|
1685
|
+
!current.threadId ||
|
|
1686
|
+
current.state === "closed" ||
|
|
1687
|
+
(endpointGeneration !== undefined && current.endpointGeneration !== endpointGeneration)
|
|
1688
|
+
)
|
|
1388
1689
|
return current;
|
|
1389
1690
|
if (closingIntent(current)) {
|
|
1390
1691
|
closing = current;
|
|
@@ -1574,8 +1875,22 @@ export class DiscordNotificationDaemon {
|
|
|
1574
1875
|
let lost = false;
|
|
1575
1876
|
let renewal: Promise<boolean> | undefined;
|
|
1576
1877
|
let expectedGeneration = intent.generation;
|
|
1878
|
+
const workGeneration = this.#workGeneration;
|
|
1879
|
+
const abandonCurrent = async (): Promise<void> => {
|
|
1880
|
+
await this.#abandonCreator(this.#intentKey(intent.sessionId!), intent);
|
|
1881
|
+
};
|
|
1882
|
+
const invalidate = async (): Promise<void> => {
|
|
1883
|
+
lost = true;
|
|
1884
|
+
const activeRenewal = renewal;
|
|
1885
|
+
if (activeRenewal) await activeRenewal.catch(() => false);
|
|
1886
|
+
await abandonCurrent();
|
|
1887
|
+
};
|
|
1888
|
+
this.#workInvalidators.add(invalidate);
|
|
1577
1889
|
const renew = async (): Promise<boolean> => {
|
|
1578
|
-
if (lost)
|
|
1890
|
+
if (lost || workGeneration !== this.#workGeneration) {
|
|
1891
|
+
await invalidate();
|
|
1892
|
+
return false;
|
|
1893
|
+
}
|
|
1579
1894
|
if (renewal) return await renewal;
|
|
1580
1895
|
const currentRenewal = (async (): Promise<boolean> => {
|
|
1581
1896
|
const now = this.#now();
|
|
@@ -1594,6 +1909,15 @@ export class DiscordNotificationDaemon {
|
|
|
1594
1909
|
updatedAt: now,
|
|
1595
1910
|
};
|
|
1596
1911
|
});
|
|
1912
|
+
if (workGeneration !== this.#workGeneration) {
|
|
1913
|
+
if (current?.state === "creating" && current.createOwner === intent.createOwner) {
|
|
1914
|
+
intent.generation = current.generation;
|
|
1915
|
+
intent.createLeaseExpiresAt = current.createLeaseExpiresAt;
|
|
1916
|
+
}
|
|
1917
|
+
lost = true;
|
|
1918
|
+
await abandonCurrent();
|
|
1919
|
+
return false;
|
|
1920
|
+
}
|
|
1597
1921
|
if (
|
|
1598
1922
|
current?.state !== "creating" ||
|
|
1599
1923
|
current.createOwner !== intent.createOwner ||
|
|
@@ -1628,6 +1952,7 @@ export class DiscordNotificationDaemon {
|
|
|
1628
1952
|
return result;
|
|
1629
1953
|
} finally {
|
|
1630
1954
|
clearInterval(timer);
|
|
1955
|
+
this.#workInvalidators.delete(invalidate);
|
|
1631
1956
|
}
|
|
1632
1957
|
}
|
|
1633
1958
|
async #clearPending(record: DiscordConversation, actionId: string, actionNonce: string): Promise<void> {
|
|
@@ -1658,7 +1983,7 @@ export class DiscordNotificationDaemon {
|
|
|
1658
1983
|
parentChannelId: record.parentChannelId,
|
|
1659
1984
|
threadId: record.threadId!,
|
|
1660
1985
|
});
|
|
1661
|
-
await this.#requireLiveBinding(record.sessionId!, record.endpointGeneration
|
|
1986
|
+
await this.#requireLiveBinding(record.sessionId!, record.endpointGeneration!, record.attachmentAuthorityId);
|
|
1662
1987
|
const result = await this.#store.transact(key, current => {
|
|
1663
1988
|
if (
|
|
1664
1989
|
current?.state !== "active" ||
|
|
@@ -1704,6 +2029,7 @@ export class DiscordNotificationDaemon {
|
|
|
1704
2029
|
revalidate: () => boolean | Promise<boolean>,
|
|
1705
2030
|
terminalizeStaleBeforeProvider = false,
|
|
1706
2031
|
): Promise<ChatEffectReceipt> {
|
|
2032
|
+
const workGeneration = this.#workGeneration;
|
|
1707
2033
|
const claimed = await this.#rescheduleAfterEffectTransition(
|
|
1708
2034
|
this.#effects.enqueueAndClaim<TPayload>(
|
|
1709
2035
|
{ id, kind, transport: "discord", sessionId, endpointGeneration, payload },
|
|
@@ -1723,7 +2049,7 @@ export class DiscordNotificationDaemon {
|
|
|
1723
2049
|
if (initial?.state === "terminal") {
|
|
1724
2050
|
if (!initial.receipt) throw new Error(`Discord effect ${id} has no receipt`);
|
|
1725
2051
|
if (terminalizeStaleBeforeProvider && initial.receipt.status === "stale_noop")
|
|
1726
|
-
throw new
|
|
2052
|
+
throw new DiscordAttachmentBindingError("Discord thread effect is no longer current.");
|
|
1727
2053
|
return initial.receipt;
|
|
1728
2054
|
}
|
|
1729
2055
|
const reclaimed = await this.#rescheduleAfterEffectTransition(
|
|
@@ -1733,22 +2059,38 @@ export class DiscordNotificationDaemon {
|
|
|
1733
2059
|
effect = reclaimed;
|
|
1734
2060
|
}
|
|
1735
2061
|
const lease: ChatEffectLease = { owner: this.#providerOwner, epoch: effect.epoch };
|
|
2062
|
+
if (workGeneration !== this.#workGeneration) {
|
|
2063
|
+
await this.#rescheduleAfterEffectTransition(
|
|
2064
|
+
this.#effects.record(id, lease, "uncertain", { status: "shutdown_timeout" }),
|
|
2065
|
+
);
|
|
2066
|
+
throw new Error(`Discord effect ${id} was admitted after shutdown drain expiry`);
|
|
2067
|
+
}
|
|
1736
2068
|
let renewalLost = false;
|
|
1737
2069
|
let revalidationFailed = false;
|
|
1738
2070
|
let providerEffectStarted = false;
|
|
1739
2071
|
let renewal: Promise<boolean> | undefined;
|
|
1740
|
-
const
|
|
1741
|
-
|
|
2072
|
+
const invalidate = async (): Promise<void> => {
|
|
2073
|
+
renewalLost = true;
|
|
2074
|
+
await this.#rescheduleAfterEffectTransition(
|
|
2075
|
+
this.#effects.record(id, lease, "uncertain", { status: "shutdown_timeout" }),
|
|
2076
|
+
);
|
|
2077
|
+
};
|
|
2078
|
+
this.#workInvalidators.add(invalidate);
|
|
2079
|
+
const renewLease = async (): Promise<boolean> => {
|
|
2080
|
+
if (renewalLost || workGeneration !== this.#workGeneration) {
|
|
2081
|
+
renewalLost = true;
|
|
2082
|
+
return false;
|
|
2083
|
+
}
|
|
1742
2084
|
if (renewal) return await renewal;
|
|
1743
2085
|
const currentRenewal = (async (): Promise<boolean> => {
|
|
1744
2086
|
const renewed = await this.#rescheduleAfterEffectTransition(
|
|
1745
2087
|
this.#effects.renew(id, lease, this.#providerLeaseMs),
|
|
1746
2088
|
);
|
|
1747
|
-
if (
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
renewalLost = true;
|
|
2089
|
+
if (workGeneration !== this.#workGeneration) {
|
|
2090
|
+
await invalidate();
|
|
2091
|
+
return false;
|
|
1751
2092
|
}
|
|
2093
|
+
if (!renewed) renewalLost = true;
|
|
1752
2094
|
return !renewalLost;
|
|
1753
2095
|
})();
|
|
1754
2096
|
renewal = currentRenewal;
|
|
@@ -1760,19 +2102,24 @@ export class DiscordNotificationDaemon {
|
|
|
1760
2102
|
};
|
|
1761
2103
|
const timer = setInterval(
|
|
1762
2104
|
() => {
|
|
1763
|
-
void
|
|
2105
|
+
void renewLease().catch(() => {});
|
|
1764
2106
|
},
|
|
1765
2107
|
Math.max(1, Math.floor(this.#providerLeaseMs / 3)),
|
|
1766
2108
|
);
|
|
1767
2109
|
const ensure = async (): Promise<void> => {
|
|
1768
|
-
if (!(await
|
|
2110
|
+
if (!(await renewLease())) throw new Error(`Discord effect ${id} lost its fence`);
|
|
2111
|
+
if (!(await revalidate())) {
|
|
2112
|
+
revalidationFailed = true;
|
|
2113
|
+
renewalLost = true;
|
|
2114
|
+
throw new Error(`Discord effect ${id} lost its fence`);
|
|
2115
|
+
}
|
|
1769
2116
|
};
|
|
1770
2117
|
try {
|
|
1771
2118
|
await ensure();
|
|
1772
2119
|
const receipt = await operation(ensure, () => {
|
|
1773
2120
|
providerEffectStarted = true;
|
|
1774
2121
|
});
|
|
1775
|
-
await
|
|
2122
|
+
if (!(await renewLease())) throw new Error(`Discord effect ${id} lost its fence`);
|
|
1776
2123
|
const committed = await this.#effects.record(id, lease, "terminal", receipt);
|
|
1777
2124
|
if (!committed) throw new Error(`Discord effect ${id} lost its fence before commit`);
|
|
1778
2125
|
return receipt;
|
|
@@ -1788,6 +2135,7 @@ export class DiscordNotificationDaemon {
|
|
|
1788
2135
|
throw error;
|
|
1789
2136
|
} finally {
|
|
1790
2137
|
clearInterval(timer);
|
|
2138
|
+
this.#workInvalidators.delete(invalidate);
|
|
1791
2139
|
}
|
|
1792
2140
|
}
|
|
1793
2141
|
|
|
@@ -1806,13 +2154,17 @@ export class DiscordNotificationDaemon {
|
|
|
1806
2154
|
content: string;
|
|
1807
2155
|
nonce: string;
|
|
1808
2156
|
components?: DiscordMessageComponent[];
|
|
2157
|
+
attachmentAuthorityId?: string;
|
|
1809
2158
|
}>(id);
|
|
1810
2159
|
const payload = existingEffect?.payload ?? {
|
|
1811
2160
|
threadId: record.threadId!,
|
|
1812
2161
|
content,
|
|
1813
2162
|
nonce,
|
|
2163
|
+
...(record.attachmentAuthorityId === undefined ? {} : { attachmentAuthorityId: record.attachmentAuthorityId }),
|
|
1814
2164
|
...(components ? { components } : {}),
|
|
1815
2165
|
};
|
|
2166
|
+
if (payload.attachmentAuthorityId !== record.attachmentAuthorityId)
|
|
2167
|
+
throw new DiscordAttachmentBindingError("Discord provider effect belongs to another attachment.");
|
|
1816
2168
|
await this.#runEffect(
|
|
1817
2169
|
id,
|
|
1818
2170
|
"post-message",
|
|
@@ -1853,10 +2205,17 @@ export class DiscordNotificationDaemon {
|
|
|
1853
2205
|
(allowInactive || current.state === "active") &&
|
|
1854
2206
|
current.generation === record.generation &&
|
|
1855
2207
|
current.endpointGeneration === record.endpointGeneration &&
|
|
2208
|
+
current.attachmentAuthorityId === payload.attachmentAuthorityId &&
|
|
1856
2209
|
(!actionPublication || current.pendingActionEffectId === id);
|
|
1857
2210
|
return (
|
|
1858
2211
|
mappingCurrent &&
|
|
1859
|
-
(closing ||
|
|
2212
|
+
(closing ||
|
|
2213
|
+
allowInactive ||
|
|
2214
|
+
(await this.#bindingCurrent(
|
|
2215
|
+
record.sessionId!,
|
|
2216
|
+
record.endpointGeneration!,
|
|
2217
|
+
record.attachmentAuthorityId,
|
|
2218
|
+
)))
|
|
1860
2219
|
);
|
|
1861
2220
|
},
|
|
1862
2221
|
);
|
|
@@ -1869,12 +2228,22 @@ export class DiscordNotificationDaemon {
|
|
|
1869
2228
|
closing = false,
|
|
1870
2229
|
occurrenceId?: string,
|
|
1871
2230
|
): Promise<void> {
|
|
2231
|
+
const existingEffect = await this.#effects.read<{ attachmentAuthorityId?: string }>(id);
|
|
2232
|
+
if (existingEffect && existingEffect.payload.attachmentAuthorityId !== record.attachmentAuthorityId)
|
|
2233
|
+
throw new DiscordAttachmentBindingError("Discord thread effect belongs to another attachment.");
|
|
1872
2234
|
await this.#runEffect(
|
|
1873
2235
|
id,
|
|
1874
2236
|
operation,
|
|
1875
2237
|
record.sessionId,
|
|
1876
2238
|
record.endpointGeneration!,
|
|
1877
|
-
{
|
|
2239
|
+
{
|
|
2240
|
+
threadId: record.threadId!,
|
|
2241
|
+
locked,
|
|
2242
|
+
...(record.attachmentAuthorityId === undefined
|
|
2243
|
+
? {}
|
|
2244
|
+
: { attachmentAuthorityId: record.attachmentAuthorityId }),
|
|
2245
|
+
...(occurrenceId === undefined ? {} : { occurrenceId }),
|
|
2246
|
+
},
|
|
1878
2247
|
|
|
1879
2248
|
async (ensure, beforeProvider) => {
|
|
1880
2249
|
await ensure();
|
|
@@ -1899,6 +2268,7 @@ export class DiscordNotificationDaemon {
|
|
|
1899
2268
|
current.state === (operation === "archive" ? "active" : "resuming") &&
|
|
1900
2269
|
current.generation === record.generation &&
|
|
1901
2270
|
current.endpointGeneration === record.endpointGeneration &&
|
|
2271
|
+
current.attachmentAuthorityId === record.attachmentAuthorityId &&
|
|
1902
2272
|
(occurrenceId === undefined ||
|
|
1903
2273
|
(operation === "archive"
|
|
1904
2274
|
? current.archiveEffectId === id && current.archiveOccurrenceId === occurrenceId
|
|
@@ -1906,7 +2276,12 @@ export class DiscordNotificationDaemon {
|
|
|
1906
2276
|
return (
|
|
1907
2277
|
mappingCurrent &&
|
|
1908
2278
|
(closing ||
|
|
1909
|
-
(!!record.sessionId &&
|
|
2279
|
+
(!!record.sessionId &&
|
|
2280
|
+
(await this.#bindingCurrent(
|
|
2281
|
+
record.sessionId,
|
|
2282
|
+
record.endpointGeneration!,
|
|
2283
|
+
record.attachmentAuthorityId,
|
|
2284
|
+
))))
|
|
1910
2285
|
);
|
|
1911
2286
|
},
|
|
1912
2287
|
!closing,
|
|
@@ -1920,7 +2295,15 @@ export class DiscordNotificationDaemon {
|
|
|
1920
2295
|
"create-thread",
|
|
1921
2296
|
intent.sessionId,
|
|
1922
2297
|
intent.endpointGeneration!,
|
|
1923
|
-
{
|
|
2298
|
+
{
|
|
2299
|
+
guildId: intent.guildId,
|
|
2300
|
+
parentId: intent.parentChannelId,
|
|
2301
|
+
name,
|
|
2302
|
+
nonce,
|
|
2303
|
+
...(intent.attachmentAuthorityId === undefined
|
|
2304
|
+
? {}
|
|
2305
|
+
: { attachmentAuthorityId: intent.attachmentAuthorityId }),
|
|
2306
|
+
},
|
|
1924
2307
|
async ensure => {
|
|
1925
2308
|
await ensure();
|
|
1926
2309
|
const existing = await this.options.provider.findThreadByNonce({
|
|
@@ -1951,7 +2334,8 @@ export class DiscordNotificationDaemon {
|
|
|
1951
2334
|
current.createOwner === intent.createOwner &&
|
|
1952
2335
|
current.generation === intent.generation &&
|
|
1953
2336
|
(current.createLeaseExpiresAt ?? 0) > this.#now() &&
|
|
1954
|
-
|
|
2337
|
+
current.attachmentAuthorityId === intent.attachmentAuthorityId &&
|
|
2338
|
+
(await this.#bindingCurrent(intent.sessionId!, intent.endpointGeneration!, intent.attachmentAuthorityId))
|
|
1955
2339
|
);
|
|
1956
2340
|
},
|
|
1957
2341
|
);
|
|
@@ -1960,7 +2344,13 @@ export class DiscordNotificationDaemon {
|
|
|
1960
2344
|
}
|
|
1961
2345
|
async #recoverCreateThread(
|
|
1962
2346
|
effect: ChatEffect,
|
|
1963
|
-
payload: {
|
|
2347
|
+
payload: {
|
|
2348
|
+
guildId?: string;
|
|
2349
|
+
parentId?: string;
|
|
2350
|
+
name?: string;
|
|
2351
|
+
nonce?: string;
|
|
2352
|
+
attachmentAuthorityId?: string;
|
|
2353
|
+
},
|
|
1964
2354
|
): Promise<void> {
|
|
1965
2355
|
if (!effect.sessionId || !payload.nonce) return;
|
|
1966
2356
|
const intentKey = this.#intentKey(effect.sessionId);
|
|
@@ -1970,18 +2360,25 @@ export class DiscordNotificationDaemon {
|
|
|
1970
2360
|
intent.sessionId === effect.sessionId &&
|
|
1971
2361
|
intent.guildId === payload.guildId &&
|
|
1972
2362
|
intent.parentChannelId === payload.parentId &&
|
|
2363
|
+
intent.attachmentAuthorityId === payload.attachmentAuthorityId &&
|
|
1973
2364
|
discordEffectNonce(`create:${intent.sessionId}:${intent.createNonce}`) === payload.nonce;
|
|
1974
2365
|
if (!matchesIntent || !intent) {
|
|
1975
2366
|
if (effect.state !== "terminal") await this.#terminalizeEffect(effect.id, "rejected");
|
|
1976
2367
|
|
|
1977
2368
|
return;
|
|
1978
2369
|
}
|
|
1979
|
-
if (!(await this.#bindingCurrent(effect.sessionId, effect.endpointGeneration))) {
|
|
2370
|
+
if (!(await this.#bindingCurrent(effect.sessionId, effect.endpointGeneration, payload.attachmentAuthorityId))) {
|
|
1980
2371
|
await this.#store.delete(intentKey, intent.generation);
|
|
1981
2372
|
return;
|
|
1982
2373
|
}
|
|
1983
2374
|
if (effect.state !== "terminal") {
|
|
1984
|
-
await this.#create(
|
|
2375
|
+
await this.#create(
|
|
2376
|
+
effect.sessionId,
|
|
2377
|
+
effect.endpointGeneration,
|
|
2378
|
+
intent.createNonce!,
|
|
2379
|
+
payload.name,
|
|
2380
|
+
intent.attachmentAuthorityId,
|
|
2381
|
+
);
|
|
1985
2382
|
return;
|
|
1986
2383
|
}
|
|
1987
2384
|
const threadId = effect.receipt?.threadId;
|
|
@@ -1994,7 +2391,7 @@ export class DiscordNotificationDaemon {
|
|
|
1994
2391
|
await this.#store.delete(intentKey, intent.generation);
|
|
1995
2392
|
return;
|
|
1996
2393
|
}
|
|
1997
|
-
await this.#requireLiveBinding(effect.sessionId, effect.endpointGeneration);
|
|
2394
|
+
await this.#requireLiveBinding(effect.sessionId, effect.endpointGeneration, payload.attachmentAuthorityId);
|
|
1998
2395
|
const key = discordConversationKey({
|
|
1999
2396
|
appId: intent.appId,
|
|
2000
2397
|
guildId: intent.guildId,
|
|
@@ -2014,6 +2411,7 @@ export class DiscordNotificationDaemon {
|
|
|
2014
2411
|
threadId,
|
|
2015
2412
|
sessionId: intent.sessionId,
|
|
2016
2413
|
endpointGeneration: intent.endpointGeneration,
|
|
2414
|
+
attachmentAuthorityId: intent.attachmentAuthorityId,
|
|
2017
2415
|
createNonce: intent.createNonce,
|
|
2018
2416
|
effectIncarnationId: intent.createNonce,
|
|
2019
2417
|
updatedAt: this.#now(),
|