@gajae-code/coding-agent 0.16.4 → 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 +54 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- 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/helpers.d.ts +33 -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/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -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 +12 -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/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +6 -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-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/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 +4 -2
- 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.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +2 -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 +4 -3
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +9 -7
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +198 -14
- package/src/config/model-preset-registry.ts +115 -16
- package/src/config/model-registry.ts +16 -12
- 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/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +50 -62
- package/src/discovery/helpers.ts +297 -32
- 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 +20 -2
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- 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 +80 -47
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +29 -9
- 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/session-state-lock.ts +51 -8
- 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 +6 -6
- 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 +6 -1
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/tool-status-header.ts +3 -1
- package/src/modes/controllers/command-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/interactive-mode.ts +6 -5
- 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 +16 -3
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/manager.ts +20 -2
- package/src/runtime-mcp/pool.test.ts +20 -11
- package/src/sdk/broker/broker.ts +119 -28
- 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 +35 -5
- 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 +387 -48
- 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 +2 -2
- 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 +50 -8
- package/src/sdk/host/session-runtime.ts +158 -51
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session.ts +155 -26
- package/src/sdk/transport/relay.ts +35 -13
- package/src/session/agent-session.ts +117 -52
- 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 +49 -43
- package/src/setup/paseo/setup-deps.ts +1 -1
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/system-prompt.ts +25 -3
- package/src/task/executor.ts +5 -0
- package/src/task/index.ts +4 -0
- package/src/tools/bash.ts +28 -11
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +13 -2
- 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/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +15 -0
- package/src/tools/subagent.ts +3 -2
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/tools/ultragoal-ask-guard.ts +2 -2
|
@@ -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,
|
|
@@ -117,6 +117,8 @@ export interface SlackNotificationDaemonOptions {
|
|
|
117
117
|
content: string,
|
|
118
118
|
attachment: SessionAttachment,
|
|
119
119
|
idempotencyKey: string,
|
|
120
|
+
beforeDispatch: () => void,
|
|
121
|
+
dispatchFence: <T>(dispatch: () => Promise<T>) => Promise<T>,
|
|
120
122
|
) => Promise<boolean>;
|
|
121
123
|
}
|
|
122
124
|
|
|
@@ -225,6 +227,10 @@ export class SlackNotificationDaemon {
|
|
|
225
227
|
readonly #activeWork = new Set<Promise<unknown>>();
|
|
226
228
|
readonly #workInvalidators = new Set<SlackProviderWork>();
|
|
227
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>();
|
|
228
234
|
readonly #rollovers = new Map<string, Promise<SlackRootPublication>>();
|
|
229
235
|
#workGeneration = 0;
|
|
230
236
|
#lifecycleTail: Promise<void> = Promise.resolve();
|
|
@@ -682,7 +688,9 @@ export class SlackNotificationDaemon {
|
|
|
682
688
|
}
|
|
683
689
|
|
|
684
690
|
async postRoot(sessionId: string, body: string, endpointGeneration?: number): Promise<SlackConversation> {
|
|
685
|
-
|
|
691
|
+
const conversation = (await this.#postRoot(sessionId, body, endpointGeneration)).conversation;
|
|
692
|
+
this.#admitActivatedRoot(conversation);
|
|
693
|
+
return conversation;
|
|
686
694
|
}
|
|
687
695
|
|
|
688
696
|
/**
|
|
@@ -716,7 +724,7 @@ export class SlackNotificationDaemon {
|
|
|
716
724
|
authority.attachmentAuthorityId ??
|
|
717
725
|
(attachment?.generation === authority.endpointGeneration ? attachment.authorityId : undefined);
|
|
718
726
|
await this.#verifyExistingRoot(rootTs);
|
|
719
|
-
|
|
727
|
+
const conversation = await claimSlackThreadBinding({
|
|
720
728
|
store: this.store,
|
|
721
729
|
key: this.#intentKey(sessionId),
|
|
722
730
|
teamId: this.options.teamId,
|
|
@@ -737,6 +745,8 @@ export class SlackNotificationDaemon {
|
|
|
737
745
|
},
|
|
738
746
|
now: this.#now,
|
|
739
747
|
});
|
|
748
|
+
this.#admitActivatedRoot(conversation);
|
|
749
|
+
return conversation;
|
|
740
750
|
}
|
|
741
751
|
|
|
742
752
|
/**
|
|
@@ -968,6 +978,7 @@ export class SlackNotificationDaemon {
|
|
|
968
978
|
? await this.#resumeWithRootPublication(sessionId, body, generation, rootPublication?.clientMsgId)
|
|
969
979
|
: await this.#postRoot(sessionId, body, generation, rootPublication?.clientMsgId);
|
|
970
980
|
conversation = publication.conversation;
|
|
981
|
+
this.#admitActivatedRoot(conversation);
|
|
971
982
|
bodyWasUsedAsRoot ||= publication.created;
|
|
972
983
|
}
|
|
973
984
|
if (!conversation.rootTs) return conversation;
|
|
@@ -1130,13 +1141,68 @@ export class SlackNotificationDaemon {
|
|
|
1130
1141
|
}
|
|
1131
1142
|
|
|
1132
1143
|
async close(sessionId: string, marker = "Session closed.", endpointGeneration?: number): Promise<boolean> {
|
|
1133
|
-
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
|
+
}
|
|
1134
1165
|
}
|
|
1135
1166
|
|
|
1136
1167
|
async recoverCleanup(sessionId: string, endpointGeneration: number, authorityId?: string): Promise<boolean> {
|
|
1137
1168
|
const found = await this.findSession(sessionId, true);
|
|
1138
1169
|
if (!found) return true;
|
|
1139
|
-
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
|
+
}
|
|
1140
1206
|
if (found.record.state === "closed_marker" || found.record.state === "error") return true;
|
|
1141
1207
|
if (
|
|
1142
1208
|
(found.record.state === "posting_root" || found.record.state === "resumed_root") &&
|
|
@@ -1146,10 +1212,14 @@ export class SlackNotificationDaemon {
|
|
|
1146
1212
|
if (found.record.state !== "active") return false;
|
|
1147
1213
|
if (!found.record.rootTs) return false;
|
|
1148
1214
|
if (found.record.attachmentAuthorityId === authorityId && found.record.cleanupEffectId === undefined) return true;
|
|
1149
|
-
const effectId =
|
|
1150
|
-
if (
|
|
1215
|
+
const effectId = found.record.cleanupEffectId;
|
|
1216
|
+
if (!effectId) return false;
|
|
1151
1217
|
const effect = await this.#journal.read(effectId);
|
|
1152
|
-
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) {
|
|
1153
1223
|
await this.store.transact(found.key, current =>
|
|
1154
1224
|
current?.cleanupEffectId === effectId
|
|
1155
1225
|
? nextRecord(current, { cleanupEffectId: undefined, updatedAt: this.#now() })
|
|
@@ -1162,8 +1232,19 @@ export class SlackNotificationDaemon {
|
|
|
1162
1232
|
}
|
|
1163
1233
|
|
|
1164
1234
|
async retireAttachment(sessionId: string, endpointGeneration: number): Promise<void> {
|
|
1165
|
-
const
|
|
1166
|
-
|
|
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
|
+
}
|
|
1167
1248
|
const attachmentAuthorityId = found.record.attachmentAuthorityId;
|
|
1168
1249
|
const scope: SlackProviderWorkScope = {
|
|
1169
1250
|
sessionId,
|
|
@@ -1174,6 +1255,7 @@ export class SlackNotificationDaemon {
|
|
|
1174
1255
|
let releaseScope = false;
|
|
1175
1256
|
try {
|
|
1176
1257
|
const draining = this.#invalidateAndDrainProviderWork(scope, deadline);
|
|
1258
|
+
releaseScope = true;
|
|
1177
1259
|
let inboundDispatches: SlackInboundDispatchReceipt[] = [];
|
|
1178
1260
|
// Persist the mapping fence before waiting on terminal effects. A detached
|
|
1179
1261
|
// terminalization can then never be replayed through this attachment.
|
|
@@ -1193,11 +1275,10 @@ export class SlackNotificationDaemon {
|
|
|
1193
1275
|
updatedAt: this.#now(),
|
|
1194
1276
|
});
|
|
1195
1277
|
});
|
|
1196
|
-
releaseScope = true;
|
|
1197
1278
|
await draining;
|
|
1198
1279
|
if (inboundDispatches.length === 0) return;
|
|
1199
1280
|
const terminalization = this.#retirementTerminalization(
|
|
1200
|
-
inboundDispatches.map(receipt => this.#
|
|
1281
|
+
inboundDispatches.map(receipt => this.#terminalizeInboundEffect(receipt.effectId, "stale_binding")),
|
|
1201
1282
|
);
|
|
1202
1283
|
this.#trackRetirementTerminalization(slackProviderWorkScopeKey(scope), terminalization);
|
|
1203
1284
|
const remaining = deadline - this.#now();
|
|
@@ -1213,6 +1294,7 @@ export class SlackNotificationDaemon {
|
|
|
1213
1294
|
logger.warn("Slack attachment retirement exceeded the 5000ms deadline; fenced pending inbound effects.");
|
|
1214
1295
|
} finally {
|
|
1215
1296
|
if (releaseScope) this.#releaseRetiredProviderWorkScope(scope);
|
|
1297
|
+
this.#retiringAttachmentGenerations.delete(generationKey);
|
|
1216
1298
|
}
|
|
1217
1299
|
}
|
|
1218
1300
|
|
|
@@ -1221,13 +1303,17 @@ export class SlackNotificationDaemon {
|
|
|
1221
1303
|
marker: string,
|
|
1222
1304
|
allowRemovedAttachment: boolean,
|
|
1223
1305
|
expectedEndpointGeneration?: number,
|
|
1306
|
+
onResolved?: (record: SlackConversation) => void,
|
|
1224
1307
|
): Promise<boolean> {
|
|
1225
1308
|
const found = await this.findSession(sessionId, true);
|
|
1226
1309
|
if (!found?.record.rootTs || found.record.state !== "active") return false;
|
|
1227
1310
|
if (expectedEndpointGeneration !== undefined && found.record.endpointGeneration !== expectedEndpointGeneration)
|
|
1228
1311
|
return false;
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
+
{
|
|
1231
1317
|
let admitted = false;
|
|
1232
1318
|
await this.store.transact(found.key, current => {
|
|
1233
1319
|
if (
|
|
@@ -1268,6 +1354,7 @@ export class SlackNotificationDaemon {
|
|
|
1268
1354
|
);
|
|
1269
1355
|
await this.#postDurable(effectId, sessionId, this.#requireEndpointGeneration(found.record), durable.payload);
|
|
1270
1356
|
let closed = false;
|
|
1357
|
+
let inboundDispatches: SlackInboundDispatchReceipt[] = [];
|
|
1271
1358
|
await this.store.transact(found.key, current => {
|
|
1272
1359
|
if (
|
|
1273
1360
|
current?.state !== "active" ||
|
|
@@ -1279,13 +1366,24 @@ export class SlackNotificationDaemon {
|
|
|
1279
1366
|
)
|
|
1280
1367
|
return current;
|
|
1281
1368
|
closed = true;
|
|
1369
|
+
inboundDispatches = [...(current.inboundDispatches ?? [])];
|
|
1282
1370
|
return nextRecord(current, {
|
|
1283
1371
|
state: "closed_marker",
|
|
1284
1372
|
cleanupEffectId: undefined,
|
|
1285
1373
|
pendingActionId: undefined,
|
|
1374
|
+
inboundDispatches: [],
|
|
1286
1375
|
updatedAt: this.#now(),
|
|
1287
1376
|
});
|
|
1288
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
|
+
);
|
|
1289
1387
|
|
|
1290
1388
|
return closed;
|
|
1291
1389
|
}
|
|
@@ -1296,7 +1394,9 @@ export class SlackNotificationDaemon {
|
|
|
1296
1394
|
endpointGeneration?: number,
|
|
1297
1395
|
publicationId?: string,
|
|
1298
1396
|
): Promise<SlackConversation> {
|
|
1299
|
-
|
|
1397
|
+
const conversation = await this.#track(this.#resume(sessionId, body, endpointGeneration, publicationId));
|
|
1398
|
+
this.#admitActivatedRoot(conversation);
|
|
1399
|
+
return conversation;
|
|
1300
1400
|
}
|
|
1301
1401
|
|
|
1302
1402
|
async #resume(
|
|
@@ -1415,7 +1515,7 @@ export class SlackNotificationDaemon {
|
|
|
1415
1515
|
}
|
|
1416
1516
|
let closed = false;
|
|
1417
1517
|
try {
|
|
1418
|
-
closed = await this.#track(this.#
|
|
1518
|
+
closed = await this.#track(this.#closeWithFence(sessionId, "Session closed.", false, previousGeneration));
|
|
1419
1519
|
} catch (error) {
|
|
1420
1520
|
if (!(error instanceof SlackStaleEffectError)) throw error;
|
|
1421
1521
|
// The old-generation close marker was deliberately suppressed after
|
|
@@ -1582,7 +1682,7 @@ export class SlackNotificationDaemon {
|
|
|
1582
1682
|
});
|
|
1583
1683
|
});
|
|
1584
1684
|
if (!sessionId || !receipt) {
|
|
1585
|
-
await this.#
|
|
1685
|
+
await this.#terminalizeInboundEffect(effectId, "rejected");
|
|
1586
1686
|
return undefined;
|
|
1587
1687
|
}
|
|
1588
1688
|
return { key, endpoint, sessionId, receipt };
|
|
@@ -1601,6 +1701,7 @@ export class SlackNotificationDaemon {
|
|
|
1601
1701
|
await this.#finalizeTerminalInboundDispatch(claim.key, claim.receipt);
|
|
1602
1702
|
return false;
|
|
1603
1703
|
}
|
|
1704
|
+
if (effect?.state === "uncertain") return false;
|
|
1604
1705
|
if (
|
|
1605
1706
|
!current ||
|
|
1606
1707
|
!this.#mappedInboundDispatchable(current, claim.receipt, effect) ||
|
|
@@ -1637,18 +1738,40 @@ export class SlackNotificationDaemon {
|
|
|
1637
1738
|
? {}
|
|
1638
1739
|
: { attachmentAuthorityId: claim.receipt.attachmentAuthorityId }),
|
|
1639
1740
|
};
|
|
1741
|
+
let dispatchEntered = false;
|
|
1640
1742
|
try {
|
|
1641
1743
|
if (effect.payload.type === "command") {
|
|
1642
1744
|
const payload = effect.payload;
|
|
1643
|
-
const accepted = await this.#withEffectLease(
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
claim.
|
|
1649
|
-
payload.
|
|
1650
|
-
|
|
1651
|
-
|
|
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
|
+
);
|
|
1652
1775
|
if (!(await this.#inboundEffectCurrent(claim, effect.id))) {
|
|
1653
1776
|
await this.#terminalizeStaleInboundDispatch(claim.key, claim.receipt, "stale_binding");
|
|
1654
1777
|
return false;
|
|
@@ -1659,10 +1782,30 @@ export class SlackNotificationDaemon {
|
|
|
1659
1782
|
if (recorded) await this.#finishDispatch(claim, "terminal");
|
|
1660
1783
|
return accepted && !!recorded;
|
|
1661
1784
|
}
|
|
1662
|
-
await this.#withEffectLease(
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
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
|
+
);
|
|
1666
1809
|
if (!(await this.#inboundEffectCurrent(claim, effect.id))) {
|
|
1667
1810
|
await this.#terminalizeStaleInboundDispatch(claim.key, claim.receipt, "stale_binding");
|
|
1668
1811
|
return false;
|
|
@@ -1675,7 +1818,7 @@ export class SlackNotificationDaemon {
|
|
|
1675
1818
|
await this.#terminalizeStaleInboundDispatch(claim.key, claim.receipt, "stale_binding");
|
|
1676
1819
|
return false;
|
|
1677
1820
|
}
|
|
1678
|
-
const state = this.#isDefiniteSdkPreSendFailure(error) ? "accepted" : "uncertain";
|
|
1821
|
+
const state = this.#isDefiniteSdkPreSendFailure(error) || !dispatchEntered ? "accepted" : "uncertain";
|
|
1679
1822
|
|
|
1680
1823
|
const recorded = await this.#rescheduleAfterEffectTransition(
|
|
1681
1824
|
this.#journal.record(effect.id, lease, state, { status: state }),
|
|
@@ -1691,6 +1834,18 @@ export class SlackNotificationDaemon {
|
|
|
1691
1834
|
|
|
1692
1835
|
effectId: string,
|
|
1693
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;
|
|
1694
1849
|
const endpoint = await this.#resolveAttachment(claim.sessionId);
|
|
1695
1850
|
if (!endpoint?.isCurrent()) return false;
|
|
1696
1851
|
if (
|
|
@@ -1717,12 +1872,120 @@ export class SlackNotificationDaemon {
|
|
|
1717
1872
|
);
|
|
1718
1873
|
}
|
|
1719
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
|
+
|
|
1720
1982
|
async #withEffectLease<T>(
|
|
1721
1983
|
id: string,
|
|
1722
1984
|
lease: ChatEffectLease,
|
|
1723
1985
|
scope: SlackProviderWorkScope,
|
|
1724
1986
|
operation: (signal: AbortSignal) => Promise<T>,
|
|
1725
1987
|
workGeneration = this.#workGeneration,
|
|
1988
|
+
preserveAmbiguityOnRetire = false,
|
|
1726
1989
|
): Promise<T> {
|
|
1727
1990
|
const scopeKey = slackProviderWorkScopeKey(scope);
|
|
1728
1991
|
const controller = new AbortController();
|
|
@@ -1741,13 +2004,16 @@ export class SlackNotificationDaemon {
|
|
|
1741
2004
|
await invalidation;
|
|
1742
2005
|
};
|
|
1743
2006
|
const terminalize = async (): Promise<void> => {
|
|
2007
|
+
if (preserveAmbiguityOnRetire && (await this.#journal.read(id))?.state === "dispatching")
|
|
2008
|
+
return await invalidate();
|
|
1744
2009
|
abort();
|
|
1745
2010
|
terminalization ??= (async (): Promise<void> => {
|
|
1746
2011
|
const receipt = { status: "stale_binding" };
|
|
1747
2012
|
const terminalized = await this.#rescheduleAfterEffectTransition(
|
|
1748
|
-
this.#journal.
|
|
2013
|
+
this.#journal.terminalizeDecided(id, receipt, lease),
|
|
1749
2014
|
);
|
|
1750
|
-
if (!terminalized)
|
|
2015
|
+
if (!terminalized)
|
|
2016
|
+
await this.#rescheduleAfterEffectTransition(this.#journal.terminalizeDecided(id, receipt));
|
|
1751
2017
|
})();
|
|
1752
2018
|
await terminalization;
|
|
1753
2019
|
};
|
|
@@ -1818,11 +2084,23 @@ export class SlackNotificationDaemon {
|
|
|
1818
2084
|
const dispatchableEffectIds = new Set<string>();
|
|
1819
2085
|
for (const [key, record] of Object.entries(document.conversations)) {
|
|
1820
2086
|
for (const receipt of record.inboundDispatches ?? []) {
|
|
1821
|
-
|
|
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
|
+
}
|
|
1822
2095
|
if (effect?.state === "terminal") {
|
|
1823
2096
|
await this.#finalizeTerminalInboundDispatch(key, receipt);
|
|
1824
2097
|
continue;
|
|
1825
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;
|
|
1826
2104
|
if (!this.#mappedInboundDispatchable(record, receipt, effect)) {
|
|
1827
2105
|
await this.#terminalizeStaleInboundDispatch(key, receipt, "stale_mapping");
|
|
1828
2106
|
continue;
|
|
@@ -1836,11 +2114,7 @@ export class SlackNotificationDaemon {
|
|
|
1836
2114
|
await this.#terminalizeStaleInboundDispatch(key, receipt, "stale_binding");
|
|
1837
2115
|
continue;
|
|
1838
2116
|
}
|
|
1839
|
-
if (
|
|
1840
|
-
effect.state === "uncertain" ||
|
|
1841
|
-
(effect.state === "leased" && (effect.leaseExpiresAt ?? 0) > this.#now())
|
|
1842
|
-
)
|
|
1843
|
-
continue;
|
|
2117
|
+
if (effect.state === "leased" && (effect.leaseExpiresAt ?? 0) > this.#now()) continue;
|
|
1844
2118
|
dispatchableEffectIds.add(receipt.effectId);
|
|
1845
2119
|
const inflightKey = `${key}\u0000${receipt.key}`;
|
|
1846
2120
|
if (this.#inflightInbound.has(inflightKey)) continue;
|
|
@@ -1853,6 +2127,11 @@ export class SlackNotificationDaemon {
|
|
|
1853
2127
|
}
|
|
1854
2128
|
}
|
|
1855
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
|
+
}
|
|
1856
2135
|
if (
|
|
1857
2136
|
dispatchableEffectIds.has(effect.id) ||
|
|
1858
2137
|
effect.transport !== "slack" ||
|
|
@@ -1863,6 +2142,7 @@ export class SlackNotificationDaemon {
|
|
|
1863
2142
|
effect.kind !== "sdk.inbound.user_message")
|
|
1864
2143
|
)
|
|
1865
2144
|
continue;
|
|
2145
|
+
if (effect.state === "uncertain") continue;
|
|
1866
2146
|
const adopted = await this.#adoptOrphanInbound(effect as ChatEffect<SlackInboundEffectPayload>);
|
|
1867
2147
|
if (!adopted) continue;
|
|
1868
2148
|
const endpoint = await this.#resolveAttachment(adopted.sessionId);
|
|
@@ -1877,8 +2157,7 @@ export class SlackNotificationDaemon {
|
|
|
1877
2157
|
await this.#terminalizeStaleInboundDispatch(adopted.key, adopted.receipt, "stale_binding");
|
|
1878
2158
|
continue;
|
|
1879
2159
|
}
|
|
1880
|
-
if (effect.state === "
|
|
1881
|
-
continue;
|
|
2160
|
+
if (effect.state === "leased" && (effect.leaseExpiresAt ?? 0) > this.#now()) continue;
|
|
1882
2161
|
await this.#dispatchInbound({
|
|
1883
2162
|
key: adopted.key,
|
|
1884
2163
|
endpoint,
|
|
@@ -2014,8 +2293,8 @@ export class SlackNotificationDaemon {
|
|
|
2014
2293
|
receipt: SlackInboundDispatchReceipt,
|
|
2015
2294
|
status: "stale_binding" | "stale_mapping",
|
|
2016
2295
|
): Promise<void> {
|
|
2017
|
-
await this.#
|
|
2018
|
-
|
|
2296
|
+
if (await this.#terminalizeInboundEffect(receipt.effectId, status))
|
|
2297
|
+
await this.#finalizeTerminalInboundDispatch(key, receipt);
|
|
2019
2298
|
}
|
|
2020
2299
|
async #finalizeTerminalInboundDispatch(key: string, receipt: SlackInboundDispatchReceipt): Promise<void> {
|
|
2021
2300
|
await this.store.transact(key, current => {
|
|
@@ -2139,7 +2418,32 @@ export class SlackNotificationDaemon {
|
|
|
2139
2418
|
}
|
|
2140
2419
|
|
|
2141
2420
|
async #terminalizeRejectedInbound(effectId: string): Promise<void> {
|
|
2142
|
-
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";
|
|
2143
2447
|
}
|
|
2144
2448
|
|
|
2145
2449
|
async #finishDispatch(
|
|
@@ -2225,7 +2529,7 @@ export class SlackNotificationDaemon {
|
|
|
2225
2529
|
return;
|
|
2226
2530
|
// Receipt recovery must retain the original mapping through an endpoint roll;
|
|
2227
2531
|
// inbound routing still fences it against the now-current endpoint generation.
|
|
2228
|
-
await this.store.transact(this.#intentKey(effect.sessionId), current =>
|
|
2532
|
+
const activated = await this.store.transact(this.#intentKey(effect.sessionId), current =>
|
|
2229
2533
|
current &&
|
|
2230
2534
|
current.state === "posting_root" &&
|
|
2231
2535
|
current.sessionId === effect.sessionId &&
|
|
@@ -2244,6 +2548,13 @@ export class SlackNotificationDaemon {
|
|
|
2244
2548
|
})
|
|
2245
2549
|
: current,
|
|
2246
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);
|
|
2247
2558
|
}
|
|
2248
2559
|
|
|
2249
2560
|
async #activateReconciledAction(
|
|
@@ -2435,7 +2746,8 @@ export class SlackNotificationDaemon {
|
|
|
2435
2746
|
.reduce<number | undefined>(
|
|
2436
2747
|
(earliest, effect) => {
|
|
2437
2748
|
const claimAt =
|
|
2438
|
-
effect.state === "leased"
|
|
2749
|
+
(effect.state === "leased" || effect.state === "dispatching") &&
|
|
2750
|
+
Number.isFinite(effect.leaseExpiresAt)
|
|
2439
2751
|
? effect.leaseExpiresAt
|
|
2440
2752
|
: effect.state === "pending" ||
|
|
2441
2753
|
effect.state === "accepted" ||
|
|
@@ -2528,6 +2840,15 @@ export class SlackNotificationDaemon {
|
|
|
2528
2840
|
const threadTs = payload.threadTs;
|
|
2529
2841
|
const records = Object.values((await this.store.load()).conversations);
|
|
2530
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
|
+
);
|
|
2531
2852
|
return records.some(
|
|
2532
2853
|
record =>
|
|
2533
2854
|
record.sessionId === effect.sessionId &&
|
|
@@ -2693,6 +3014,23 @@ export class SlackNotificationDaemon {
|
|
|
2693
3014
|
}
|
|
2694
3015
|
if (workGeneration !== this.#workGeneration)
|
|
2695
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
|
+
}
|
|
2696
3034
|
return await this.options.provider.postMessage({ ...effect!.payload, signal });
|
|
2697
3035
|
},
|
|
2698
3036
|
workGeneration,
|
|
@@ -2711,6 +3049,7 @@ export class SlackNotificationDaemon {
|
|
|
2711
3049
|
|
|
2712
3050
|
return posted;
|
|
2713
3051
|
} catch (error) {
|
|
3052
|
+
if (error instanceof ConversationLockTimeoutError) throw error;
|
|
2714
3053
|
if (error instanceof SlackReconciledAbsentEffectError) {
|
|
2715
3054
|
await this.#rescheduleAfterEffectTransition(
|
|
2716
3055
|
this.#journal.record(id, lease, "terminal", { provider: "slack", status: "not_found" }),
|
|
@@ -3023,9 +3362,9 @@ export class SlackNotificationDaemon {
|
|
|
3023
3362
|
);
|
|
3024
3363
|
}
|
|
3025
3364
|
#isDefiniteSdkPreSendFailure(error: unknown): boolean {
|
|
3365
|
+
if (error instanceof SdkPreparedDispatchError) return true;
|
|
3026
3366
|
if (error instanceof SlackEndpointBindingError) return true;
|
|
3027
3367
|
if (error instanceof SessionRouterError) return error.phase === "pre_send";
|
|
3028
|
-
if (error instanceof SdkClientError) return error.code === "connection_closed";
|
|
3029
3368
|
return (
|
|
3030
3369
|
error instanceof Error &&
|
|
3031
3370
|
error.name === "ChatDeliveryError" &&
|