@gajae-code/coding-agent 0.16.3 → 0.16.6
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 +113 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/args.d.ts +2 -2
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/cli/root-flags.d.ts +4 -2
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +7 -0
- package/dist/types/discovery/claude-plugins.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +35 -0
- package/dist/types/exec/bash-executor.d.ts +17 -1
- package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
- package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-signals.d.ts +2 -0
- package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
- package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
- package/dist/types/exec/bash-shell-worker.d.ts +1 -0
- package/dist/types/exec/isolated-shell.d.ts +19 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
- package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +46 -6
- package/dist/types/extensibility/extensions/types.d.ts +16 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/extensibility/skill-management.d.ts +9 -4
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
- package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/components/settings-selector.d.ts +6 -1
- package/dist/types/modes/components/status-line/command.d.ts +35 -0
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -0
- package/dist/types/modes/components/tool-status-header.d.ts +14 -0
- package/dist/types/modes/controllers/event-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/runtime-mcp/client.d.ts +9 -1
- package/dist/types/runtime-mcp/manager.d.ts +7 -1
- package/dist/types/runtime-mcp/pool.d.ts +22 -3
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/runtime-mcp/types.d.ts +22 -0
- package/dist/types/sdk/broker/ensure.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
- package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
- package/dist/types/sdk/client/client.d.ts +6 -0
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/session-runtime.d.ts +26 -1
- package/dist/types/sdk/lifecycle/service.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +10 -2
- package/dist/types/sdk/session-api-key.d.ts +26 -0
- package/dist/types/sdk/session.d.ts +5 -1
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/setup/paseo/announce.d.ts +112 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +10 -0
- package/dist/types/task/executor.d.ts +5 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +7 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +10 -0
- package/dist/types/tools/path-utils.d.ts +2 -0
- package/dist/types/tools/search.d.ts +6 -0
- package/package.json +9 -7
- package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
- package/scripts/generate-sdk-operation-inventory.ts +11 -0
- package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/args.ts +20 -9
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli/root-flags.ts +5 -3
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +379 -31
- package/src/config/model-preset-registry.ts +120 -19
- package/src/config/model-profiles.ts +40 -0
- package/src/config/model-registry.ts +73 -23
- package/src/config/model-resolver.ts +48 -0
- package/src/config/settings-schema.ts +33 -1
- package/src/config/settings.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +33 -2
- package/src/coordinator-mcp/server.ts +477 -101
- package/src/crash/sanitize.ts +1 -1
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +52 -55
- package/src/discovery/claude-plugins.ts +12 -4
- package/src/discovery/helpers.ts +307 -34
- package/src/exec/bash-executor.ts +330 -50
- package/src/exec/bash-shell-guardian.ts +388 -0
- package/src/exec/bash-shell-runtime-entry.ts +5 -0
- package/src/exec/bash-shell-signals.ts +29 -0
- package/src/exec/bash-shell-supervisor-entry.ts +3 -0
- package/src/exec/bash-shell-supervisor.ts +359 -0
- package/src/exec/bash-shell-worker-entry.ts +5 -0
- package/src/exec/bash-shell-worker-protocol.ts +36 -0
- package/src/exec/bash-shell-worker.ts +116 -0
- package/src/exec/isolated-shell.ts +393 -0
- package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
- package/src/extensibility/extensions/function-hooks.ts +959 -0
- package/src/extensibility/extensions/index.ts +22 -0
- package/src/extensibility/extensions/loader.ts +53 -2
- package/src/extensibility/extensions/runner.ts +895 -264
- package/src/extensibility/extensions/types.ts +42 -0
- package/src/extensibility/extensions/wrapper.ts +56 -15
- package/src/extensibility/gjc-plugins/compiler.ts +228 -60
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/installer.ts +13 -12
- package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
- package/src/extensibility/gjc-plugins/paths.ts +13 -0
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
- package/src/extensibility/gjc-plugins/schema.ts +121 -4
- package/src/extensibility/gjc-plugins/types.ts +25 -0
- package/src/extensibility/hooks/loader.ts +133 -16
- package/src/extensibility/hooks/types.ts +28 -0
- package/src/extensibility/runtime-skill-discovery.ts +149 -190
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +73 -25
- package/src/gjc-runtime/empty-delete-gc.ts +94 -4
- package/src/gjc-runtime/gc-runtime.ts +29 -0
- package/src/gjc-runtime/linux-proc.ts +14 -0
- package/src/gjc-runtime/repository-binding.ts +1 -1
- package/src/gjc-runtime/session-state-lock.ts +143 -10
- package/src/gjc-runtime/session-state-sidecar.ts +27 -11
- package/src/gjc-runtime/state-writer.ts +30 -29
- package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
- package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -0
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +12 -12
- package/src/lsp/config.ts +10 -3
- package/src/lsp/index.ts +1 -1
- package/src/lsp/path-trust.ts +69 -14
- package/src/main.ts +78 -35
- package/src/modes/DESIGN.md +54 -0
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/assistant-message.ts +22 -9
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/model-selector.ts +166 -33
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/settings-selector.ts +1075 -235
- package/src/modes/components/status-line/command.ts +190 -0
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/segments.ts +32 -5
- package/src/modes/components/status-line/types.ts +5 -0
- package/src/modes/components/tool-status-header.ts +396 -22
- package/src/modes/controllers/command-controller.ts +3 -0
- package/src/modes/controllers/event-controller.ts +151 -7
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +53 -6
- package/src/modes/print-mode.ts +5 -1
- package/src/modes/prompt-action-autocomplete.ts +54 -27
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/types.ts +17 -3
- package/src/modes/utils/ui-helpers.ts +2 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/search.md +2 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/client.ts +60 -17
- package/src/runtime-mcp/manager.ts +189 -14
- package/src/runtime-mcp/pool.test.ts +218 -0
- package/src/runtime-mcp/pool.ts +224 -23
- package/src/runtime-mcp/transports/stdio.ts +158 -24
- package/src/runtime-mcp/types.ts +24 -0
- package/src/sdk/broker/broker.ts +124 -32
- package/src/sdk/broker/ensure.ts +248 -41
- package/src/sdk/broker/lifecycle-ledger.ts +7 -17
- package/src/sdk/broker/lifecycle.ts +56 -6
- package/src/sdk/broker/startup-failure.ts +15 -6
- package/src/sdk/bus/chat-daemon-control.ts +3 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
- package/src/sdk/bus/chat-effect-journal.ts +132 -7
- package/src/sdk/bus/conversation-store.ts +29 -0
- package/src/sdk/bus/lifecycle-commands.ts +17 -11
- package/src/sdk/bus/slack-conversation.ts +1 -0
- package/src/sdk/bus/slack-daemon.ts +478 -49
- package/src/sdk/bus/slack-live-provider.ts +12 -0
- package/src/sdk/bus/slack-provider.ts +5 -0
- package/src/sdk/bus/telegram-daemon.ts +16 -2
- package/src/sdk/bus/topic-registry.ts +19 -0
- package/src/sdk/cli/master-cli.ts +1 -1
- package/src/sdk/cli/session-cli.ts +11 -8
- package/src/sdk/client/client.ts +66 -17
- package/src/sdk/host/host.ts +29 -9
- package/src/sdk/host/query/revision-store.ts +19 -13
- package/src/sdk/host/reverse-leases.ts +7 -4
- package/src/sdk/host/session-runtime.test.ts +52 -10
- package/src/sdk/host/session-runtime.ts +383 -44
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +77 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session-api-key.ts +94 -0
- package/src/sdk/session.ts +228 -55
- package/src/sdk/transport/relay.ts +35 -13
- package/src/sdk/transport/serve-cli.ts +3 -3
- package/src/session/agent-session.ts +240 -90
- package/src/session/contribution-prep.ts +214 -21
- package/src/session/fallback-chain-controller.ts +38 -0
- package/src/session/internal/managed-session-storage.ts +19 -5
- package/src/session/messages.ts +5 -0
- package/src/session/session-manager.ts +29 -5
- package/src/session-import/redact.ts +5 -1
- package/src/setup/paseo/announce.ts +456 -0
- package/src/setup/paseo/setup-deps.ts +15 -3
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +29 -3
- package/src/task/executor.ts +7 -1
- package/src/task/index.ts +21 -4
- package/src/tools/bash.ts +63 -19
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +29 -1
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +10 -0
- package/src/tools/job.ts +21 -4
- package/src/tools/monitor.ts +8 -2
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/read.ts +14 -2
- package/src/tools/render-utils.ts +1 -3
- package/src/tools/resource-gc.ts +28 -8
- package/src/tools/search.ts +116 -58
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +37 -2
- package/src/tools/subagent.ts +6 -3
- package/src/tools/tool-catalog.generated.ts +10 -3
- package/src/tools/ultragoal-ask-guard.ts +2 -2
- package/src/web/insane/bridge.ts +113 -23
- package/src/web/search/providers/perplexity.ts +102 -41
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { logger } from "@gajae-code/utils";
|
|
3
|
-
import {
|
|
3
|
+
import { SdkPreparedDispatchError } from "../client/client";
|
|
4
4
|
import { type SessionAttachment, SessionRouterError } from "../router";
|
|
5
5
|
|
|
6
6
|
import type { ChatDeliveryError } from "./chat-daemon-runtime";
|
|
7
|
-
import { ConversationStore } from "./conversation-store";
|
|
7
|
+
import { ConversationLockTimeoutError, ConversationStore } from "./conversation-store";
|
|
8
8
|
import {
|
|
9
9
|
acceptsSlackInbound,
|
|
10
10
|
normalizeSlackConversation,
|
|
@@ -58,7 +58,9 @@ function slackPublicationClientMsgId(publicationId: string): string {
|
|
|
58
58
|
|
|
59
59
|
// Durable filesystem publication leases must outlast one event-loop and persistence turn.
|
|
60
60
|
const MIN_PUBLICATION_LEASE_MS = 100;
|
|
61
|
+
const INBOUND_REACTION_DEADLINE_MS = 5_000;
|
|
61
62
|
const RETIREMENT_DRAIN_MS = 5_000;
|
|
63
|
+
const INBOUND_REACTION = "eyes";
|
|
62
64
|
|
|
63
65
|
type SlackProviderWorkScope = {
|
|
64
66
|
sessionId: string;
|
|
@@ -70,6 +72,8 @@ type SlackProviderWork = {
|
|
|
70
72
|
scopeKey: string;
|
|
71
73
|
invalidate(): Promise<void>;
|
|
72
74
|
abort(): void;
|
|
75
|
+
/** Best-effort work is aborted before the graceful shutdown drain. */
|
|
76
|
+
abortOnStop?: boolean;
|
|
73
77
|
terminalize(): Promise<void>;
|
|
74
78
|
settled: Promise<void>;
|
|
75
79
|
};
|
|
@@ -113,6 +117,8 @@ export interface SlackNotificationDaemonOptions {
|
|
|
113
117
|
content: string,
|
|
114
118
|
attachment: SessionAttachment,
|
|
115
119
|
idempotencyKey: string,
|
|
120
|
+
beforeDispatch: () => void,
|
|
121
|
+
dispatchFence: <T>(dispatch: () => Promise<T>) => Promise<T>,
|
|
116
122
|
) => Promise<boolean>;
|
|
117
123
|
}
|
|
118
124
|
|
|
@@ -221,6 +227,10 @@ export class SlackNotificationDaemon {
|
|
|
221
227
|
readonly #activeWork = new Set<Promise<unknown>>();
|
|
222
228
|
readonly #workInvalidators = new Set<SlackProviderWork>();
|
|
223
229
|
readonly #retiredProviderWorkScopes = new Map<string, SlackRetiredProviderWorkScope>();
|
|
230
|
+
readonly #retiringAttachmentGenerations = new Set<string>();
|
|
231
|
+
readonly #closingSessions = new Map<string, number>();
|
|
232
|
+
readonly #closingInboundRoots = new Map<string, number>();
|
|
233
|
+
readonly #retiredInboundRoots = new Set<string>();
|
|
224
234
|
readonly #rollovers = new Map<string, Promise<SlackRootPublication>>();
|
|
225
235
|
#workGeneration = 0;
|
|
226
236
|
#lifecycleTail: Promise<void> = Promise.resolve();
|
|
@@ -362,6 +372,7 @@ export class SlackNotificationDaemon {
|
|
|
362
372
|
this.#startedGeneration = undefined;
|
|
363
373
|
if (this.#providerRunningGeneration === lifecycleGeneration) this.#providerRunningGeneration = undefined;
|
|
364
374
|
this.#clearLeaseRecoveryTimer();
|
|
375
|
+
for (const work of this.#workInvalidators) if (work.abortOnStop) work.abort();
|
|
365
376
|
if (stopProvider) this.#providerStopRequestedGenerations.add(lifecycleGeneration);
|
|
366
377
|
const providerStop = stopProvider ? Promise.resolve().then(() => this.options.provider.stop()) : undefined;
|
|
367
378
|
const providerStopResult = providerStop?.then(
|
|
@@ -449,6 +460,7 @@ export class SlackNotificationDaemon {
|
|
|
449
460
|
|
|
450
461
|
/** Accepted inbound effects are durably claimed before their Socket Mode ACK. */
|
|
451
462
|
async handleEnvelope(envelope: SlackSocketEnvelope): Promise<boolean> {
|
|
463
|
+
const lifecycleGeneration = this.#lifecycleGeneration;
|
|
452
464
|
if (!text(envelope.envelope_id)) return false;
|
|
453
465
|
const inbound = messageFromEnvelope(envelope.payload);
|
|
454
466
|
if (!inbound || inbound.teamId !== this.options.teamId || inbound.channelId !== this.options.channelId) {
|
|
@@ -479,6 +491,15 @@ export class SlackNotificationDaemon {
|
|
|
479
491
|
this.#inflightInbound.add(inflightKey);
|
|
480
492
|
try {
|
|
481
493
|
await this.options.provider.ack(envelope.envelope_id);
|
|
494
|
+
const messageTs = text(inbound.event.ts);
|
|
495
|
+
if (messageTs)
|
|
496
|
+
this.#scheduleInboundReaction(
|
|
497
|
+
claim.sessionId,
|
|
498
|
+
claim.endpoint,
|
|
499
|
+
inbound.channelId,
|
|
500
|
+
messageTs,
|
|
501
|
+
lifecycleGeneration,
|
|
502
|
+
);
|
|
482
503
|
return await this.#dispatchInbound(claim);
|
|
483
504
|
} finally {
|
|
484
505
|
this.#inflightInbound.delete(inflightKey);
|
|
@@ -491,6 +512,76 @@ export class SlackNotificationDaemon {
|
|
|
491
512
|
return work.finally(() => this.#activeWork.delete(work));
|
|
492
513
|
}
|
|
493
514
|
|
|
515
|
+
/** Track the best-effort acknowledgement in the owning attachment's provider-work scope. */
|
|
516
|
+
#scheduleInboundReaction(
|
|
517
|
+
sessionId: string,
|
|
518
|
+
endpoint: SessionAttachment,
|
|
519
|
+
channel: string,
|
|
520
|
+
timestamp: string,
|
|
521
|
+
lifecycleGeneration: number,
|
|
522
|
+
): void {
|
|
523
|
+
const scope: SlackProviderWorkScope = {
|
|
524
|
+
sessionId,
|
|
525
|
+
endpointGeneration: endpoint.generation,
|
|
526
|
+
...(endpoint.authorityId === undefined ? {} : { attachmentAuthorityId: endpoint.authorityId }),
|
|
527
|
+
};
|
|
528
|
+
const scopeKey = slackProviderWorkScopeKey(scope);
|
|
529
|
+
const workGeneration = this.#workGeneration;
|
|
530
|
+
const controller = new AbortController();
|
|
531
|
+
const settled = Promise.withResolvers<void>();
|
|
532
|
+
let abortKind: "deadline" | "lifecycle" | undefined;
|
|
533
|
+
const abort = (kind: "deadline" | "lifecycle"): void => {
|
|
534
|
+
abortKind ??= kind;
|
|
535
|
+
controller.abort();
|
|
536
|
+
};
|
|
537
|
+
const work: SlackProviderWork = {
|
|
538
|
+
scopeKey,
|
|
539
|
+
invalidate: async () => abort("lifecycle"),
|
|
540
|
+
abort: () => abort("lifecycle"),
|
|
541
|
+
abortOnStop: true,
|
|
542
|
+
terminalize: async () => abort("lifecycle"),
|
|
543
|
+
settled: settled.promise,
|
|
544
|
+
};
|
|
545
|
+
this.#workInvalidators.add(work);
|
|
546
|
+
const reaction = (async () => {
|
|
547
|
+
const deadline = setTimeout(() => abort("deadline"), INBOUND_REACTION_DEADLINE_MS);
|
|
548
|
+
try {
|
|
549
|
+
if (
|
|
550
|
+
workGeneration !== this.#workGeneration ||
|
|
551
|
+
lifecycleGeneration !== this.#lifecycleGeneration ||
|
|
552
|
+
this.#retiredProviderWorkScopes.has(scopeKey) ||
|
|
553
|
+
!endpoint.isCurrent()
|
|
554
|
+
) {
|
|
555
|
+
abort("lifecycle");
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
await this.options.provider.addReaction({
|
|
559
|
+
channel,
|
|
560
|
+
timestamp,
|
|
561
|
+
name: INBOUND_REACTION,
|
|
562
|
+
signal: controller.signal,
|
|
563
|
+
});
|
|
564
|
+
if (abortKind === "deadline")
|
|
565
|
+
logger.warn(
|
|
566
|
+
`Slack inbound acknowledgement reaction exceeded the ${INBOUND_REACTION_DEADLINE_MS}ms deadline and was aborted.`,
|
|
567
|
+
);
|
|
568
|
+
} catch {
|
|
569
|
+
if (abortKind !== "lifecycle")
|
|
570
|
+
logger.warn(
|
|
571
|
+
abortKind === "deadline"
|
|
572
|
+
? `Slack inbound acknowledgement reaction exceeded the ${INBOUND_REACTION_DEADLINE_MS}ms deadline and was aborted.`
|
|
573
|
+
: "Slack inbound acknowledgement reaction failed.",
|
|
574
|
+
);
|
|
575
|
+
} finally {
|
|
576
|
+
clearTimeout(deadline);
|
|
577
|
+
this.#workInvalidators.delete(work);
|
|
578
|
+
settled.resolve();
|
|
579
|
+
this.#tryReleaseRetiredProviderWorkScope(scopeKey);
|
|
580
|
+
}
|
|
581
|
+
})();
|
|
582
|
+
void this.#track(reaction);
|
|
583
|
+
}
|
|
584
|
+
|
|
494
585
|
async #invalidateActiveWork(): Promise<void> {
|
|
495
586
|
this.#workGeneration += 1;
|
|
496
587
|
await Promise.allSettled([...this.#workInvalidators].map(work => work.invalidate()));
|
|
@@ -597,7 +688,9 @@ export class SlackNotificationDaemon {
|
|
|
597
688
|
}
|
|
598
689
|
|
|
599
690
|
async postRoot(sessionId: string, body: string, endpointGeneration?: number): Promise<SlackConversation> {
|
|
600
|
-
|
|
691
|
+
const conversation = (await this.#postRoot(sessionId, body, endpointGeneration)).conversation;
|
|
692
|
+
this.#admitActivatedRoot(conversation);
|
|
693
|
+
return conversation;
|
|
601
694
|
}
|
|
602
695
|
|
|
603
696
|
/**
|
|
@@ -631,7 +724,7 @@ export class SlackNotificationDaemon {
|
|
|
631
724
|
authority.attachmentAuthorityId ??
|
|
632
725
|
(attachment?.generation === authority.endpointGeneration ? attachment.authorityId : undefined);
|
|
633
726
|
await this.#verifyExistingRoot(rootTs);
|
|
634
|
-
|
|
727
|
+
const conversation = await claimSlackThreadBinding({
|
|
635
728
|
store: this.store,
|
|
636
729
|
key: this.#intentKey(sessionId),
|
|
637
730
|
teamId: this.options.teamId,
|
|
@@ -652,6 +745,8 @@ export class SlackNotificationDaemon {
|
|
|
652
745
|
},
|
|
653
746
|
now: this.#now,
|
|
654
747
|
});
|
|
748
|
+
this.#admitActivatedRoot(conversation);
|
|
749
|
+
return conversation;
|
|
655
750
|
}
|
|
656
751
|
|
|
657
752
|
/**
|
|
@@ -883,6 +978,7 @@ export class SlackNotificationDaemon {
|
|
|
883
978
|
? await this.#resumeWithRootPublication(sessionId, body, generation, rootPublication?.clientMsgId)
|
|
884
979
|
: await this.#postRoot(sessionId, body, generation, rootPublication?.clientMsgId);
|
|
885
980
|
conversation = publication.conversation;
|
|
981
|
+
this.#admitActivatedRoot(conversation);
|
|
886
982
|
bodyWasUsedAsRoot ||= publication.created;
|
|
887
983
|
}
|
|
888
984
|
if (!conversation.rootTs) return conversation;
|
|
@@ -1045,13 +1141,68 @@ export class SlackNotificationDaemon {
|
|
|
1045
1141
|
}
|
|
1046
1142
|
|
|
1047
1143
|
async close(sessionId: string, marker = "Session closed.", endpointGeneration?: number): Promise<boolean> {
|
|
1048
|
-
return await this.#track(this.#
|
|
1144
|
+
return await this.#track(this.#closeWithFence(sessionId, marker, true, endpointGeneration));
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
async #closeWithFence(
|
|
1148
|
+
sessionId: string,
|
|
1149
|
+
marker: string,
|
|
1150
|
+
allowRemovedAttachment: boolean,
|
|
1151
|
+
endpointGeneration?: number,
|
|
1152
|
+
): Promise<boolean> {
|
|
1153
|
+
this.#incrementFence(this.#closingSessions, sessionId);
|
|
1154
|
+
let closingRootKey: string | undefined;
|
|
1155
|
+
try {
|
|
1156
|
+
return await this.#close(sessionId, marker, allowRemovedAttachment, endpointGeneration, record => {
|
|
1157
|
+
closingRootKey = `${sessionId}\u0000${this.#requireEndpointGeneration(record)}\u0000${record.rootTs}`;
|
|
1158
|
+
this.#incrementFence(this.#closingInboundRoots, closingRootKey);
|
|
1159
|
+
this.#decrementFence(this.#closingSessions, sessionId);
|
|
1160
|
+
});
|
|
1161
|
+
} finally {
|
|
1162
|
+
if (closingRootKey) this.#decrementFence(this.#closingInboundRoots, closingRootKey);
|
|
1163
|
+
else this.#decrementFence(this.#closingSessions, sessionId);
|
|
1164
|
+
}
|
|
1049
1165
|
}
|
|
1050
1166
|
|
|
1051
1167
|
async recoverCleanup(sessionId: string, endpointGeneration: number, authorityId?: string): Promise<boolean> {
|
|
1052
1168
|
const found = await this.findSession(sessionId, true);
|
|
1053
1169
|
if (!found) return true;
|
|
1054
|
-
if (found.record.endpointGeneration !== endpointGeneration)
|
|
1170
|
+
if (found.record.endpointGeneration !== endpointGeneration) {
|
|
1171
|
+
const effectId = found.record.cleanupEffectId;
|
|
1172
|
+
if (effectId?.startsWith("close-marker:")) {
|
|
1173
|
+
let inboundDispatches: SlackInboundDispatchReceipt[] = [];
|
|
1174
|
+
const closed = await this.store.transact(found.key, current => {
|
|
1175
|
+
if (
|
|
1176
|
+
current?.state !== "active" ||
|
|
1177
|
+
current.sessionId !== sessionId ||
|
|
1178
|
+
current.cleanupEffectId !== effectId
|
|
1179
|
+
)
|
|
1180
|
+
return current;
|
|
1181
|
+
inboundDispatches = [...(current.inboundDispatches ?? [])];
|
|
1182
|
+
return nextRecord(current, {
|
|
1183
|
+
state: "closed_marker",
|
|
1184
|
+
cleanupEffectId: undefined,
|
|
1185
|
+
pendingActionId: undefined,
|
|
1186
|
+
inboundDispatches: [],
|
|
1187
|
+
updatedAt: this.#now(),
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
1190
|
+
if (closed?.state === "closed_marker") {
|
|
1191
|
+
if (found.record.rootTs)
|
|
1192
|
+
this.#retireInboundRoot(
|
|
1193
|
+
`${sessionId}\u0000${this.#requireEndpointGeneration(found.record)}\u0000${found.record.rootTs}`,
|
|
1194
|
+
);
|
|
1195
|
+
await this.#journal.terminalizeDecided(effectId, { status: "stale_binding" });
|
|
1196
|
+
if (inboundDispatches.length > 0)
|
|
1197
|
+
await this.#retirementTerminalization(
|
|
1198
|
+
inboundDispatches.map(receipt =>
|
|
1199
|
+
this.#terminalizeInboundEffect(receipt.effectId, "stale_binding"),
|
|
1200
|
+
),
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1055
1206
|
if (found.record.state === "closed_marker" || found.record.state === "error") return true;
|
|
1056
1207
|
if (
|
|
1057
1208
|
(found.record.state === "posting_root" || found.record.state === "resumed_root") &&
|
|
@@ -1061,10 +1212,14 @@ export class SlackNotificationDaemon {
|
|
|
1061
1212
|
if (found.record.state !== "active") return false;
|
|
1062
1213
|
if (!found.record.rootTs) return false;
|
|
1063
1214
|
if (found.record.attachmentAuthorityId === authorityId && found.record.cleanupEffectId === undefined) return true;
|
|
1064
|
-
const effectId =
|
|
1065
|
-
if (
|
|
1215
|
+
const effectId = found.record.cleanupEffectId;
|
|
1216
|
+
if (!effectId) return false;
|
|
1066
1217
|
const effect = await this.#journal.read(effectId);
|
|
1067
|
-
if (!effect
|
|
1218
|
+
if (!effect) {
|
|
1219
|
+
await this.close(sessionId, "Session closed.", endpointGeneration);
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
if (effect.state === "terminal" && !effect.receipt?.messageId && !effect.receipt?.timestamp) {
|
|
1068
1223
|
await this.store.transact(found.key, current =>
|
|
1069
1224
|
current?.cleanupEffectId === effectId
|
|
1070
1225
|
? nextRecord(current, { cleanupEffectId: undefined, updatedAt: this.#now() })
|
|
@@ -1077,8 +1232,19 @@ export class SlackNotificationDaemon {
|
|
|
1077
1232
|
}
|
|
1078
1233
|
|
|
1079
1234
|
async retireAttachment(sessionId: string, endpointGeneration: number): Promise<void> {
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1235
|
+
const generationKey = `${sessionId}\u0000${endpointGeneration}`;
|
|
1236
|
+
this.#retiringAttachmentGenerations.add(generationKey);
|
|
1237
|
+
let found: { key: string; record: SlackConversation } | undefined;
|
|
1238
|
+
try {
|
|
1239
|
+
found = await this.findSession(sessionId, true);
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
this.#retiringAttachmentGenerations.delete(generationKey);
|
|
1242
|
+
throw error;
|
|
1243
|
+
}
|
|
1244
|
+
if (found?.record.endpointGeneration !== endpointGeneration) {
|
|
1245
|
+
this.#retiringAttachmentGenerations.delete(generationKey);
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1082
1248
|
const attachmentAuthorityId = found.record.attachmentAuthorityId;
|
|
1083
1249
|
const scope: SlackProviderWorkScope = {
|
|
1084
1250
|
sessionId,
|
|
@@ -1089,6 +1255,7 @@ export class SlackNotificationDaemon {
|
|
|
1089
1255
|
let releaseScope = false;
|
|
1090
1256
|
try {
|
|
1091
1257
|
const draining = this.#invalidateAndDrainProviderWork(scope, deadline);
|
|
1258
|
+
releaseScope = true;
|
|
1092
1259
|
let inboundDispatches: SlackInboundDispatchReceipt[] = [];
|
|
1093
1260
|
// Persist the mapping fence before waiting on terminal effects. A detached
|
|
1094
1261
|
// terminalization can then never be replayed through this attachment.
|
|
@@ -1108,11 +1275,10 @@ export class SlackNotificationDaemon {
|
|
|
1108
1275
|
updatedAt: this.#now(),
|
|
1109
1276
|
});
|
|
1110
1277
|
});
|
|
1111
|
-
releaseScope = true;
|
|
1112
1278
|
await draining;
|
|
1113
1279
|
if (inboundDispatches.length === 0) return;
|
|
1114
1280
|
const terminalization = this.#retirementTerminalization(
|
|
1115
|
-
inboundDispatches.map(receipt => this.#
|
|
1281
|
+
inboundDispatches.map(receipt => this.#terminalizeInboundEffect(receipt.effectId, "stale_binding")),
|
|
1116
1282
|
);
|
|
1117
1283
|
this.#trackRetirementTerminalization(slackProviderWorkScopeKey(scope), terminalization);
|
|
1118
1284
|
const remaining = deadline - this.#now();
|
|
@@ -1128,6 +1294,7 @@ export class SlackNotificationDaemon {
|
|
|
1128
1294
|
logger.warn("Slack attachment retirement exceeded the 5000ms deadline; fenced pending inbound effects.");
|
|
1129
1295
|
} finally {
|
|
1130
1296
|
if (releaseScope) this.#releaseRetiredProviderWorkScope(scope);
|
|
1297
|
+
this.#retiringAttachmentGenerations.delete(generationKey);
|
|
1131
1298
|
}
|
|
1132
1299
|
}
|
|
1133
1300
|
|
|
@@ -1136,13 +1303,17 @@ export class SlackNotificationDaemon {
|
|
|
1136
1303
|
marker: string,
|
|
1137
1304
|
allowRemovedAttachment: boolean,
|
|
1138
1305
|
expectedEndpointGeneration?: number,
|
|
1306
|
+
onResolved?: (record: SlackConversation) => void,
|
|
1139
1307
|
): Promise<boolean> {
|
|
1140
1308
|
const found = await this.findSession(sessionId, true);
|
|
1141
1309
|
if (!found?.record.rootTs || found.record.state !== "active") return false;
|
|
1142
1310
|
if (expectedEndpointGeneration !== undefined && found.record.endpointGeneration !== expectedEndpointGeneration)
|
|
1143
1311
|
return false;
|
|
1144
|
-
|
|
1145
|
-
|
|
1312
|
+
onResolved?.(found.record);
|
|
1313
|
+
const effectId =
|
|
1314
|
+
found.record.cleanupEffectId ??
|
|
1315
|
+
`${allowRemovedAttachment ? "close-marker-cleanup" : "close-marker"}:${sessionId}:${found.record.clientMsgId ?? found.record.rootTs}`;
|
|
1316
|
+
{
|
|
1146
1317
|
let admitted = false;
|
|
1147
1318
|
await this.store.transact(found.key, current => {
|
|
1148
1319
|
if (
|
|
@@ -1183,6 +1354,7 @@ export class SlackNotificationDaemon {
|
|
|
1183
1354
|
);
|
|
1184
1355
|
await this.#postDurable(effectId, sessionId, this.#requireEndpointGeneration(found.record), durable.payload);
|
|
1185
1356
|
let closed = false;
|
|
1357
|
+
let inboundDispatches: SlackInboundDispatchReceipt[] = [];
|
|
1186
1358
|
await this.store.transact(found.key, current => {
|
|
1187
1359
|
if (
|
|
1188
1360
|
current?.state !== "active" ||
|
|
@@ -1194,13 +1366,24 @@ export class SlackNotificationDaemon {
|
|
|
1194
1366
|
)
|
|
1195
1367
|
return current;
|
|
1196
1368
|
closed = true;
|
|
1369
|
+
inboundDispatches = [...(current.inboundDispatches ?? [])];
|
|
1197
1370
|
return nextRecord(current, {
|
|
1198
1371
|
state: "closed_marker",
|
|
1199
1372
|
cleanupEffectId: undefined,
|
|
1200
1373
|
pendingActionId: undefined,
|
|
1374
|
+
inboundDispatches: [],
|
|
1201
1375
|
updatedAt: this.#now(),
|
|
1202
1376
|
});
|
|
1203
1377
|
});
|
|
1378
|
+
if (closed) {
|
|
1379
|
+
this.#retireInboundRoot(
|
|
1380
|
+
`${sessionId}\u0000${this.#requireEndpointGeneration(found.record)}\u0000${found.record.rootTs}`,
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
if (closed && inboundDispatches.length > 0)
|
|
1384
|
+
await this.#retirementTerminalization(
|
|
1385
|
+
inboundDispatches.map(receipt => this.#terminalizeInboundEffect(receipt.effectId, "stale_binding")),
|
|
1386
|
+
);
|
|
1204
1387
|
|
|
1205
1388
|
return closed;
|
|
1206
1389
|
}
|
|
@@ -1211,7 +1394,9 @@ export class SlackNotificationDaemon {
|
|
|
1211
1394
|
endpointGeneration?: number,
|
|
1212
1395
|
publicationId?: string,
|
|
1213
1396
|
): Promise<SlackConversation> {
|
|
1214
|
-
|
|
1397
|
+
const conversation = await this.#track(this.#resume(sessionId, body, endpointGeneration, publicationId));
|
|
1398
|
+
this.#admitActivatedRoot(conversation);
|
|
1399
|
+
return conversation;
|
|
1215
1400
|
}
|
|
1216
1401
|
|
|
1217
1402
|
async #resume(
|
|
@@ -1330,7 +1515,7 @@ export class SlackNotificationDaemon {
|
|
|
1330
1515
|
}
|
|
1331
1516
|
let closed = false;
|
|
1332
1517
|
try {
|
|
1333
|
-
closed = await this.#track(this.#
|
|
1518
|
+
closed = await this.#track(this.#closeWithFence(sessionId, "Session closed.", false, previousGeneration));
|
|
1334
1519
|
} catch (error) {
|
|
1335
1520
|
if (!(error instanceof SlackStaleEffectError)) throw error;
|
|
1336
1521
|
// The old-generation close marker was deliberately suppressed after
|
|
@@ -1496,7 +1681,11 @@ export class SlackNotificationDaemon {
|
|
|
1496
1681
|
updatedAt: this.#now(),
|
|
1497
1682
|
});
|
|
1498
1683
|
});
|
|
1499
|
-
|
|
1684
|
+
if (!sessionId || !receipt) {
|
|
1685
|
+
await this.#terminalizeInboundEffect(effectId, "rejected");
|
|
1686
|
+
return undefined;
|
|
1687
|
+
}
|
|
1688
|
+
return { key, endpoint, sessionId, receipt };
|
|
1500
1689
|
}
|
|
1501
1690
|
|
|
1502
1691
|
async #dispatchInbound(claim: {
|
|
@@ -1512,6 +1701,7 @@ export class SlackNotificationDaemon {
|
|
|
1512
1701
|
await this.#finalizeTerminalInboundDispatch(claim.key, claim.receipt);
|
|
1513
1702
|
return false;
|
|
1514
1703
|
}
|
|
1704
|
+
if (effect?.state === "uncertain") return false;
|
|
1515
1705
|
if (
|
|
1516
1706
|
!current ||
|
|
1517
1707
|
!this.#mappedInboundDispatchable(current, claim.receipt, effect) ||
|
|
@@ -1548,18 +1738,40 @@ export class SlackNotificationDaemon {
|
|
|
1548
1738
|
? {}
|
|
1549
1739
|
: { attachmentAuthorityId: claim.receipt.attachmentAuthorityId }),
|
|
1550
1740
|
};
|
|
1741
|
+
let dispatchEntered = false;
|
|
1551
1742
|
try {
|
|
1552
1743
|
if (effect.payload.type === "command") {
|
|
1553
1744
|
const payload = effect.payload;
|
|
1554
|
-
const accepted = await this.#withEffectLease(
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
claim.
|
|
1560
|
-
payload.
|
|
1561
|
-
|
|
1562
|
-
|
|
1745
|
+
const accepted = await this.#withEffectLease(
|
|
1746
|
+
effect.id,
|
|
1747
|
+
lease,
|
|
1748
|
+
workScope,
|
|
1749
|
+
async () => {
|
|
1750
|
+
if (!(await this.#inboundEffectCurrent(claim, effect.id))) throw new SlackStaleEffectError();
|
|
1751
|
+
if (!this.#inboundEffectAdmitted(claim, payload.routing.rootTs)) throw new SlackStaleEffectError();
|
|
1752
|
+
if (!(await this.#journal.markDispatching(effect.id, lease)))
|
|
1753
|
+
throw new Error("Slack inbound command dispatch boundary could not be persisted");
|
|
1754
|
+
if (
|
|
1755
|
+
!(await this.#inboundEffectCurrent(claim, effect.id)) ||
|
|
1756
|
+
!this.#inboundEffectAdmitted(claim, payload.routing.rootTs) ||
|
|
1757
|
+
!(await this.#journal.ownsDispatching(effect.id, lease)) ||
|
|
1758
|
+
!this.#inboundEffectAdmitted(claim, payload.routing.rootTs)
|
|
1759
|
+
)
|
|
1760
|
+
throw new SlackStaleEffectError();
|
|
1761
|
+
const boundary = this.#durableDispatchBoundary(claim, payload.routing.rootTs, effect.id, lease);
|
|
1762
|
+
dispatchEntered = true;
|
|
1763
|
+
return await (this.options.onCommand?.(
|
|
1764
|
+
claim.sessionId,
|
|
1765
|
+
payload.content,
|
|
1766
|
+
claim.endpoint,
|
|
1767
|
+
payload.idempotencyKey,
|
|
1768
|
+
boundary.beforeDispatch,
|
|
1769
|
+
boundary.dispatchFence,
|
|
1770
|
+
) ?? Promise.resolve(false));
|
|
1771
|
+
},
|
|
1772
|
+
this.#workGeneration,
|
|
1773
|
+
true,
|
|
1774
|
+
);
|
|
1563
1775
|
if (!(await this.#inboundEffectCurrent(claim, effect.id))) {
|
|
1564
1776
|
await this.#terminalizeStaleInboundDispatch(claim.key, claim.receipt, "stale_binding");
|
|
1565
1777
|
return false;
|
|
@@ -1570,10 +1782,30 @@ export class SlackNotificationDaemon {
|
|
|
1570
1782
|
if (recorded) await this.#finishDispatch(claim, "terminal");
|
|
1571
1783
|
return accepted && !!recorded;
|
|
1572
1784
|
}
|
|
1573
|
-
await this.#withEffectLease(
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1785
|
+
await this.#withEffectLease(
|
|
1786
|
+
effect.id,
|
|
1787
|
+
lease,
|
|
1788
|
+
workScope,
|
|
1789
|
+
async () => {
|
|
1790
|
+
if (!(await this.#inboundEffectCurrent(claim, effect.id))) throw new SlackStaleEffectError();
|
|
1791
|
+
if (!this.#inboundEffectAdmitted(claim, effect.payload.routing.rootTs))
|
|
1792
|
+
throw new SlackStaleEffectError();
|
|
1793
|
+
if (!(await this.#journal.markDispatching(effect.id, lease)))
|
|
1794
|
+
throw new Error("Slack inbound SDK dispatch boundary could not be persisted");
|
|
1795
|
+
if (
|
|
1796
|
+
!(await this.#inboundEffectCurrent(claim, effect.id)) ||
|
|
1797
|
+
!this.#inboundEffectAdmitted(claim, effect.payload.routing.rootTs) ||
|
|
1798
|
+
!(await this.#journal.ownsDispatching(effect.id, lease)) ||
|
|
1799
|
+
!this.#inboundEffectAdmitted(claim, effect.payload.routing.rootTs)
|
|
1800
|
+
)
|
|
1801
|
+
throw new SlackStaleEffectError();
|
|
1802
|
+
const boundary = this.#durableDispatchBoundary(claim, effect.payload.routing.rootTs, effect.id, lease);
|
|
1803
|
+
dispatchEntered = true;
|
|
1804
|
+
await claim.endpoint.send(effect.payload, boundary);
|
|
1805
|
+
},
|
|
1806
|
+
this.#workGeneration,
|
|
1807
|
+
true,
|
|
1808
|
+
);
|
|
1577
1809
|
if (!(await this.#inboundEffectCurrent(claim, effect.id))) {
|
|
1578
1810
|
await this.#terminalizeStaleInboundDispatch(claim.key, claim.receipt, "stale_binding");
|
|
1579
1811
|
return false;
|
|
@@ -1586,7 +1818,7 @@ export class SlackNotificationDaemon {
|
|
|
1586
1818
|
await this.#terminalizeStaleInboundDispatch(claim.key, claim.receipt, "stale_binding");
|
|
1587
1819
|
return false;
|
|
1588
1820
|
}
|
|
1589
|
-
const state = this.#isDefiniteSdkPreSendFailure(error) ? "accepted" : "uncertain";
|
|
1821
|
+
const state = this.#isDefiniteSdkPreSendFailure(error) || !dispatchEntered ? "accepted" : "uncertain";
|
|
1590
1822
|
|
|
1591
1823
|
const recorded = await this.#rescheduleAfterEffectTransition(
|
|
1592
1824
|
this.#journal.record(effect.id, lease, state, { status: state }),
|
|
@@ -1602,6 +1834,18 @@ export class SlackNotificationDaemon {
|
|
|
1602
1834
|
|
|
1603
1835
|
effectId: string,
|
|
1604
1836
|
): Promise<boolean> {
|
|
1837
|
+
if (
|
|
1838
|
+
this.#retiredProviderWorkScopes.has(
|
|
1839
|
+
slackProviderWorkScopeKey({
|
|
1840
|
+
sessionId: claim.sessionId,
|
|
1841
|
+
endpointGeneration: claim.receipt.endpointGeneration,
|
|
1842
|
+
...(claim.receipt.attachmentAuthorityId === undefined
|
|
1843
|
+
? {}
|
|
1844
|
+
: { attachmentAuthorityId: claim.receipt.attachmentAuthorityId }),
|
|
1845
|
+
}),
|
|
1846
|
+
)
|
|
1847
|
+
)
|
|
1848
|
+
return false;
|
|
1605
1849
|
const endpoint = await this.#resolveAttachment(claim.sessionId);
|
|
1606
1850
|
if (!endpoint?.isCurrent()) return false;
|
|
1607
1851
|
if (
|
|
@@ -1628,12 +1872,120 @@ export class SlackNotificationDaemon {
|
|
|
1628
1872
|
);
|
|
1629
1873
|
}
|
|
1630
1874
|
|
|
1875
|
+
#inboundEffectAdmitted(
|
|
1876
|
+
claim: {
|
|
1877
|
+
endpoint: SessionAttachment;
|
|
1878
|
+
sessionId: string;
|
|
1879
|
+
receipt: SlackInboundDispatchReceipt;
|
|
1880
|
+
},
|
|
1881
|
+
rootTs: string,
|
|
1882
|
+
): boolean {
|
|
1883
|
+
return (
|
|
1884
|
+
claim.endpoint.isCurrent() &&
|
|
1885
|
+
!this.#closingSessions.has(claim.sessionId) &&
|
|
1886
|
+
!this.#closingInboundRoots.has(`${claim.sessionId}\u0000${claim.receipt.endpointGeneration}\u0000${rootTs}`) &&
|
|
1887
|
+
!this.#retiredInboundRoots.has(`${claim.sessionId}\u0000${claim.receipt.endpointGeneration}\u0000${rootTs}`) &&
|
|
1888
|
+
!this.#retiringAttachmentGenerations.has(`${claim.sessionId}\u0000${claim.receipt.endpointGeneration}`) &&
|
|
1889
|
+
!this.#retiredProviderWorkScopes.has(
|
|
1890
|
+
slackProviderWorkScopeKey({
|
|
1891
|
+
sessionId: claim.sessionId,
|
|
1892
|
+
endpointGeneration: claim.receipt.endpointGeneration,
|
|
1893
|
+
...(claim.receipt.attachmentAuthorityId === undefined
|
|
1894
|
+
? {}
|
|
1895
|
+
: { attachmentAuthorityId: claim.receipt.attachmentAuthorityId }),
|
|
1896
|
+
}),
|
|
1897
|
+
)
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
#durableDispatchBoundary(
|
|
1902
|
+
claim: {
|
|
1903
|
+
key: string;
|
|
1904
|
+
endpoint: SessionAttachment;
|
|
1905
|
+
sessionId: string;
|
|
1906
|
+
receipt: SlackInboundDispatchReceipt;
|
|
1907
|
+
},
|
|
1908
|
+
rootTs: string,
|
|
1909
|
+
effectId: string,
|
|
1910
|
+
lease: ChatEffectLease,
|
|
1911
|
+
): {
|
|
1912
|
+
beforeDispatch: () => void;
|
|
1913
|
+
dispatchFence: <T>(dispatch: () => Promise<T>) => Promise<T>;
|
|
1914
|
+
} {
|
|
1915
|
+
const wire = Promise.withResolvers<void>();
|
|
1916
|
+
let wireReached = false;
|
|
1917
|
+
return {
|
|
1918
|
+
beforeDispatch: () => {
|
|
1919
|
+
if (!this.#inboundEffectAdmitted(claim, rootTs)) throw new SlackStaleEffectError();
|
|
1920
|
+
wireReached = true;
|
|
1921
|
+
wire.resolve();
|
|
1922
|
+
},
|
|
1923
|
+
dispatchFence: async dispatch => {
|
|
1924
|
+
const result = await (async () => {
|
|
1925
|
+
try {
|
|
1926
|
+
return await this.store.dispatchWithSnapshotFence(claim.key, current => {
|
|
1927
|
+
if (
|
|
1928
|
+
!current ||
|
|
1929
|
+
current.sessionId !== claim.sessionId ||
|
|
1930
|
+
!acceptsSlackInbound(current, rootTs, claim.receipt.endpointGeneration) ||
|
|
1931
|
+
current.attachmentAuthorityId !== claim.receipt.attachmentAuthorityId ||
|
|
1932
|
+
!(current.inboundDispatches ?? []).some(receipt =>
|
|
1933
|
+
this.#sameInboundReceipt(receipt, claim.receipt),
|
|
1934
|
+
)
|
|
1935
|
+
)
|
|
1936
|
+
return undefined;
|
|
1937
|
+
const result = this.#journal
|
|
1938
|
+
.dispatchWithLeaseFence(effectId, lease, dispatch, wire.promise)
|
|
1939
|
+
.then(value => {
|
|
1940
|
+
if (!value) throw new SlackStaleEffectError();
|
|
1941
|
+
return value.value;
|
|
1942
|
+
});
|
|
1943
|
+
return { result, dispatched: wire.promise };
|
|
1944
|
+
});
|
|
1945
|
+
} catch (error) {
|
|
1946
|
+
if (error instanceof ConversationLockTimeoutError && !wireReached)
|
|
1947
|
+
throw new SessionRouterError("pre_send", "Slack durable dispatch fence timed out before send.");
|
|
1948
|
+
throw error;
|
|
1949
|
+
}
|
|
1950
|
+
})();
|
|
1951
|
+
if (result === undefined) throw new SlackStaleEffectError();
|
|
1952
|
+
return result.value;
|
|
1953
|
+
},
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
#admitActivatedRoot(conversation: SlackConversation): void {
|
|
1958
|
+
if (!conversation.sessionId || !conversation.endpointGeneration || !conversation.rootTs) return;
|
|
1959
|
+
const rootKey = `${conversation.sessionId}\u0000${conversation.endpointGeneration}\u0000${conversation.rootTs}`;
|
|
1960
|
+
if (this.#retiredInboundRoots.has(rootKey)) return;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
#retireInboundRoot(rootKey: string): void {
|
|
1964
|
+
this.#retiredInboundRoots.add(rootKey);
|
|
1965
|
+
while (this.#retiredInboundRoots.size > MAX_TERMINAL_CHAT_EFFECTS * 2) {
|
|
1966
|
+
const oldest = this.#retiredInboundRoots.values().next().value;
|
|
1967
|
+
if (oldest === undefined) break;
|
|
1968
|
+
this.#retiredInboundRoots.delete(oldest);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
#incrementFence(fences: Map<string, number>, key: string): void {
|
|
1973
|
+
fences.set(key, (fences.get(key) ?? 0) + 1);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
#decrementFence(fences: Map<string, number>, key: string): void {
|
|
1977
|
+
const remaining = (fences.get(key) ?? 1) - 1;
|
|
1978
|
+
if (remaining === 0) fences.delete(key);
|
|
1979
|
+
else fences.set(key, remaining);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1631
1982
|
async #withEffectLease<T>(
|
|
1632
1983
|
id: string,
|
|
1633
1984
|
lease: ChatEffectLease,
|
|
1634
1985
|
scope: SlackProviderWorkScope,
|
|
1635
1986
|
operation: (signal: AbortSignal) => Promise<T>,
|
|
1636
1987
|
workGeneration = this.#workGeneration,
|
|
1988
|
+
preserveAmbiguityOnRetire = false,
|
|
1637
1989
|
): Promise<T> {
|
|
1638
1990
|
const scopeKey = slackProviderWorkScopeKey(scope);
|
|
1639
1991
|
const controller = new AbortController();
|
|
@@ -1652,13 +2004,16 @@ export class SlackNotificationDaemon {
|
|
|
1652
2004
|
await invalidation;
|
|
1653
2005
|
};
|
|
1654
2006
|
const terminalize = async (): Promise<void> => {
|
|
2007
|
+
if (preserveAmbiguityOnRetire && (await this.#journal.read(id))?.state === "dispatching")
|
|
2008
|
+
return await invalidate();
|
|
1655
2009
|
abort();
|
|
1656
2010
|
terminalization ??= (async (): Promise<void> => {
|
|
1657
2011
|
const receipt = { status: "stale_binding" };
|
|
1658
2012
|
const terminalized = await this.#rescheduleAfterEffectTransition(
|
|
1659
|
-
this.#journal.
|
|
2013
|
+
this.#journal.terminalizeDecided(id, receipt, lease),
|
|
1660
2014
|
);
|
|
1661
|
-
if (!terminalized)
|
|
2015
|
+
if (!terminalized)
|
|
2016
|
+
await this.#rescheduleAfterEffectTransition(this.#journal.terminalizeDecided(id, receipt));
|
|
1662
2017
|
})();
|
|
1663
2018
|
await terminalization;
|
|
1664
2019
|
};
|
|
@@ -1729,11 +2084,23 @@ export class SlackNotificationDaemon {
|
|
|
1729
2084
|
const dispatchableEffectIds = new Set<string>();
|
|
1730
2085
|
for (const [key, record] of Object.entries(document.conversations)) {
|
|
1731
2086
|
for (const receipt of record.inboundDispatches ?? []) {
|
|
1732
|
-
|
|
2087
|
+
let effect = await this.#journal.read<SlackInboundEffectPayload>(receipt.effectId);
|
|
2088
|
+
if (effect?.state === "dispatching") {
|
|
2089
|
+
const inflightKey = `${key}\u0000${receipt.key}`;
|
|
2090
|
+
if (this.#inflightInbound.has(inflightKey) || (effect.leaseExpiresAt ?? 0) > this.#now()) continue;
|
|
2091
|
+
effect = await this.#journal.recoverDispatchingAsUncertain<SlackInboundEffectPayload>(effect.id, {
|
|
2092
|
+
onlyIfExpired: true,
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
1733
2095
|
if (effect?.state === "terminal") {
|
|
1734
2096
|
await this.#finalizeTerminalInboundDispatch(key, receipt);
|
|
1735
2097
|
continue;
|
|
1736
2098
|
}
|
|
2099
|
+
// A sent command whose response was lost is permanently fail-closed.
|
|
2100
|
+
// Attachment or mapping drift cannot turn that unknown remote outcome
|
|
2101
|
+
// into a definite stale refusal: doing so would erase the ambiguity and
|
|
2102
|
+
// make a duplicate provider envelope eligible for a new dispatch.
|
|
2103
|
+
if (effect?.state === "uncertain") continue;
|
|
1737
2104
|
if (!this.#mappedInboundDispatchable(record, receipt, effect)) {
|
|
1738
2105
|
await this.#terminalizeStaleInboundDispatch(key, receipt, "stale_mapping");
|
|
1739
2106
|
continue;
|
|
@@ -1747,11 +2114,7 @@ export class SlackNotificationDaemon {
|
|
|
1747
2114
|
await this.#terminalizeStaleInboundDispatch(key, receipt, "stale_binding");
|
|
1748
2115
|
continue;
|
|
1749
2116
|
}
|
|
1750
|
-
if (
|
|
1751
|
-
effect.state === "uncertain" ||
|
|
1752
|
-
(effect.state === "leased" && (effect.leaseExpiresAt ?? 0) > this.#now())
|
|
1753
|
-
)
|
|
1754
|
-
continue;
|
|
2117
|
+
if (effect.state === "leased" && (effect.leaseExpiresAt ?? 0) > this.#now()) continue;
|
|
1755
2118
|
dispatchableEffectIds.add(receipt.effectId);
|
|
1756
2119
|
const inflightKey = `${key}\u0000${receipt.key}`;
|
|
1757
2120
|
if (this.#inflightInbound.has(inflightKey)) continue;
|
|
@@ -1764,6 +2127,11 @@ export class SlackNotificationDaemon {
|
|
|
1764
2127
|
}
|
|
1765
2128
|
}
|
|
1766
2129
|
for (const effect of await this.#journal.list()) {
|
|
2130
|
+
if (effect.state === "dispatching") {
|
|
2131
|
+
if ((effect.leaseExpiresAt ?? 0) > this.#now()) continue;
|
|
2132
|
+
await this.#journal.recoverDispatchingAsUncertain(effect.id, { onlyIfExpired: true });
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
1767
2135
|
if (
|
|
1768
2136
|
dispatchableEffectIds.has(effect.id) ||
|
|
1769
2137
|
effect.transport !== "slack" ||
|
|
@@ -1774,6 +2142,7 @@ export class SlackNotificationDaemon {
|
|
|
1774
2142
|
effect.kind !== "sdk.inbound.user_message")
|
|
1775
2143
|
)
|
|
1776
2144
|
continue;
|
|
2145
|
+
if (effect.state === "uncertain") continue;
|
|
1777
2146
|
const adopted = await this.#adoptOrphanInbound(effect as ChatEffect<SlackInboundEffectPayload>);
|
|
1778
2147
|
if (!adopted) continue;
|
|
1779
2148
|
const endpoint = await this.#resolveAttachment(adopted.sessionId);
|
|
@@ -1788,8 +2157,7 @@ export class SlackNotificationDaemon {
|
|
|
1788
2157
|
await this.#terminalizeStaleInboundDispatch(adopted.key, adopted.receipt, "stale_binding");
|
|
1789
2158
|
continue;
|
|
1790
2159
|
}
|
|
1791
|
-
if (effect.state === "
|
|
1792
|
-
continue;
|
|
2160
|
+
if (effect.state === "leased" && (effect.leaseExpiresAt ?? 0) > this.#now()) continue;
|
|
1793
2161
|
await this.#dispatchInbound({
|
|
1794
2162
|
key: adopted.key,
|
|
1795
2163
|
endpoint,
|
|
@@ -1925,8 +2293,8 @@ export class SlackNotificationDaemon {
|
|
|
1925
2293
|
receipt: SlackInboundDispatchReceipt,
|
|
1926
2294
|
status: "stale_binding" | "stale_mapping",
|
|
1927
2295
|
): Promise<void> {
|
|
1928
|
-
await this.#
|
|
1929
|
-
|
|
2296
|
+
if (await this.#terminalizeInboundEffect(receipt.effectId, status))
|
|
2297
|
+
await this.#finalizeTerminalInboundDispatch(key, receipt);
|
|
1930
2298
|
}
|
|
1931
2299
|
async #finalizeTerminalInboundDispatch(key: string, receipt: SlackInboundDispatchReceipt): Promise<void> {
|
|
1932
2300
|
await this.store.transact(key, current => {
|
|
@@ -2020,7 +2388,8 @@ export class SlackNotificationDaemon {
|
|
|
2020
2388
|
current.seenEventIds.includes(routing.eventId) ||
|
|
2021
2389
|
current.seenInteractionIds.includes(routing.interactionId) ||
|
|
2022
2390
|
current.seenRetryKeys.includes(routing.retryKey) ||
|
|
2023
|
-
(routing.kind === "action" && replyableActionId(current.pendingActionId) !== routing.actionId)
|
|
2391
|
+
(routing.kind === "action" && replyableActionId(current.pendingActionId) !== routing.actionId) ||
|
|
2392
|
+
(routing.kind === "message" && replyableActionId(current.pendingActionId) !== undefined)
|
|
2024
2393
|
)
|
|
2025
2394
|
return current;
|
|
2026
2395
|
receipt = {
|
|
@@ -2049,7 +2418,32 @@ export class SlackNotificationDaemon {
|
|
|
2049
2418
|
}
|
|
2050
2419
|
|
|
2051
2420
|
async #terminalizeRejectedInbound(effectId: string): Promise<void> {
|
|
2052
|
-
await this.#
|
|
2421
|
+
await this.#terminalizeInboundEffect(effectId, "rejected");
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
async #terminalizeInboundEffect(
|
|
2425
|
+
effectId: string,
|
|
2426
|
+
status: "rejected" | "stale_binding" | "stale_mapping",
|
|
2427
|
+
): Promise<boolean> {
|
|
2428
|
+
let effect = await this.#journal.read(effectId);
|
|
2429
|
+
if (!effect || effect.state === "terminal") return true;
|
|
2430
|
+
if (effect.state === "uncertain") return false;
|
|
2431
|
+
let lease: ChatEffectLease | undefined;
|
|
2432
|
+
if (effect.state === "leased") {
|
|
2433
|
+
if ((effect.leaseExpiresAt ?? 0) > this.#now()) return false;
|
|
2434
|
+
const claimed = await this.#journal.claim(
|
|
2435
|
+
effectId,
|
|
2436
|
+
this.#publicationOwnerId,
|
|
2437
|
+
Math.max(this.#publicationLeaseMs, 100),
|
|
2438
|
+
);
|
|
2439
|
+
if (!claimed) {
|
|
2440
|
+
effect = await this.#journal.read(effectId);
|
|
2441
|
+
return effect?.state === "terminal";
|
|
2442
|
+
}
|
|
2443
|
+
lease = { owner: this.#publicationOwnerId, epoch: claimed.epoch };
|
|
2444
|
+
}
|
|
2445
|
+
const terminal = await this.#journal.terminalizeDecided(effectId, { status }, lease);
|
|
2446
|
+
return terminal?.state === "terminal";
|
|
2053
2447
|
}
|
|
2054
2448
|
|
|
2055
2449
|
async #finishDispatch(
|
|
@@ -2135,7 +2529,7 @@ export class SlackNotificationDaemon {
|
|
|
2135
2529
|
return;
|
|
2136
2530
|
// Receipt recovery must retain the original mapping through an endpoint roll;
|
|
2137
2531
|
// inbound routing still fences it against the now-current endpoint generation.
|
|
2138
|
-
await this.store.transact(this.#intentKey(effect.sessionId), current =>
|
|
2532
|
+
const activated = await this.store.transact(this.#intentKey(effect.sessionId), current =>
|
|
2139
2533
|
current &&
|
|
2140
2534
|
current.state === "posting_root" &&
|
|
2141
2535
|
current.sessionId === effect.sessionId &&
|
|
@@ -2154,6 +2548,13 @@ export class SlackNotificationDaemon {
|
|
|
2154
2548
|
})
|
|
2155
2549
|
: current,
|
|
2156
2550
|
);
|
|
2551
|
+
if (
|
|
2552
|
+
activated?.state === "active" &&
|
|
2553
|
+
activated.sessionId === effect.sessionId &&
|
|
2554
|
+
activated.endpointGeneration === effect.endpointGeneration &&
|
|
2555
|
+
activated.rootTs === rootTs
|
|
2556
|
+
)
|
|
2557
|
+
this.#admitActivatedRoot(activated);
|
|
2157
2558
|
}
|
|
2158
2559
|
|
|
2159
2560
|
async #activateReconciledAction(
|
|
@@ -2345,7 +2746,8 @@ export class SlackNotificationDaemon {
|
|
|
2345
2746
|
.reduce<number | undefined>(
|
|
2346
2747
|
(earliest, effect) => {
|
|
2347
2748
|
const claimAt =
|
|
2348
|
-
effect.state === "leased"
|
|
2749
|
+
(effect.state === "leased" || effect.state === "dispatching") &&
|
|
2750
|
+
Number.isFinite(effect.leaseExpiresAt)
|
|
2349
2751
|
? effect.leaseExpiresAt
|
|
2350
2752
|
: effect.state === "pending" ||
|
|
2351
2753
|
effect.state === "accepted" ||
|
|
@@ -2438,6 +2840,15 @@ export class SlackNotificationDaemon {
|
|
|
2438
2840
|
const threadTs = payload.threadTs;
|
|
2439
2841
|
const records = Object.values((await this.store.load()).conversations);
|
|
2440
2842
|
if (typeof threadTs === "string") {
|
|
2843
|
+
if (effect.id.startsWith("close-marker-cleanup:") || effect.id.startsWith("close-marker:"))
|
|
2844
|
+
return records.some(
|
|
2845
|
+
record =>
|
|
2846
|
+
record.sessionId === effect.sessionId &&
|
|
2847
|
+
(record.state === "active" || record.state === "resumed_root") &&
|
|
2848
|
+
record.rootTs === threadTs &&
|
|
2849
|
+
record.endpointGeneration === effect.endpointGeneration &&
|
|
2850
|
+
record.cleanupEffectId === effect.id,
|
|
2851
|
+
);
|
|
2441
2852
|
return records.some(
|
|
2442
2853
|
record =>
|
|
2443
2854
|
record.sessionId === effect.sessionId &&
|
|
@@ -2603,6 +3014,23 @@ export class SlackNotificationDaemon {
|
|
|
2603
3014
|
}
|
|
2604
3015
|
if (workGeneration !== this.#workGeneration)
|
|
2605
3016
|
throw new Error(`Slack effect ${id} lost its shutdown fence`);
|
|
3017
|
+
if (id.startsWith("close-marker:") || id.startsWith("close-marker-cleanup:")) {
|
|
3018
|
+
const sent = await this.store.dispatchWithSnapshotFence(this.#intentKey(sessionId), current => {
|
|
3019
|
+
if (
|
|
3020
|
+
!current ||
|
|
3021
|
+
current.sessionId !== sessionId ||
|
|
3022
|
+
current.cleanupEffectId !== id ||
|
|
3023
|
+
current.endpointGeneration !== endpointGeneration
|
|
3024
|
+
)
|
|
3025
|
+
return undefined;
|
|
3026
|
+
return {
|
|
3027
|
+
result: this.options.provider.postMessage({ ...effect!.payload, signal }),
|
|
3028
|
+
dispatched: Promise.resolve(),
|
|
3029
|
+
};
|
|
3030
|
+
});
|
|
3031
|
+
if (!sent) throw new SlackStaleEffectError();
|
|
3032
|
+
return sent.value;
|
|
3033
|
+
}
|
|
2606
3034
|
return await this.options.provider.postMessage({ ...effect!.payload, signal });
|
|
2607
3035
|
},
|
|
2608
3036
|
workGeneration,
|
|
@@ -2621,6 +3049,7 @@ export class SlackNotificationDaemon {
|
|
|
2621
3049
|
|
|
2622
3050
|
return posted;
|
|
2623
3051
|
} catch (error) {
|
|
3052
|
+
if (error instanceof ConversationLockTimeoutError) throw error;
|
|
2624
3053
|
if (error instanceof SlackReconciledAbsentEffectError) {
|
|
2625
3054
|
await this.#rescheduleAfterEffectTransition(
|
|
2626
3055
|
this.#journal.record(id, lease, "terminal", { provider: "slack", status: "not_found" }),
|
|
@@ -2933,9 +3362,9 @@ export class SlackNotificationDaemon {
|
|
|
2933
3362
|
);
|
|
2934
3363
|
}
|
|
2935
3364
|
#isDefiniteSdkPreSendFailure(error: unknown): boolean {
|
|
3365
|
+
if (error instanceof SdkPreparedDispatchError) return true;
|
|
2936
3366
|
if (error instanceof SlackEndpointBindingError) return true;
|
|
2937
3367
|
if (error instanceof SessionRouterError) return error.phase === "pre_send";
|
|
2938
|
-
if (error instanceof SdkClientError) return error.code === "connection_closed";
|
|
2939
3368
|
return (
|
|
2940
3369
|
error instanceof Error &&
|
|
2941
3370
|
error.name === "ChatDeliveryError" &&
|