@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,6 +1,8 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { logger } from "@gajae-code/utils";
|
|
2
3
|
import { SdkClientError } from "../client/client";
|
|
3
|
-
import {
|
|
4
|
+
import { type SessionAttachment, SessionRouterError } from "../router";
|
|
5
|
+
|
|
4
6
|
import type { ChatDeliveryError } from "./chat-daemon-runtime";
|
|
5
7
|
import { ConversationStore } from "./conversation-store";
|
|
6
8
|
import {
|
|
@@ -56,19 +58,35 @@ function slackPublicationClientMsgId(publicationId: string): string {
|
|
|
56
58
|
|
|
57
59
|
// Durable filesystem publication leases must outlast one event-loop and persistence turn.
|
|
58
60
|
const MIN_PUBLICATION_LEASE_MS = 100;
|
|
61
|
+
const RETIREMENT_DRAIN_MS = 5_000;
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
type SlackProviderWorkScope = {
|
|
64
|
+
sessionId: string;
|
|
65
|
+
endpointGeneration: number;
|
|
66
|
+
attachmentAuthorityId?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type SlackProviderWork = {
|
|
70
|
+
scopeKey: string;
|
|
71
|
+
invalidate(): Promise<void>;
|
|
72
|
+
abort(): void;
|
|
73
|
+
terminalize(): Promise<void>;
|
|
74
|
+
settled: Promise<void>;
|
|
75
|
+
};
|
|
63
76
|
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
type SlackRetiredProviderWorkScope = {
|
|
78
|
+
retirements: number;
|
|
79
|
+
terminalizations: Set<Promise<void>>;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function slackProviderWorkScopeKey(scope: SlackProviderWorkScope): string {
|
|
83
|
+
return JSON.stringify([scope.sessionId, scope.endpointGeneration, scope.attachmentAuthorityId]);
|
|
66
84
|
}
|
|
67
85
|
|
|
68
|
-
/** Proven right to bind one session at one endpoint generation. */
|
|
69
86
|
export interface SlackBindingAuthority {
|
|
70
87
|
sessionId: string;
|
|
71
88
|
endpointGeneration: number;
|
|
89
|
+
attachmentAuthorityId?: string;
|
|
72
90
|
}
|
|
73
91
|
|
|
74
92
|
export interface SlackNotificationDaemonOptions {
|
|
@@ -87,18 +105,13 @@ export interface SlackNotificationDaemonOptions {
|
|
|
87
105
|
publicationOwnerId?: string;
|
|
88
106
|
publicationLeaseMs?: number;
|
|
89
107
|
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Exact discovery/attachment authority for adopting an existing root. The
|
|
93
|
-
* runtime supplies index, endpoint, and attachment proof; without it only the
|
|
94
|
-
* resolvable endpoint generation can be proven.
|
|
95
|
-
*/
|
|
108
|
+
resolveAttachment: (sessionId: string) => Promise<SessionAttachment | null>;
|
|
109
|
+
/** Exact Router proof for adopting an existing root. */
|
|
96
110
|
resolveBindingAuthority?: (sessionId: string) => Promise<SlackBindingAuthority | undefined>;
|
|
97
|
-
createClient: (endpoint: SlackEndpoint) => SlackSdkClient;
|
|
98
111
|
onCommand?: (
|
|
99
112
|
sessionId: string,
|
|
100
113
|
content: string,
|
|
101
|
-
|
|
114
|
+
attachment: SessionAttachment,
|
|
102
115
|
idempotencyKey: string,
|
|
103
116
|
) => Promise<boolean>;
|
|
104
117
|
}
|
|
@@ -126,6 +139,7 @@ type SlackInboundRouting = {
|
|
|
126
139
|
teamId: string;
|
|
127
140
|
channelId: string;
|
|
128
141
|
rootTs: string;
|
|
142
|
+
attachmentAuthorityId?: string;
|
|
129
143
|
actorId: string;
|
|
130
144
|
eventId: string;
|
|
131
145
|
interactionId: string;
|
|
@@ -188,14 +202,18 @@ export class SlackNotificationDaemon {
|
|
|
188
202
|
readonly store: ConversationStore<SlackConversation>;
|
|
189
203
|
readonly #now: () => number;
|
|
190
204
|
readonly #randomId: () => string;
|
|
191
|
-
readonly #
|
|
205
|
+
readonly #resolveAttachment: (sessionId: string) => Promise<SessionAttachment | null>;
|
|
206
|
+
|
|
192
207
|
readonly #publicationOwnerId: string;
|
|
193
208
|
readonly #publicationLeaseMs: number;
|
|
194
209
|
readonly #journal: ChatEffectJournal;
|
|
195
210
|
|
|
196
211
|
readonly #inflightInbound = new Set<string>();
|
|
197
212
|
readonly #activeWork = new Set<Promise<unknown>>();
|
|
213
|
+
readonly #workInvalidators = new Set<SlackProviderWork>();
|
|
214
|
+
readonly #retiredProviderWorkScopes = new Map<string, SlackRetiredProviderWorkScope>();
|
|
198
215
|
readonly #rollovers = new Map<string, Promise<SlackRootPublication>>();
|
|
216
|
+
#workGeneration = 0;
|
|
199
217
|
#lifecycleTail: Promise<void> = Promise.resolve();
|
|
200
218
|
#lifecycleGeneration = 0;
|
|
201
219
|
#startedGeneration: number | undefined;
|
|
@@ -203,9 +221,12 @@ export class SlackNotificationDaemon {
|
|
|
203
221
|
#startOperationGeneration: number | undefined;
|
|
204
222
|
#stopOperation: Promise<void> | undefined;
|
|
205
223
|
#started = false;
|
|
206
|
-
#
|
|
207
|
-
#
|
|
208
|
-
#
|
|
224
|
+
#providerStartingGeneration: number | undefined;
|
|
225
|
+
#providerRunningGeneration: number | undefined;
|
|
226
|
+
readonly #providerStopRequestedGenerations = new Set<number>();
|
|
227
|
+
#providerLifecycleTail: Promise<void> | undefined;
|
|
228
|
+
#providerLifecycleError: unknown;
|
|
229
|
+
#providerLifecycleErrorSet = false;
|
|
209
230
|
#leaseRecoveryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
210
231
|
#leaseRecoveryAt: number | undefined;
|
|
211
232
|
#leaseRecoveryFailures = 0;
|
|
@@ -221,15 +242,30 @@ export class SlackNotificationDaemon {
|
|
|
221
242
|
this.#publicationOwnerId = options.publicationOwnerId ?? randomUUID();
|
|
222
243
|
this.#publicationLeaseMs = Math.max(options.publicationLeaseMs ?? 30_000, MIN_PUBLICATION_LEASE_MS);
|
|
223
244
|
this.#journal = new ChatEffectJournal({ agentDir: options.agentDir, transport: "slack", now: this.#now });
|
|
224
|
-
this.#
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
});
|
|
245
|
+
this.#resolveAttachment = options.resolveAttachment;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
restartBlocked(): boolean {
|
|
249
|
+
return this.#providerLifecycleTail !== undefined || this.#providerLifecycleErrorSet;
|
|
230
250
|
}
|
|
231
251
|
|
|
232
252
|
async start(): Promise<void> {
|
|
253
|
+
if (this.#providerLifecycleErrorSet) throw this.#providerLifecycleError;
|
|
254
|
+
if (this.#providerLifecycleTail) {
|
|
255
|
+
const tail = this.#providerLifecycleTail;
|
|
256
|
+
const settled = await Promise.race([
|
|
257
|
+
Promise.allSettled([tail]).then(() => true),
|
|
258
|
+
Bun.sleep(5_000).then(() => false),
|
|
259
|
+
]);
|
|
260
|
+
if (!settled) throw new Error("Prior Slack provider shutdown did not settle before restart.");
|
|
261
|
+
try {
|
|
262
|
+
await tail;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
this.#recordProviderLifecycleError(error);
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
if (this.#providerLifecycleTail === tail) this.#providerLifecycleTail = undefined;
|
|
268
|
+
}
|
|
233
269
|
const lifecycleGeneration = this.#lifecycleGeneration;
|
|
234
270
|
if (this.#started && this.#startedGeneration === lifecycleGeneration) return;
|
|
235
271
|
if (this.#startOperation && this.#startOperationGeneration === lifecycleGeneration)
|
|
@@ -253,26 +289,38 @@ export class SlackNotificationDaemon {
|
|
|
253
289
|
!this.#started ||
|
|
254
290
|
this.#startedGeneration !== lifecycleGeneration
|
|
255
291
|
) {
|
|
256
|
-
this.#
|
|
257
|
-
|
|
258
|
-
|
|
292
|
+
if (this.#startedGeneration === lifecycleGeneration) {
|
|
293
|
+
this.#started = false;
|
|
294
|
+
this.#startedGeneration = undefined;
|
|
295
|
+
this.#clearLeaseRecoveryTimer();
|
|
296
|
+
}
|
|
259
297
|
return;
|
|
260
298
|
}
|
|
261
|
-
this.#
|
|
299
|
+
this.#providerStartingGeneration = lifecycleGeneration;
|
|
262
300
|
try {
|
|
263
301
|
await this.options.provider.start(async envelope => {
|
|
302
|
+
if (
|
|
303
|
+
!this.#started ||
|
|
304
|
+
this.#startedGeneration !== lifecycleGeneration ||
|
|
305
|
+
lifecycleGeneration !== this.#lifecycleGeneration
|
|
306
|
+
)
|
|
307
|
+
return;
|
|
264
308
|
await this.#track(this.handleEnvelope(envelope));
|
|
265
309
|
});
|
|
266
|
-
this.#
|
|
310
|
+
if (this.#startedGeneration === lifecycleGeneration && this.#lifecycleGeneration === lifecycleGeneration)
|
|
311
|
+
this.#providerRunningGeneration = lifecycleGeneration;
|
|
267
312
|
} finally {
|
|
268
|
-
this.#
|
|
313
|
+
if (this.#providerStartingGeneration === lifecycleGeneration)
|
|
314
|
+
this.#providerStartingGeneration = undefined;
|
|
269
315
|
}
|
|
270
316
|
if (lifecycleGeneration !== this.#lifecycleGeneration || this.#startedGeneration !== lifecycleGeneration) {
|
|
271
|
-
this.#
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
317
|
+
if (this.#startedGeneration === lifecycleGeneration) {
|
|
318
|
+
this.#started = false;
|
|
319
|
+
this.#startedGeneration = undefined;
|
|
320
|
+
this.#clearLeaseRecoveryTimer();
|
|
321
|
+
}
|
|
322
|
+
if (this.#providerRunningGeneration === lifecycleGeneration) this.#providerRunningGeneration = undefined;
|
|
323
|
+
if (!this.#providerStopRequestedGenerations.has(lifecycleGeneration)) await this.options.provider.stop();
|
|
276
324
|
}
|
|
277
325
|
} catch (error) {
|
|
278
326
|
if (this.#startedGeneration === lifecycleGeneration) {
|
|
@@ -298,23 +346,90 @@ export class SlackNotificationDaemon {
|
|
|
298
346
|
async stop(): Promise<void> {
|
|
299
347
|
if (this.#stopOperation) return await this.#stopOperation;
|
|
300
348
|
const lifecycleGeneration = this.#lifecycleGeneration++;
|
|
301
|
-
const stopProvider =
|
|
349
|
+
const stopProvider =
|
|
350
|
+
this.#providerStartingGeneration === lifecycleGeneration ||
|
|
351
|
+
this.#providerRunningGeneration === lifecycleGeneration;
|
|
302
352
|
this.#started = false;
|
|
303
353
|
this.#startedGeneration = undefined;
|
|
304
|
-
this.#
|
|
354
|
+
if (this.#providerRunningGeneration === lifecycleGeneration) this.#providerRunningGeneration = undefined;
|
|
305
355
|
this.#clearLeaseRecoveryTimer();
|
|
306
|
-
if (stopProvider) this.#
|
|
307
|
-
const providerStop = stopProvider ? this.options.provider.stop() : undefined;
|
|
308
|
-
const
|
|
356
|
+
if (stopProvider) this.#providerStopRequestedGenerations.add(lifecycleGeneration);
|
|
357
|
+
const providerStop = stopProvider ? Promise.resolve().then(() => this.options.provider.stop()) : undefined;
|
|
358
|
+
const providerStopResult = providerStop?.then(
|
|
359
|
+
() => ({ kind: "stopped" as const }),
|
|
360
|
+
error => ({ kind: "rejected" as const, error }),
|
|
361
|
+
);
|
|
362
|
+
let providerStopError: unknown;
|
|
363
|
+
let providerStopRejected = false;
|
|
364
|
+
const shutdownDeadline = this.#now() + 5_000;
|
|
365
|
+
const predecessor = this.#lifecycleTail;
|
|
366
|
+
const operation = (async () => {
|
|
367
|
+
const predecessorRemaining = shutdownDeadline - this.#now();
|
|
368
|
+
const predecessorSettled =
|
|
369
|
+
predecessorRemaining > 0 &&
|
|
370
|
+
(await Promise.race([
|
|
371
|
+
predecessor.then(
|
|
372
|
+
() => true,
|
|
373
|
+
() => true,
|
|
374
|
+
),
|
|
375
|
+
Bun.sleep(predecessorRemaining).then(() => false),
|
|
376
|
+
]));
|
|
377
|
+
if (!predecessorSettled) {
|
|
378
|
+
await this.#invalidateActiveWork();
|
|
379
|
+
logger.warn(
|
|
380
|
+
"Slack lifecycle predecessor exceeded the shutdown deadline; continuing with Router revocation.",
|
|
381
|
+
);
|
|
382
|
+
}
|
|
309
383
|
// Calling provider.stop() before joining the lifecycle queue lets a provider
|
|
310
|
-
// cancel an open that resolves only after its socket is stopped.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
384
|
+
// cancel an open that resolves only after its socket is stopped. The same
|
|
385
|
+
// operation deadline covers provider teardown and admitted work draining.
|
|
386
|
+
if (providerStop && providerStopResult) {
|
|
387
|
+
const remaining = shutdownDeadline - this.#now();
|
|
388
|
+
const outcome = await Promise.race([
|
|
389
|
+
providerStopResult,
|
|
390
|
+
Bun.sleep(Math.max(0, remaining)).then(() => ({ kind: "timeout" as const })),
|
|
391
|
+
]);
|
|
392
|
+
if (outcome.kind === "rejected") {
|
|
393
|
+
providerStopRejected = true;
|
|
394
|
+
providerStopError = outcome.error;
|
|
395
|
+
} else if (outcome.kind === "timeout") {
|
|
396
|
+
this.#retainProviderLifecycle(providerStop);
|
|
397
|
+
await this.#invalidateActiveWork();
|
|
398
|
+
logger.warn("Slack provider stop exceeded the shutdown deadline; continuing with Router revocation.");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Drain until quiescent, but never let hung provider work prevent
|
|
402
|
+
// SessionRouter authority revocation and daemon ownership release.
|
|
403
|
+
while (this.#activeWork.size > 0) {
|
|
404
|
+
const remaining = shutdownDeadline - this.#now();
|
|
405
|
+
if (remaining <= 0) {
|
|
406
|
+
await this.#invalidateActiveWork();
|
|
407
|
+
logger.warn(
|
|
408
|
+
"Slack provider work exceeded the 5000ms shutdown drain; continuing with Router revocation.",
|
|
409
|
+
);
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
const settled = await Promise.race([
|
|
413
|
+
Promise.allSettled([...this.#activeWork]).then(() => true),
|
|
414
|
+
Bun.sleep(remaining).then(() => false),
|
|
415
|
+
]);
|
|
416
|
+
if (!settled) {
|
|
417
|
+
await this.#invalidateActiveWork();
|
|
418
|
+
logger.warn(
|
|
419
|
+
"Slack provider work exceeded the 5000ms shutdown drain; continuing with Router revocation.",
|
|
420
|
+
);
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (providerStopRejected) {
|
|
425
|
+
this.#recordProviderLifecycleError(providerStopError);
|
|
426
|
+
throw providerStopError;
|
|
427
|
+
}
|
|
428
|
+
})();
|
|
429
|
+
this.#lifecycleTail = operation.then(
|
|
430
|
+
() => undefined,
|
|
431
|
+
() => undefined,
|
|
432
|
+
);
|
|
318
433
|
this.#stopOperation = operation;
|
|
319
434
|
try {
|
|
320
435
|
await operation;
|
|
@@ -367,6 +482,102 @@ export class SlackNotificationDaemon {
|
|
|
367
482
|
return work.finally(() => this.#activeWork.delete(work));
|
|
368
483
|
}
|
|
369
484
|
|
|
485
|
+
async #invalidateActiveWork(): Promise<void> {
|
|
486
|
+
this.#workGeneration += 1;
|
|
487
|
+
await Promise.allSettled([...this.#workInvalidators].map(work => work.invalidate()));
|
|
488
|
+
}
|
|
489
|
+
#fenceRetiredProviderWorkScope(scope: SlackProviderWorkScope): string {
|
|
490
|
+
const scopeKey = slackProviderWorkScopeKey(scope);
|
|
491
|
+
const retired = this.#retiredProviderWorkScopes.get(scopeKey);
|
|
492
|
+
if (retired) retired.retirements++;
|
|
493
|
+
else this.#retiredProviderWorkScopes.set(scopeKey, { retirements: 1, terminalizations: new Set() });
|
|
494
|
+
return scopeKey;
|
|
495
|
+
}
|
|
496
|
+
#tryReleaseRetiredProviderWorkScope(scopeKey: string): void {
|
|
497
|
+
const retired = this.#retiredProviderWorkScopes.get(scopeKey);
|
|
498
|
+
if (
|
|
499
|
+
retired?.retirements !== 0 ||
|
|
500
|
+
retired.terminalizations.size > 0 ||
|
|
501
|
+
[...this.#workInvalidators].some(work => work.scopeKey === scopeKey)
|
|
502
|
+
)
|
|
503
|
+
return;
|
|
504
|
+
this.#retiredProviderWorkScopes.delete(scopeKey);
|
|
505
|
+
}
|
|
506
|
+
#trackRetirementTerminalization(scopeKey: string, terminalization: Promise<void>): void {
|
|
507
|
+
const retired = this.#retiredProviderWorkScopes.get(scopeKey);
|
|
508
|
+
if (!retired) return;
|
|
509
|
+
retired.terminalizations.add(terminalization);
|
|
510
|
+
void terminalization.then(() => {
|
|
511
|
+
const current = this.#retiredProviderWorkScopes.get(scopeKey);
|
|
512
|
+
if (!current) return;
|
|
513
|
+
current.terminalizations.delete(terminalization);
|
|
514
|
+
this.#tryReleaseRetiredProviderWorkScope(scopeKey);
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
#retirementTerminalization(terminalizations: readonly Promise<unknown>[]): Promise<void> {
|
|
518
|
+
return Promise.all(
|
|
519
|
+
terminalizations.map(terminalization =>
|
|
520
|
+
terminalization.then(
|
|
521
|
+
() => undefined,
|
|
522
|
+
error => {
|
|
523
|
+
logger.warn(`Slack retired attachment terminalization failed: ${String(error)}`);
|
|
524
|
+
},
|
|
525
|
+
),
|
|
526
|
+
),
|
|
527
|
+
).then(() => undefined);
|
|
528
|
+
}
|
|
529
|
+
async #invalidateAndDrainProviderWork(scope: SlackProviderWorkScope, deadline: number): Promise<void> {
|
|
530
|
+
const scopeKey = this.#fenceRetiredProviderWorkScope(scope);
|
|
531
|
+
for (;;) {
|
|
532
|
+
const active = [...this.#workInvalidators].filter(work => work.scopeKey === scopeKey);
|
|
533
|
+
if (active.length === 0) return;
|
|
534
|
+
for (const work of active) work.abort();
|
|
535
|
+
const remaining = deadline - this.#now();
|
|
536
|
+
if (remaining <= 0) {
|
|
537
|
+
const terminalization = this.#retirementTerminalization(active.map(work => work.terminalize()));
|
|
538
|
+
this.#trackRetirementTerminalization(scopeKey, terminalization);
|
|
539
|
+
logger.warn("Slack attachment provider work exceeded the 5000ms retirement drain; fenced pending effects.");
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const drained = await Promise.race([
|
|
543
|
+
Promise.all(active.map(work => work.settled)).then(() => true),
|
|
544
|
+
Bun.sleep(remaining).then(() => false),
|
|
545
|
+
]);
|
|
546
|
+
if (drained) continue;
|
|
547
|
+
const lingering = [...this.#workInvalidators].filter(work => work.scopeKey === scopeKey);
|
|
548
|
+
for (const work of lingering) work.abort();
|
|
549
|
+
const terminalization = this.#retirementTerminalization(lingering.map(work => work.terminalize()));
|
|
550
|
+
this.#trackRetirementTerminalization(scopeKey, terminalization);
|
|
551
|
+
logger.warn("Slack attachment provider work exceeded the 5000ms retirement drain; fenced pending effects.");
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
#releaseRetiredProviderWorkScope(scope: SlackProviderWorkScope): void {
|
|
556
|
+
const scopeKey = slackProviderWorkScopeKey(scope);
|
|
557
|
+
const retired = this.#retiredProviderWorkScopes.get(scopeKey);
|
|
558
|
+
if (!retired || retired.retirements === 0) return;
|
|
559
|
+
retired.retirements--;
|
|
560
|
+
this.#tryReleaseRetiredProviderWorkScope(scopeKey);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
#recordProviderLifecycleError(error: unknown): void {
|
|
564
|
+
this.#providerLifecycleError = error;
|
|
565
|
+
this.#providerLifecycleErrorSet = true;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
#retainProviderLifecycle(tail: Promise<void>): void {
|
|
569
|
+
this.#providerLifecycleTail = tail;
|
|
570
|
+
void tail.then(
|
|
571
|
+
() => {
|
|
572
|
+
if (this.#providerLifecycleTail === tail) this.#providerLifecycleTail = undefined;
|
|
573
|
+
},
|
|
574
|
+
error => {
|
|
575
|
+
this.#recordProviderLifecycleError(error);
|
|
576
|
+
if (this.#providerLifecycleTail === tail) this.#providerLifecycleTail = undefined;
|
|
577
|
+
},
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
370
581
|
#enqueueLifecycle<T>(operation: () => Promise<T>): Promise<T> {
|
|
371
582
|
const scheduled = this.#lifecycleTail.then(operation, operation);
|
|
372
583
|
this.#lifecycleTail = scheduled.then(
|
|
@@ -405,6 +616,11 @@ export class SlackNotificationDaemon {
|
|
|
405
616
|
"session_not_live",
|
|
406
617
|
"Slack thread binding requires an exact live session endpoint.",
|
|
407
618
|
);
|
|
619
|
+
const attachment =
|
|
620
|
+
authority.attachmentAuthorityId === undefined ? await this.#resolveAttachment(sessionId) : undefined;
|
|
621
|
+
const attachmentAuthorityId =
|
|
622
|
+
authority.attachmentAuthorityId ??
|
|
623
|
+
(attachment?.generation === authority.endpointGeneration ? attachment.authorityId : undefined);
|
|
408
624
|
await this.#verifyExistingRoot(rootTs);
|
|
409
625
|
return await claimSlackThreadBinding({
|
|
410
626
|
store: this.store,
|
|
@@ -414,8 +630,14 @@ export class SlackNotificationDaemon {
|
|
|
414
630
|
sessionId,
|
|
415
631
|
rootTs,
|
|
416
632
|
endpointGeneration: authority.endpointGeneration,
|
|
633
|
+
...(attachmentAuthorityId === undefined ? {} : { attachmentAuthorityId }),
|
|
417
634
|
revalidate: async () => {
|
|
418
|
-
|
|
635
|
+
const current = await this.#bindingAuthority(sessionId);
|
|
636
|
+
if (
|
|
637
|
+
!current ||
|
|
638
|
+
current.endpointGeneration !== authority.endpointGeneration ||
|
|
639
|
+
current.attachmentAuthorityId !== attachmentAuthorityId
|
|
640
|
+
)
|
|
419
641
|
return false;
|
|
420
642
|
return commitAuthority ? await commitAuthority() : true;
|
|
421
643
|
},
|
|
@@ -429,10 +651,28 @@ export class SlackNotificationDaemon {
|
|
|
429
651
|
* usable generation, which is all a directly constructed daemon can prove.
|
|
430
652
|
*/
|
|
431
653
|
async #bindingAuthority(sessionId: string): Promise<SlackBindingAuthority | undefined> {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
654
|
+
const resolved = this.options.resolveBindingAuthority
|
|
655
|
+
? await this.options.resolveBindingAuthority(sessionId)
|
|
656
|
+
: undefined;
|
|
657
|
+
if (this.options.resolveBindingAuthority && !resolved) return undefined;
|
|
658
|
+
const endpoint = await this.#resolveAttachment(sessionId);
|
|
659
|
+
if (
|
|
660
|
+
!endpoint?.isCurrent() ||
|
|
661
|
+
endpoint.sessionId !== sessionId ||
|
|
662
|
+
!Number.isSafeInteger(endpoint.generation) ||
|
|
663
|
+
endpoint.generation <= 0 ||
|
|
664
|
+
(resolved !== undefined &&
|
|
665
|
+
(resolved.sessionId !== sessionId ||
|
|
666
|
+
resolved.endpointGeneration !== endpoint.generation ||
|
|
667
|
+
(resolved.attachmentAuthorityId !== undefined &&
|
|
668
|
+
resolved.attachmentAuthorityId !== endpoint.authorityId)))
|
|
669
|
+
)
|
|
670
|
+
return undefined;
|
|
671
|
+
return {
|
|
672
|
+
sessionId,
|
|
673
|
+
endpointGeneration: endpoint.generation,
|
|
674
|
+
...(endpoint.authorityId === undefined ? {} : { attachmentAuthorityId: endpoint.authorityId }),
|
|
675
|
+
};
|
|
436
676
|
}
|
|
437
677
|
|
|
438
678
|
/** Prove the operator-supplied root exists in the configured channel before persisting anything. */
|
|
@@ -456,14 +696,14 @@ export class SlackNotificationDaemon {
|
|
|
456
696
|
endpointGeneration?: number,
|
|
457
697
|
requestedClientMsgId?: string,
|
|
458
698
|
): Promise<SlackRootPublication> {
|
|
459
|
-
const endpoint = await this.#
|
|
699
|
+
const endpoint = await this.#resolveAttachment(sessionId);
|
|
460
700
|
if (!endpoint || (endpointGeneration !== undefined && endpoint.generation !== endpointGeneration))
|
|
461
701
|
throw new SlackEndpointBindingError("Slack root publication requires the current session endpoint.");
|
|
462
702
|
const generation = endpoint.generation;
|
|
463
703
|
const pendingKey = this.#intentKey(sessionId);
|
|
464
704
|
let claimed = false;
|
|
465
705
|
const pending = await this.store.transact(pendingKey, current => {
|
|
466
|
-
if (current?.state === "active") return current;
|
|
706
|
+
if (current?.state === "active" && current.attachmentAuthorityId === endpoint.authorityId) return current;
|
|
467
707
|
const now = this.#now();
|
|
468
708
|
if (
|
|
469
709
|
current?.rootPublicationOwner !== undefined &&
|
|
@@ -493,12 +733,17 @@ export class SlackNotificationDaemon {
|
|
|
493
733
|
rootPublicationLeaseExpiresAt: now + this.#publicationLeaseMs,
|
|
494
734
|
rootPublicationFence: (current?.rootPublicationFence ?? 0) + 1,
|
|
495
735
|
endpointGeneration: generation,
|
|
736
|
+
attachmentAuthorityId: endpoint.authorityId,
|
|
496
737
|
updatedAt: now,
|
|
497
|
-
seenEventIds: current?.seenEventIds ?? [],
|
|
498
|
-
seenContextIds:
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
738
|
+
seenEventIds: current?.attachmentAuthorityId === endpoint.authorityId ? (current?.seenEventIds ?? []) : [],
|
|
739
|
+
seenContextIds:
|
|
740
|
+
current?.attachmentAuthorityId === endpoint.authorityId ? (current?.seenContextIds ?? []) : [],
|
|
741
|
+
seenRetryKeys:
|
|
742
|
+
current?.attachmentAuthorityId === endpoint.authorityId ? (current?.seenRetryKeys ?? []) : [],
|
|
743
|
+
seenInteractionIds:
|
|
744
|
+
current?.attachmentAuthorityId === endpoint.authorityId ? (current?.seenInteractionIds ?? []) : [],
|
|
745
|
+
inboundDispatches:
|
|
746
|
+
current?.attachmentAuthorityId === endpoint.authorityId ? (current?.inboundDispatches ?? []) : [],
|
|
502
747
|
};
|
|
503
748
|
});
|
|
504
749
|
// An already-active session root is authoritative whether this daemon
|
|
@@ -524,6 +769,7 @@ export class SlackNotificationDaemon {
|
|
|
524
769
|
channel: this.options.channelId,
|
|
525
770
|
text: body,
|
|
526
771
|
clientMsgId,
|
|
772
|
+
...(endpoint.authorityId === undefined ? {} : { attachmentAuthorityId: endpoint.authorityId }),
|
|
527
773
|
});
|
|
528
774
|
createdRootEffect = !durable.existed;
|
|
529
775
|
posted = await this.#postDurable(effectId, sessionId, generation, durable.payload);
|
|
@@ -567,6 +813,7 @@ export class SlackNotificationDaemon {
|
|
|
567
813
|
state: "active",
|
|
568
814
|
rootTs: confirmedPosted.ts,
|
|
569
815
|
endpointGeneration: generation,
|
|
816
|
+
attachmentAuthorityId: endpoint.authorityId,
|
|
570
817
|
updatedAt: this.#now(),
|
|
571
818
|
lastError: undefined,
|
|
572
819
|
rootPublicationOwner: undefined,
|
|
@@ -587,7 +834,17 @@ export class SlackNotificationDaemon {
|
|
|
587
834
|
endpointGeneration?: number,
|
|
588
835
|
publicationId?: string,
|
|
589
836
|
): Promise<SlackConversation> {
|
|
590
|
-
|
|
837
|
+
return await this.#track(this.#notify(sessionId, body, actionId, endpointGeneration, publicationId));
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async #notify(
|
|
841
|
+
sessionId: string,
|
|
842
|
+
body: string,
|
|
843
|
+
actionId?: string,
|
|
844
|
+
endpointGeneration?: number,
|
|
845
|
+
publicationId?: string,
|
|
846
|
+
): Promise<SlackConversation> {
|
|
847
|
+
const endpoint = await this.#resolveAttachment(sessionId);
|
|
591
848
|
if (!endpoint || (endpointGeneration !== undefined && endpoint.generation !== endpointGeneration))
|
|
592
849
|
throw new SlackEndpointBindingError("Slack notification requires the current session endpoint.");
|
|
593
850
|
const generation = endpoint.generation;
|
|
@@ -602,7 +859,8 @@ export class SlackNotificationDaemon {
|
|
|
602
859
|
const usedExistingRoot =
|
|
603
860
|
existing?.record.state === "active" &&
|
|
604
861
|
!!existing.record.rootTs &&
|
|
605
|
-
existing.record.endpointGeneration === generation
|
|
862
|
+
existing.record.endpointGeneration === generation &&
|
|
863
|
+
existing.record.attachmentAuthorityId === endpoint.authorityId;
|
|
606
864
|
let conversation: SlackConversation;
|
|
607
865
|
let bodyWasUsedAsRoot =
|
|
608
866
|
usedExistingRoot &&
|
|
@@ -648,6 +906,9 @@ export class SlackNotificationDaemon {
|
|
|
648
906
|
threadTs: conversation.rootTs,
|
|
649
907
|
text: body,
|
|
650
908
|
clientMsgId: publication.clientMsgId,
|
|
909
|
+
...(conversation.attachmentAuthorityId === undefined
|
|
910
|
+
? {}
|
|
911
|
+
: { attachmentAuthorityId: conversation.attachmentAuthorityId }),
|
|
651
912
|
});
|
|
652
913
|
return conversation;
|
|
653
914
|
}
|
|
@@ -696,6 +957,9 @@ export class SlackNotificationDaemon {
|
|
|
696
957
|
threadTs: conversation.rootTs,
|
|
697
958
|
text: body,
|
|
698
959
|
clientMsgId,
|
|
960
|
+
...(conversation.attachmentAuthorityId === undefined
|
|
961
|
+
? {}
|
|
962
|
+
: { attachmentAuthorityId: conversation.attachmentAuthorityId }),
|
|
699
963
|
});
|
|
700
964
|
published = await this.#postDurable(effectId, sessionId, conversationGeneration, durable.payload);
|
|
701
965
|
});
|
|
@@ -742,6 +1006,10 @@ export class SlackNotificationDaemon {
|
|
|
742
1006
|
|
|
743
1007
|
/** Posts a safe command outcome to the active mapped root thread. */
|
|
744
1008
|
async postCommandResult(sessionId: string, content: string): Promise<boolean> {
|
|
1009
|
+
return await this.#track(this.#postCommandResult(sessionId, content));
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async #postCommandResult(sessionId: string, content: string): Promise<boolean> {
|
|
745
1013
|
const found = await this.findSession(sessionId, false);
|
|
746
1014
|
if (found?.record.state !== "active" || !found.record.rootTs) return false;
|
|
747
1015
|
const generation = this.#requireEndpointGeneration(found.record);
|
|
@@ -750,6 +1018,9 @@ export class SlackNotificationDaemon {
|
|
|
750
1018
|
threadTs: found.record.rootTs,
|
|
751
1019
|
text: content,
|
|
752
1020
|
clientMsgId: this.#randomId(),
|
|
1021
|
+
...(found.record.attachmentAuthorityId === undefined
|
|
1022
|
+
? {}
|
|
1023
|
+
: { attachmentAuthorityId: found.record.attachmentAuthorityId }),
|
|
753
1024
|
});
|
|
754
1025
|
return true;
|
|
755
1026
|
}
|
|
@@ -764,10 +1035,117 @@ export class SlackNotificationDaemon {
|
|
|
764
1035
|
);
|
|
765
1036
|
}
|
|
766
1037
|
|
|
767
|
-
async close(sessionId: string, marker = "Session closed."): Promise<boolean> {
|
|
1038
|
+
async close(sessionId: string, marker = "Session closed.", endpointGeneration?: number): Promise<boolean> {
|
|
1039
|
+
return await this.#track(this.#close(sessionId, marker, true, endpointGeneration));
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
async recoverCleanup(sessionId: string, endpointGeneration: number): Promise<void> {
|
|
1043
|
+
const found = await this.findSession(sessionId, true);
|
|
1044
|
+
if (
|
|
1045
|
+
!found?.record.rootTs ||
|
|
1046
|
+
found.record.state !== "active" ||
|
|
1047
|
+
found.record.endpointGeneration !== endpointGeneration
|
|
1048
|
+
)
|
|
1049
|
+
return;
|
|
1050
|
+
const effectId = `close-marker-cleanup:${sessionId}:${found.record.clientMsgId ?? found.record.rootTs}`;
|
|
1051
|
+
if (found.record.cleanupEffectId !== effectId) return;
|
|
1052
|
+
const effect = await this.#journal.read(effectId);
|
|
1053
|
+
if (!effect || (effect.state === "terminal" && !effect.receipt?.messageId && !effect.receipt?.timestamp)) {
|
|
1054
|
+
await this.store.transact(found.key, current =>
|
|
1055
|
+
current?.cleanupEffectId === effectId
|
|
1056
|
+
? nextRecord(current, { cleanupEffectId: undefined, updatedAt: this.#now() })
|
|
1057
|
+
: current,
|
|
1058
|
+
);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
await this.close(sessionId, "Session closed.", endpointGeneration);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async retireAttachment(sessionId: string, endpointGeneration: number): Promise<void> {
|
|
1065
|
+
const found = await this.findSession(sessionId, true);
|
|
1066
|
+
if (found?.record.endpointGeneration !== endpointGeneration) return;
|
|
1067
|
+
const attachmentAuthorityId = found.record.attachmentAuthorityId;
|
|
1068
|
+
const scope: SlackProviderWorkScope = {
|
|
1069
|
+
sessionId,
|
|
1070
|
+
endpointGeneration,
|
|
1071
|
+
...(attachmentAuthorityId === undefined ? {} : { attachmentAuthorityId }),
|
|
1072
|
+
};
|
|
1073
|
+
const deadline = this.#now() + RETIREMENT_DRAIN_MS;
|
|
1074
|
+
let releaseScope = false;
|
|
1075
|
+
try {
|
|
1076
|
+
const draining = this.#invalidateAndDrainProviderWork(scope, deadline);
|
|
1077
|
+
let inboundDispatches: SlackInboundDispatchReceipt[] = [];
|
|
1078
|
+
// Persist the mapping fence before waiting on terminal effects. A detached
|
|
1079
|
+
// terminalization can then never be replayed through this attachment.
|
|
1080
|
+
await this.store.transact(found.key, record => {
|
|
1081
|
+
if (
|
|
1082
|
+
record?.sessionId !== sessionId ||
|
|
1083
|
+
record.endpointGeneration !== endpointGeneration ||
|
|
1084
|
+
record.attachmentAuthorityId !== attachmentAuthorityId
|
|
1085
|
+
)
|
|
1086
|
+
return record;
|
|
1087
|
+
inboundDispatches = [...(record.inboundDispatches ?? [])];
|
|
1088
|
+
return nextRecord(record, {
|
|
1089
|
+
state: "closed_marker",
|
|
1090
|
+
pendingActionId: undefined,
|
|
1091
|
+
cleanupEffectId: undefined,
|
|
1092
|
+
inboundDispatches: [],
|
|
1093
|
+
updatedAt: this.#now(),
|
|
1094
|
+
});
|
|
1095
|
+
});
|
|
1096
|
+
releaseScope = true;
|
|
1097
|
+
await draining;
|
|
1098
|
+
if (inboundDispatches.length === 0) return;
|
|
1099
|
+
const terminalization = this.#retirementTerminalization(
|
|
1100
|
+
inboundDispatches.map(receipt => this.#journal.terminalize(receipt.effectId, { status: "stale_binding" })),
|
|
1101
|
+
);
|
|
1102
|
+
this.#trackRetirementTerminalization(slackProviderWorkScopeKey(scope), terminalization);
|
|
1103
|
+
const remaining = deadline - this.#now();
|
|
1104
|
+
if (remaining <= 0) {
|
|
1105
|
+
logger.warn("Slack attachment retirement exceeded the 5000ms deadline; fenced pending inbound effects.");
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const terminalized = await Promise.race([
|
|
1109
|
+
terminalization.then(() => true),
|
|
1110
|
+
Bun.sleep(remaining).then(() => false),
|
|
1111
|
+
]);
|
|
1112
|
+
if (!terminalized)
|
|
1113
|
+
logger.warn("Slack attachment retirement exceeded the 5000ms deadline; fenced pending inbound effects.");
|
|
1114
|
+
} finally {
|
|
1115
|
+
if (releaseScope) this.#releaseRetiredProviderWorkScope(scope);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
async #close(
|
|
1120
|
+
sessionId: string,
|
|
1121
|
+
marker: string,
|
|
1122
|
+
allowRemovedAttachment: boolean,
|
|
1123
|
+
expectedEndpointGeneration?: number,
|
|
1124
|
+
): Promise<boolean> {
|
|
768
1125
|
const found = await this.findSession(sessionId, true);
|
|
769
1126
|
if (!found?.record.rootTs || found.record.state !== "active") return false;
|
|
770
|
-
|
|
1127
|
+
if (expectedEndpointGeneration !== undefined && found.record.endpointGeneration !== expectedEndpointGeneration)
|
|
1128
|
+
return false;
|
|
1129
|
+
const effectId = `${allowRemovedAttachment ? "close-marker-cleanup" : "close-marker"}:${sessionId}:${found.record.clientMsgId ?? found.record.rootTs}`;
|
|
1130
|
+
if (allowRemovedAttachment) {
|
|
1131
|
+
let admitted = false;
|
|
1132
|
+
await this.store.transact(found.key, current => {
|
|
1133
|
+
if (
|
|
1134
|
+
current?.state !== "active" ||
|
|
1135
|
+
current.sessionId !== found.record.sessionId ||
|
|
1136
|
+
current.rootTs !== found.record.rootTs ||
|
|
1137
|
+
current.endpointGeneration !== found.record.endpointGeneration ||
|
|
1138
|
+
current.clientMsgId !== found.record.clientMsgId ||
|
|
1139
|
+
(current.cleanupEffectId !== undefined && current.cleanupEffectId !== effectId)
|
|
1140
|
+
)
|
|
1141
|
+
return current;
|
|
1142
|
+
admitted = true;
|
|
1143
|
+
return current.cleanupEffectId === effectId
|
|
1144
|
+
? current
|
|
1145
|
+
: nextRecord(current, { cleanupEffectId: effectId, updatedAt: this.#now() });
|
|
1146
|
+
});
|
|
1147
|
+
if (!admitted) return false;
|
|
1148
|
+
}
|
|
771
1149
|
const existing = await this.#journal.read<{
|
|
772
1150
|
channel: string;
|
|
773
1151
|
text: string;
|
|
@@ -783,16 +1161,33 @@ export class SlackNotificationDaemon {
|
|
|
783
1161
|
threadTs: found.record.rootTs,
|
|
784
1162
|
text: marker,
|
|
785
1163
|
clientMsgId: existing?.payload.clientMsgId ?? this.#randomId(),
|
|
1164
|
+
...(found.record.attachmentAuthorityId === undefined
|
|
1165
|
+
? {}
|
|
1166
|
+
: { attachmentAuthorityId: found.record.attachmentAuthorityId }),
|
|
786
1167
|
},
|
|
787
1168
|
);
|
|
788
1169
|
await this.#postDurable(effectId, sessionId, this.#requireEndpointGeneration(found.record), durable.payload);
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1170
|
+
let closed = false;
|
|
1171
|
+
await this.store.transact(found.key, current => {
|
|
1172
|
+
if (
|
|
1173
|
+
current?.state !== "active" ||
|
|
1174
|
+
current.sessionId !== found.record.sessionId ||
|
|
1175
|
+
current.rootTs !== found.record.rootTs ||
|
|
1176
|
+
current.endpointGeneration !== found.record.endpointGeneration ||
|
|
1177
|
+
current.clientMsgId !== found.record.clientMsgId ||
|
|
1178
|
+
(allowRemovedAttachment && current.cleanupEffectId !== effectId)
|
|
1179
|
+
)
|
|
1180
|
+
return current;
|
|
1181
|
+
closed = true;
|
|
1182
|
+
return nextRecord(current, {
|
|
1183
|
+
state: "closed_marker",
|
|
1184
|
+
cleanupEffectId: undefined,
|
|
1185
|
+
pendingActionId: undefined,
|
|
1186
|
+
updatedAt: this.#now(),
|
|
1187
|
+
});
|
|
1188
|
+
});
|
|
794
1189
|
|
|
795
|
-
return
|
|
1190
|
+
return closed;
|
|
796
1191
|
}
|
|
797
1192
|
|
|
798
1193
|
async resume(
|
|
@@ -800,6 +1195,15 @@ export class SlackNotificationDaemon {
|
|
|
800
1195
|
body: string,
|
|
801
1196
|
endpointGeneration?: number,
|
|
802
1197
|
publicationId?: string,
|
|
1198
|
+
): Promise<SlackConversation> {
|
|
1199
|
+
return await this.#track(this.#resume(sessionId, body, endpointGeneration, publicationId));
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
async #resume(
|
|
1203
|
+
sessionId: string,
|
|
1204
|
+
body: string,
|
|
1205
|
+
endpointGeneration?: number,
|
|
1206
|
+
publicationId?: string,
|
|
803
1207
|
): Promise<SlackConversation> {
|
|
804
1208
|
const rootPublication =
|
|
805
1209
|
publicationId === undefined
|
|
@@ -843,7 +1247,7 @@ export class SlackNotificationDaemon {
|
|
|
843
1247
|
endpointGeneration?: number,
|
|
844
1248
|
requestedClientMsgId?: string,
|
|
845
1249
|
): Promise<SlackRootPublication> {
|
|
846
|
-
const endpoint = await this.#
|
|
1250
|
+
const endpoint = await this.#resolveAttachment(sessionId);
|
|
847
1251
|
if (!endpoint || (endpointGeneration !== undefined && endpoint.generation !== endpointGeneration))
|
|
848
1252
|
throw new SlackEndpointBindingError("Slack root rollover requires the current session endpoint.");
|
|
849
1253
|
const generation = endpoint.generation;
|
|
@@ -872,7 +1276,11 @@ export class SlackNotificationDaemon {
|
|
|
872
1276
|
const previousGeneration = this.#requireEndpointGeneration(previous.record);
|
|
873
1277
|
if (previousGeneration > generation)
|
|
874
1278
|
throw new SlackEndpointBindingError("Slack root belongs to a newer endpoint generation.");
|
|
875
|
-
if (
|
|
1279
|
+
if (
|
|
1280
|
+
previousGeneration === generation &&
|
|
1281
|
+
previous.record.attachmentAuthorityId === endpoint.authorityId &&
|
|
1282
|
+
(!forced || waitedForRollover)
|
|
1283
|
+
)
|
|
876
1284
|
return { conversation: previous.record, created: false };
|
|
877
1285
|
|
|
878
1286
|
let claimed = false;
|
|
@@ -907,7 +1315,7 @@ export class SlackNotificationDaemon {
|
|
|
907
1315
|
}
|
|
908
1316
|
let closed = false;
|
|
909
1317
|
try {
|
|
910
|
-
closed = await this
|
|
1318
|
+
closed = await this.#track(this.#close(sessionId, "Session closed.", false, previousGeneration));
|
|
911
1319
|
} catch (error) {
|
|
912
1320
|
if (!(error instanceof SlackStaleEffectError)) throw error;
|
|
913
1321
|
// The old-generation close marker was deliberately suppressed after
|
|
@@ -946,7 +1354,7 @@ export class SlackNotificationDaemon {
|
|
|
946
1354
|
},
|
|
947
1355
|
actorId: string,
|
|
948
1356
|
): Promise<
|
|
949
|
-
{ key: string; endpoint:
|
|
1357
|
+
{ key: string; endpoint: SessionAttachment; sessionId: string; receipt: SlackInboundDispatchReceipt } | undefined
|
|
950
1358
|
> {
|
|
951
1359
|
const document = await this.store.load();
|
|
952
1360
|
const matched = Object.entries(document.conversations)
|
|
@@ -971,8 +1379,13 @@ export class SlackNotificationDaemon {
|
|
|
971
1379
|
!acceptsSlackInbound(record, inbound.rootTs, record.endpointGeneration)
|
|
972
1380
|
)
|
|
973
1381
|
return undefined;
|
|
974
|
-
const endpoint = await this.#
|
|
975
|
-
if (
|
|
1382
|
+
const endpoint = await this.#resolveAttachment(record.sessionId);
|
|
1383
|
+
if (
|
|
1384
|
+
!endpoint ||
|
|
1385
|
+
endpoint.generation !== record.endpointGeneration ||
|
|
1386
|
+
record.attachmentAuthorityId !== endpoint.authorityId
|
|
1387
|
+
)
|
|
1388
|
+
return undefined;
|
|
976
1389
|
const interactionId = text(inbound.event.client_msg_id) ?? inbound.eventId;
|
|
977
1390
|
const retryKey = `${inbound.eventId}:${interactionId}`;
|
|
978
1391
|
const inboundText = text(inbound.event.text);
|
|
@@ -984,6 +1397,7 @@ export class SlackNotificationDaemon {
|
|
|
984
1397
|
teamId: inbound.teamId,
|
|
985
1398
|
channelId: inbound.channelId,
|
|
986
1399
|
rootTs: inbound.rootTs,
|
|
1400
|
+
...(record.attachmentAuthorityId === undefined ? {} : { attachmentAuthorityId: record.attachmentAuthorityId }),
|
|
987
1401
|
eventId: inbound.eventId,
|
|
988
1402
|
interactionId,
|
|
989
1403
|
actorId,
|
|
@@ -1013,7 +1427,12 @@ export class SlackNotificationDaemon {
|
|
|
1013
1427
|
let sessionId: string | undefined;
|
|
1014
1428
|
let receipt: SlackInboundDispatchReceipt | undefined;
|
|
1015
1429
|
await this.store.transact(key, current => {
|
|
1016
|
-
if (
|
|
1430
|
+
if (
|
|
1431
|
+
!current?.sessionId ||
|
|
1432
|
+
!acceptsSlackInbound(current, inbound.rootTs, endpoint.generation) ||
|
|
1433
|
+
current.attachmentAuthorityId !== endpoint.authorityId
|
|
1434
|
+
)
|
|
1435
|
+
return current;
|
|
1017
1436
|
const existing = (current.inboundDispatches ?? []).find(
|
|
1018
1437
|
candidate =>
|
|
1019
1438
|
candidate.eventId === inbound.eventId ||
|
|
@@ -1044,6 +1463,9 @@ export class SlackNotificationDaemon {
|
|
|
1044
1463
|
kind: command ? "command" : "action",
|
|
1045
1464
|
...(command ? {} : { actionId: current.pendingActionId }),
|
|
1046
1465
|
endpointGeneration: endpoint.generation,
|
|
1466
|
+
...(current.attachmentAuthorityId === undefined
|
|
1467
|
+
? {}
|
|
1468
|
+
: { attachmentAuthorityId: current.attachmentAuthorityId }),
|
|
1047
1469
|
effectId,
|
|
1048
1470
|
idempotencyKey,
|
|
1049
1471
|
};
|
|
@@ -1057,7 +1479,8 @@ export class SlackNotificationDaemon {
|
|
|
1057
1479
|
|
|
1058
1480
|
async #dispatchInbound(claim: {
|
|
1059
1481
|
key: string;
|
|
1060
|
-
endpoint:
|
|
1482
|
+
endpoint: SessionAttachment;
|
|
1483
|
+
|
|
1061
1484
|
sessionId: string;
|
|
1062
1485
|
receipt: SlackInboundDispatchReceipt;
|
|
1063
1486
|
}): Promise<boolean> {
|
|
@@ -1079,7 +1502,8 @@ export class SlackNotificationDaemon {
|
|
|
1079
1502
|
}
|
|
1080
1503
|
|
|
1081
1504
|
async #dispatchEffect(
|
|
1082
|
-
claim: { key: string; endpoint:
|
|
1505
|
+
claim: { key: string; endpoint: SessionAttachment; sessionId: string; receipt: SlackInboundDispatchReceipt },
|
|
1506
|
+
|
|
1083
1507
|
effectId: string,
|
|
1084
1508
|
): Promise<boolean> {
|
|
1085
1509
|
const effect = await this.#rescheduleAfterEffectTransition(
|
|
@@ -1095,10 +1519,17 @@ export class SlackNotificationDaemon {
|
|
|
1095
1519
|
return false;
|
|
1096
1520
|
}
|
|
1097
1521
|
const lease: ChatEffectLease = { owner: this.#publicationOwnerId, epoch: effect.epoch };
|
|
1522
|
+
const workScope: SlackProviderWorkScope = {
|
|
1523
|
+
sessionId: claim.sessionId,
|
|
1524
|
+
endpointGeneration: claim.receipt.endpointGeneration,
|
|
1525
|
+
...(claim.receipt.attachmentAuthorityId === undefined
|
|
1526
|
+
? {}
|
|
1527
|
+
: { attachmentAuthorityId: claim.receipt.attachmentAuthorityId }),
|
|
1528
|
+
};
|
|
1098
1529
|
try {
|
|
1099
1530
|
if (effect.payload.type === "command") {
|
|
1100
1531
|
const payload = effect.payload;
|
|
1101
|
-
const accepted = await this.#withEffectLease(effect.id, lease, async () => {
|
|
1532
|
+
const accepted = await this.#withEffectLease(effect.id, lease, workScope, async () => {
|
|
1102
1533
|
if (!(await this.#inboundEffectCurrent(claim, effect.id))) throw new SlackStaleEffectError();
|
|
1103
1534
|
return await (this.options.onCommand?.(
|
|
1104
1535
|
claim.sessionId,
|
|
@@ -1117,9 +1548,9 @@ export class SlackNotificationDaemon {
|
|
|
1117
1548
|
if (recorded) await this.#finishDispatch(claim, "terminal");
|
|
1118
1549
|
return accepted && !!recorded;
|
|
1119
1550
|
}
|
|
1120
|
-
await this.#withEffectLease(effect.id, lease, async () => {
|
|
1551
|
+
await this.#withEffectLease(effect.id, lease, workScope, async () => {
|
|
1121
1552
|
if (!(await this.#inboundEffectCurrent(claim, effect.id))) throw new SlackStaleEffectError();
|
|
1122
|
-
|
|
1553
|
+
await claim.endpoint.send(effect.payload);
|
|
1123
1554
|
});
|
|
1124
1555
|
if (!(await this.#inboundEffectCurrent(claim, effect.id))) {
|
|
1125
1556
|
await this.#terminalizeStaleInboundDispatch(claim.key, claim.receipt, "stale_binding");
|
|
@@ -1145,11 +1576,18 @@ export class SlackNotificationDaemon {
|
|
|
1145
1576
|
}
|
|
1146
1577
|
|
|
1147
1578
|
async #inboundEffectCurrent(
|
|
1148
|
-
claim: { key: string; endpoint:
|
|
1579
|
+
claim: { key: string; endpoint: SessionAttachment; sessionId: string; receipt: SlackInboundDispatchReceipt },
|
|
1580
|
+
|
|
1149
1581
|
effectId: string,
|
|
1150
1582
|
): Promise<boolean> {
|
|
1151
|
-
const endpoint = await this.#
|
|
1152
|
-
if (!endpoint
|
|
1583
|
+
const endpoint = await this.#resolveAttachment(claim.sessionId);
|
|
1584
|
+
if (!endpoint?.isCurrent()) return false;
|
|
1585
|
+
if (
|
|
1586
|
+
endpoint.authorityId !== undefined && claim.endpoint.authorityId !== undefined
|
|
1587
|
+
? endpoint.authorityId !== claim.endpoint.authorityId
|
|
1588
|
+
: endpoint.generation !== claim.endpoint.generation
|
|
1589
|
+
)
|
|
1590
|
+
return false;
|
|
1153
1591
|
const [current, effect] = await Promise.all([
|
|
1154
1592
|
this.store.read(claim.key),
|
|
1155
1593
|
this.#journal.read<SlackInboundEffectPayload>(effectId),
|
|
@@ -1160,27 +1598,107 @@ export class SlackNotificationDaemon {
|
|
|
1160
1598
|
current.sessionId === claim.sessionId &&
|
|
1161
1599
|
acceptsSlackInbound(current, current.rootTs ?? "", claim.endpoint.generation) &&
|
|
1162
1600
|
claim.receipt.endpointGeneration === claim.endpoint.generation &&
|
|
1601
|
+
current.attachmentAuthorityId === claim.receipt.attachmentAuthorityId &&
|
|
1602
|
+
current.attachmentAuthorityId === endpoint.authorityId &&
|
|
1163
1603
|
this.#matchesInboundEffect(effect, claim.receipt) &&
|
|
1164
1604
|
(await this.#actorAuthorized(effect.payload.routing.actorId)) &&
|
|
1165
1605
|
(current.inboundDispatches ?? []).some(receipt => this.#sameInboundReceipt(receipt, claim.receipt))
|
|
1166
1606
|
);
|
|
1167
1607
|
}
|
|
1168
1608
|
|
|
1169
|
-
async #withEffectLease<T>(
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1609
|
+
async #withEffectLease<T>(
|
|
1610
|
+
id: string,
|
|
1611
|
+
lease: ChatEffectLease,
|
|
1612
|
+
scope: SlackProviderWorkScope,
|
|
1613
|
+
operation: (signal: AbortSignal) => Promise<T>,
|
|
1614
|
+
workGeneration = this.#workGeneration,
|
|
1615
|
+
): Promise<T> {
|
|
1616
|
+
const scopeKey = slackProviderWorkScopeKey(scope);
|
|
1617
|
+
const controller = new AbortController();
|
|
1618
|
+
let invalidation: Promise<void> | undefined;
|
|
1619
|
+
let terminalization: Promise<void> | undefined;
|
|
1620
|
+
let invalidated = false;
|
|
1621
|
+
const abort = (): void => {
|
|
1622
|
+
invalidated = true;
|
|
1623
|
+
controller.abort();
|
|
1624
|
+
};
|
|
1625
|
+
const invalidate = async (): Promise<void> => {
|
|
1626
|
+
abort();
|
|
1627
|
+
invalidation ??= this.#rescheduleAfterEffectTransition(
|
|
1628
|
+
this.#journal.record(id, lease, "uncertain", { status: "shutdown_timeout" }),
|
|
1629
|
+
).then(() => undefined);
|
|
1630
|
+
await invalidation;
|
|
1631
|
+
};
|
|
1632
|
+
const terminalize = async (): Promise<void> => {
|
|
1633
|
+
abort();
|
|
1634
|
+
terminalization ??= (async (): Promise<void> => {
|
|
1635
|
+
const receipt = { status: "stale_binding" };
|
|
1636
|
+
const terminalized = await this.#rescheduleAfterEffectTransition(
|
|
1637
|
+
this.#journal.terminalize(id, receipt, lease),
|
|
1638
|
+
);
|
|
1639
|
+
if (!terminalized) await this.#rescheduleAfterEffectTransition(this.#journal.terminalize(id, receipt));
|
|
1640
|
+
})();
|
|
1641
|
+
await terminalization;
|
|
1642
|
+
};
|
|
1643
|
+
const settled = Promise.withResolvers<void>();
|
|
1644
|
+
const work: SlackProviderWork = {
|
|
1645
|
+
scopeKey,
|
|
1646
|
+
invalidate,
|
|
1647
|
+
abort,
|
|
1648
|
+
terminalize,
|
|
1649
|
+
settled: settled.promise,
|
|
1650
|
+
};
|
|
1651
|
+
this.#workInvalidators.add(work);
|
|
1652
|
+
const retiring = (): boolean => this.#retiredProviderWorkScopes.has(scopeKey);
|
|
1653
|
+
const ensureLive = async (): Promise<void> => {
|
|
1654
|
+
if (retiring()) {
|
|
1655
|
+
await terminalize();
|
|
1656
|
+
throw new SlackStaleEffectError();
|
|
1657
|
+
}
|
|
1658
|
+
if (invalidated || workGeneration !== this.#workGeneration) {
|
|
1659
|
+
await invalidate();
|
|
1660
|
+
throw new Error(`Slack effect ${id} was admitted after shutdown drain expiry`);
|
|
1661
|
+
}
|
|
1662
|
+
if (
|
|
1663
|
+
!(await this.#rescheduleAfterEffectTransition(
|
|
1664
|
+
this.#journal.renew(id, lease, Math.max(this.#publicationLeaseMs, 100)),
|
|
1665
|
+
))
|
|
1666
|
+
)
|
|
1667
|
+
throw new Error("Slack effect lease renewal failed");
|
|
1668
|
+
if (retiring()) {
|
|
1669
|
+
await terminalize();
|
|
1670
|
+
throw new SlackStaleEffectError();
|
|
1671
|
+
}
|
|
1672
|
+
if (workGeneration !== this.#workGeneration) {
|
|
1673
|
+
await invalidate();
|
|
1674
|
+
throw new Error(`Slack effect ${id} lost its shutdown fence`);
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
try {
|
|
1678
|
+
await ensureLive();
|
|
1679
|
+
const result = await this.#withRenewal(
|
|
1680
|
+
async () => {
|
|
1681
|
+
await ensureLive();
|
|
1682
|
+
try {
|
|
1683
|
+
return await operation(controller.signal);
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
if (retiring()) {
|
|
1686
|
+
await terminalize();
|
|
1687
|
+
throw new SlackStaleEffectError();
|
|
1688
|
+
}
|
|
1689
|
+
throw error;
|
|
1690
|
+
}
|
|
1691
|
+
},
|
|
1692
|
+
ensureLive,
|
|
1693
|
+
false,
|
|
1694
|
+
);
|
|
1695
|
+
await ensureLive();
|
|
1696
|
+
return result;
|
|
1697
|
+
} finally {
|
|
1698
|
+
this.#workInvalidators.delete(work);
|
|
1699
|
+
settled.resolve();
|
|
1700
|
+
this.#tryReleaseRetiredProviderWorkScope(scopeKey);
|
|
1701
|
+
}
|
|
1184
1702
|
}
|
|
1185
1703
|
|
|
1186
1704
|
async #drainPendingDispatches(): Promise<void> {
|
|
@@ -1198,8 +1716,12 @@ export class SlackNotificationDaemon {
|
|
|
1198
1716
|
await this.#terminalizeStaleInboundDispatch(key, receipt, "stale_mapping");
|
|
1199
1717
|
continue;
|
|
1200
1718
|
}
|
|
1201
|
-
const endpoint = await this.#
|
|
1202
|
-
if (
|
|
1719
|
+
const endpoint = await this.#resolveAttachment(record.sessionId!);
|
|
1720
|
+
if (
|
|
1721
|
+
!endpoint ||
|
|
1722
|
+
endpoint.generation !== receipt.endpointGeneration ||
|
|
1723
|
+
record.attachmentAuthorityId !== endpoint.authorityId
|
|
1724
|
+
) {
|
|
1203
1725
|
await this.#terminalizeStaleInboundDispatch(key, receipt, "stale_binding");
|
|
1204
1726
|
continue;
|
|
1205
1727
|
}
|
|
@@ -1230,7 +1752,7 @@ export class SlackNotificationDaemon {
|
|
|
1230
1752
|
continue;
|
|
1231
1753
|
const adopted = await this.#adoptOrphanInbound(effect as ChatEffect<SlackInboundEffectPayload>);
|
|
1232
1754
|
if (!adopted) continue;
|
|
1233
|
-
const endpoint = await this.#
|
|
1755
|
+
const endpoint = await this.#resolveAttachment(adopted.sessionId);
|
|
1234
1756
|
if (
|
|
1235
1757
|
!endpoint ||
|
|
1236
1758
|
endpoint.generation !== adopted.receipt.endpointGeneration ||
|
|
@@ -1299,6 +1821,7 @@ export class SlackNotificationDaemon {
|
|
|
1299
1821
|
receipt.kind === routing.kind &&
|
|
1300
1822
|
receipt.actionId === routing.actionId &&
|
|
1301
1823
|
receipt.endpointGeneration === effect.endpointGeneration &&
|
|
1824
|
+
receipt.attachmentAuthorityId === routing.attachmentAuthorityId &&
|
|
1302
1825
|
receipt.effectId === effect.id &&
|
|
1303
1826
|
receipt.idempotencyKey === payload.idempotencyKey &&
|
|
1304
1827
|
((payload.type === "command" && routing.kind === "command" && effect.kind === "sdk.inbound.command") ||
|
|
@@ -1318,6 +1841,7 @@ export class SlackNotificationDaemon {
|
|
|
1318
1841
|
left.kind === right.kind &&
|
|
1319
1842
|
left.actionId === right.actionId &&
|
|
1320
1843
|
left.endpointGeneration === right.endpointGeneration &&
|
|
1844
|
+
left.attachmentAuthorityId === right.attachmentAuthorityId &&
|
|
1321
1845
|
left.effectId === right.effectId &&
|
|
1322
1846
|
left.idempotencyKey === right.idempotencyKey
|
|
1323
1847
|
);
|
|
@@ -1333,6 +1857,8 @@ export class SlackNotificationDaemon {
|
|
|
1333
1857
|
!!record.sessionId &&
|
|
1334
1858
|
!!record.rootTs &&
|
|
1335
1859
|
acceptsSlackInbound(record, record.rootTs, receipt.endpointGeneration) &&
|
|
1860
|
+
record.attachmentAuthorityId === receipt.attachmentAuthorityId &&
|
|
1861
|
+
effect.payload.routing.attachmentAuthorityId === receipt.attachmentAuthorityId &&
|
|
1336
1862
|
record.sessionId === effect.sessionId &&
|
|
1337
1863
|
this.#matchesInboundEffect(effect, receipt)
|
|
1338
1864
|
);
|
|
@@ -1440,7 +1966,8 @@ export class SlackNotificationDaemon {
|
|
|
1440
1966
|
!current ||
|
|
1441
1967
|
!effect.sessionId ||
|
|
1442
1968
|
current.sessionId !== effect.sessionId ||
|
|
1443
|
-
!acceptsSlackInbound(current, routing.rootTs, effect.endpointGeneration)
|
|
1969
|
+
!acceptsSlackInbound(current, routing.rootTs, effect.endpointGeneration) ||
|
|
1970
|
+
current.attachmentAuthorityId !== routing.attachmentAuthorityId
|
|
1444
1971
|
)
|
|
1445
1972
|
return current;
|
|
1446
1973
|
const existing = (current.inboundDispatches ?? []).find(
|
|
@@ -1471,6 +1998,9 @@ export class SlackNotificationDaemon {
|
|
|
1471
1998
|
kind: routing.kind,
|
|
1472
1999
|
...(routing.kind === "action" ? { actionId: routing.actionId } : {}),
|
|
1473
2000
|
endpointGeneration: effect.endpointGeneration,
|
|
2001
|
+
...(routing.attachmentAuthorityId === undefined
|
|
2002
|
+
? {}
|
|
2003
|
+
: { attachmentAuthorityId: routing.attachmentAuthorityId }),
|
|
1474
2004
|
effectId: effect.id,
|
|
1475
2005
|
idempotencyKey: payload.idempotencyKey,
|
|
1476
2006
|
};
|
|
@@ -1489,7 +2019,8 @@ export class SlackNotificationDaemon {
|
|
|
1489
2019
|
}
|
|
1490
2020
|
|
|
1491
2021
|
async #finishDispatch(
|
|
1492
|
-
claim: { key: string; endpoint:
|
|
2022
|
+
claim: { key: string; endpoint: SessionAttachment; receipt: SlackInboundDispatchReceipt },
|
|
2023
|
+
|
|
1493
2024
|
state: "terminal" | "uncertain",
|
|
1494
2025
|
): Promise<void> {
|
|
1495
2026
|
await this.store.transact(claim.key, current => {
|
|
@@ -1503,7 +2034,7 @@ export class SlackNotificationDaemon {
|
|
|
1503
2034
|
|
|
1504
2035
|
async #releaseDispatch(claim: {
|
|
1505
2036
|
key: string;
|
|
1506
|
-
endpoint:
|
|
2037
|
+
endpoint: SessionAttachment;
|
|
1507
2038
|
receipt: SlackInboundDispatchReceipt;
|
|
1508
2039
|
}): Promise<void> {
|
|
1509
2040
|
await this.store.transact(claim.key, current => {
|
|
@@ -1734,6 +2265,7 @@ export class SlackNotificationDaemon {
|
|
|
1734
2265
|
text?: unknown;
|
|
1735
2266
|
threadTs?: unknown;
|
|
1736
2267
|
clientMsgId?: unknown;
|
|
2268
|
+
attachmentAuthorityId?: unknown;
|
|
1737
2269
|
};
|
|
1738
2270
|
if (
|
|
1739
2271
|
typeof payload.channel !== "string" ||
|
|
@@ -1747,6 +2279,9 @@ export class SlackNotificationDaemon {
|
|
|
1747
2279
|
text: payload.text,
|
|
1748
2280
|
...(typeof payload.threadTs === "string" ? { threadTs: payload.threadTs } : {}),
|
|
1749
2281
|
clientMsgId: payload.clientMsgId,
|
|
2282
|
+
...(typeof payload.attachmentAuthorityId === "string"
|
|
2283
|
+
? { attachmentAuthorityId: payload.attachmentAuthorityId }
|
|
2284
|
+
: {}),
|
|
1750
2285
|
});
|
|
1751
2286
|
} catch {
|
|
1752
2287
|
failed = true;
|
|
@@ -1855,22 +2390,32 @@ export class SlackNotificationDaemon {
|
|
|
1855
2390
|
async #providerEffectCurrent(effect: ChatEffect): Promise<boolean> {
|
|
1856
2391
|
if (!effect.sessionId || !Number.isSafeInteger(effect.endpointGeneration) || effect.endpointGeneration <= 0)
|
|
1857
2392
|
return false;
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
2393
|
+
if (!effect.id.startsWith("close-marker-cleanup:")) {
|
|
2394
|
+
const endpoint = await this.#resolveAttachment(effect.sessionId);
|
|
2395
|
+
const payload = effect.payload as { attachmentAuthorityId?: unknown; threadTs?: unknown };
|
|
2396
|
+
if (
|
|
2397
|
+
!endpoint ||
|
|
2398
|
+
endpoint.generation !== effect.endpointGeneration ||
|
|
2399
|
+
payload.attachmentAuthorityId !== endpoint.authorityId
|
|
2400
|
+
)
|
|
2401
|
+
return false;
|
|
2402
|
+
}
|
|
2403
|
+
const payload = effect.payload as { attachmentAuthorityId?: unknown; threadTs?: unknown };
|
|
1861
2404
|
const threadTs = payload.threadTs;
|
|
1862
2405
|
const records = Object.values((await this.store.load()).conversations);
|
|
1863
2406
|
if (typeof threadTs === "string") {
|
|
1864
2407
|
return records.some(
|
|
1865
2408
|
record =>
|
|
1866
2409
|
record.sessionId === effect.sessionId &&
|
|
1867
|
-
acceptsSlackInbound(record, threadTs, effect.endpointGeneration)
|
|
2410
|
+
acceptsSlackInbound(record, threadTs, effect.endpointGeneration) &&
|
|
2411
|
+
record.attachmentAuthorityId === payload.attachmentAuthorityId,
|
|
1868
2412
|
);
|
|
1869
2413
|
}
|
|
1870
2414
|
return records.some(
|
|
1871
2415
|
record =>
|
|
1872
2416
|
record.sessionId === effect.sessionId &&
|
|
1873
2417
|
record.endpointGeneration === effect.endpointGeneration &&
|
|
2418
|
+
record.attachmentAuthorityId === payload.attachmentAuthorityId &&
|
|
1874
2419
|
record.state === "posting_root",
|
|
1875
2420
|
);
|
|
1876
2421
|
}
|
|
@@ -1897,23 +2442,34 @@ export class SlackNotificationDaemon {
|
|
|
1897
2442
|
id: string,
|
|
1898
2443
|
sessionId: string,
|
|
1899
2444
|
endpointGeneration: number,
|
|
1900
|
-
expected: {
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2445
|
+
expected: {
|
|
2446
|
+
channel: string;
|
|
2447
|
+
text: string;
|
|
2448
|
+
threadTs?: string;
|
|
2449
|
+
clientMsgId: string;
|
|
2450
|
+
attachmentAuthorityId?: string;
|
|
2451
|
+
},
|
|
2452
|
+
): Promise<{
|
|
2453
|
+
payload: typeof expected & { attachmentAuthorityId?: string };
|
|
2454
|
+
existed: boolean;
|
|
2455
|
+
}> {
|
|
2456
|
+
const durableExpected = { ...expected };
|
|
2457
|
+
const existing = await this.#journal.read<typeof durableExpected>(id);
|
|
2458
|
+
if (!existing) return { payload: durableExpected, existed: false };
|
|
1904
2459
|
const payload = existing.payload;
|
|
1905
2460
|
if (
|
|
1906
2461
|
existing.kind !== "provider-post" ||
|
|
1907
2462
|
existing.transport !== "slack" ||
|
|
1908
2463
|
existing.sessionId !== sessionId ||
|
|
1909
2464
|
existing.endpointGeneration !== endpointGeneration ||
|
|
2465
|
+
payload.attachmentAuthorityId !== durableExpected.attachmentAuthorityId ||
|
|
1910
2466
|
typeof payload.channel !== "string" ||
|
|
1911
2467
|
typeof payload.text !== "string" ||
|
|
1912
2468
|
typeof payload.clientMsgId !== "string" ||
|
|
1913
2469
|
(payload.threadTs !== undefined && typeof payload.threadTs !== "string") ||
|
|
1914
|
-
payload.channel !==
|
|
1915
|
-
payload.threadTs !==
|
|
1916
|
-
payload.clientMsgId !==
|
|
2470
|
+
payload.channel !== durableExpected.channel ||
|
|
2471
|
+
payload.threadTs !== durableExpected.threadTs ||
|
|
2472
|
+
payload.clientMsgId !== durableExpected.clientMsgId
|
|
1917
2473
|
) {
|
|
1918
2474
|
throw new SlackEndpointBindingError("Slack provider effect identity does not match the durable occurrence.");
|
|
1919
2475
|
}
|
|
@@ -1924,10 +2480,17 @@ export class SlackNotificationDaemon {
|
|
|
1924
2480
|
id: string,
|
|
1925
2481
|
sessionId: string,
|
|
1926
2482
|
endpointGeneration: number,
|
|
1927
|
-
payload: {
|
|
2483
|
+
payload: {
|
|
2484
|
+
channel: string;
|
|
2485
|
+
text: string;
|
|
2486
|
+
threadTs?: string;
|
|
2487
|
+
clientMsgId: string;
|
|
2488
|
+
attachmentAuthorityId?: string;
|
|
2489
|
+
},
|
|
1928
2490
|
): Promise<SlackPostedMessage> {
|
|
1929
2491
|
if (!Number.isSafeInteger(endpointGeneration) || endpointGeneration <= 0)
|
|
1930
2492
|
throw new SlackEndpointBindingError("Slack provider effects require a positive endpoint generation.");
|
|
2493
|
+
const workGeneration = this.#workGeneration;
|
|
1931
2494
|
const initial = await this.#rescheduleAfterEffectTransition(
|
|
1932
2495
|
this.#journal.enqueue({
|
|
1933
2496
|
id,
|
|
@@ -1971,6 +2534,13 @@ export class SlackNotificationDaemon {
|
|
|
1971
2534
|
}
|
|
1972
2535
|
if (!effect) throw new Error("Slack provider effect is owned by another worker");
|
|
1973
2536
|
const lease: ChatEffectLease = { owner: this.#publicationOwnerId, epoch: effect.epoch };
|
|
2537
|
+
const workScope: SlackProviderWorkScope = {
|
|
2538
|
+
sessionId,
|
|
2539
|
+
endpointGeneration,
|
|
2540
|
+
...(payload.attachmentAuthorityId === undefined
|
|
2541
|
+
? {}
|
|
2542
|
+
: { attachmentAuthorityId: payload.attachmentAuthorityId }),
|
|
2543
|
+
};
|
|
1974
2544
|
// A recovered lease may have crossed the provider boundary before its owner
|
|
1975
2545
|
// died, and a fresh post may have reached Slack at a now-superseded generation.
|
|
1976
2546
|
// Reconcile first in all cases. Only an effect that may have crossed the
|
|
@@ -1979,19 +2549,30 @@ export class SlackNotificationDaemon {
|
|
|
1979
2549
|
const requiresReconciliation =
|
|
1980
2550
|
initial.state === "accepted" || initial.state === "uncertain" || initial.state === "leased";
|
|
1981
2551
|
try {
|
|
1982
|
-
const posted = await this.#withEffectLease(
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2552
|
+
const posted = await this.#withEffectLease(
|
|
2553
|
+
id,
|
|
2554
|
+
lease,
|
|
2555
|
+
workScope,
|
|
2556
|
+
async signal => {
|
|
2557
|
+
const found = await this.options.provider.findMessageByClientMsgId({
|
|
2558
|
+
channel: effect!.payload.channel,
|
|
2559
|
+
threadTs: effect!.payload.threadTs,
|
|
2560
|
+
clientMsgId: effect!.payload.clientMsgId,
|
|
2561
|
+
signal,
|
|
2562
|
+
});
|
|
2563
|
+
if (found) return found;
|
|
2564
|
+
if (workGeneration !== this.#workGeneration)
|
|
2565
|
+
throw new Error(`Slack effect ${id} lost its shutdown fence`);
|
|
2566
|
+
if (!(await this.#providerEffectCurrent(effect!))) {
|
|
2567
|
+
if (requiresReconciliation) throw new SlackReconciledAbsentEffectError();
|
|
2568
|
+
throw new SlackStaleEffectError();
|
|
2569
|
+
}
|
|
2570
|
+
if (workGeneration !== this.#workGeneration)
|
|
2571
|
+
throw new Error(`Slack effect ${id} lost its shutdown fence`);
|
|
2572
|
+
return await this.options.provider.postMessage({ ...effect!.payload, signal });
|
|
2573
|
+
},
|
|
2574
|
+
workGeneration,
|
|
2575
|
+
);
|
|
1995
2576
|
|
|
1996
2577
|
if (
|
|
1997
2578
|
!(await this.#journal.record(id, lease, "terminal", {
|
|
@@ -2018,12 +2599,15 @@ export class SlackNotificationDaemon {
|
|
|
2018
2599
|
const reconciled = await this.#withEffectLease(
|
|
2019
2600
|
id,
|
|
2020
2601
|
lease,
|
|
2021
|
-
|
|
2602
|
+
workScope,
|
|
2603
|
+
async signal =>
|
|
2022
2604
|
await this.options.provider.findMessageByClientMsgId({
|
|
2023
2605
|
channel: effect!.payload.channel,
|
|
2024
2606
|
threadTs: effect!.payload.threadTs,
|
|
2025
2607
|
clientMsgId: effect!.payload.clientMsgId,
|
|
2608
|
+
signal,
|
|
2026
2609
|
}),
|
|
2610
|
+
workGeneration,
|
|
2027
2611
|
);
|
|
2028
2612
|
if (reconciled) {
|
|
2029
2613
|
if (
|
|
@@ -2096,7 +2680,18 @@ export class SlackNotificationDaemon {
|
|
|
2096
2680
|
}
|
|
2097
2681
|
|
|
2098
2682
|
async #withPublicationLease<T>(operation: () => Promise<T>, renew: () => Promise<void>): Promise<T> {
|
|
2099
|
-
|
|
2683
|
+
const workGeneration = this.#workGeneration;
|
|
2684
|
+
const fencedRenew = async (): Promise<void> => {
|
|
2685
|
+
if (workGeneration !== this.#workGeneration) throw new Error("Slack publication lost its shutdown fence");
|
|
2686
|
+
await renew();
|
|
2687
|
+
if (workGeneration !== this.#workGeneration) throw new Error("Slack publication lost its shutdown fence");
|
|
2688
|
+
};
|
|
2689
|
+
const result = await this.#withRenewal(async () => {
|
|
2690
|
+
if (workGeneration !== this.#workGeneration) throw new Error("Slack publication lost its shutdown fence");
|
|
2691
|
+
return await operation();
|
|
2692
|
+
}, fencedRenew);
|
|
2693
|
+
if (workGeneration !== this.#workGeneration) throw new Error("Slack publication lost its shutdown fence");
|
|
2694
|
+
return result;
|
|
2100
2695
|
}
|
|
2101
2696
|
|
|
2102
2697
|
async #withRenewal<T>(
|
|
@@ -2191,6 +2786,9 @@ export class SlackNotificationDaemon {
|
|
|
2191
2786
|
channel: payload.channel,
|
|
2192
2787
|
text: payload.text,
|
|
2193
2788
|
clientMsgId,
|
|
2789
|
+
...(current.attachmentAuthorityId === undefined
|
|
2790
|
+
? {}
|
|
2791
|
+
: { attachmentAuthorityId: current.attachmentAuthorityId }),
|
|
2194
2792
|
});
|
|
2195
2793
|
} catch (error) {
|
|
2196
2794
|
if (!(error instanceof SlackStaleEffectError)) throw error;
|
|
@@ -2302,6 +2900,7 @@ export class SlackNotificationDaemon {
|
|
|
2302
2900
|
}
|
|
2303
2901
|
#isDefiniteSdkPreSendFailure(error: unknown): boolean {
|
|
2304
2902
|
if (error instanceof SlackEndpointBindingError) return true;
|
|
2903
|
+
if (error instanceof SessionRouterError) return error.phase === "pre_send";
|
|
2305
2904
|
if (error instanceof SdkClientError) return error.code === "connection_closed";
|
|
2306
2905
|
return (
|
|
2307
2906
|
error instanceof Error &&
|