@bastani/atomic 0.9.45 → 0.9.46
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 +79 -0
- package/dist/builtin/intercom/CHANGELOG.md +22 -0
- package/dist/builtin/intercom/broker/broker.ts +124 -10
- package/dist/builtin/intercom/broker/client.ts +177 -25
- package/dist/builtin/intercom/broker/multinode-router.ts +228 -27
- package/dist/builtin/intercom/broker/multinode-state-store.ts +10 -0
- package/dist/builtin/intercom/broker/recipient-receipts.ts +38 -4
- package/dist/builtin/intercom/broker/send-handler.ts +9 -2
- package/dist/builtin/intercom/broker/sql-outbox-store.ts +29 -4
- package/dist/builtin/intercom/group.ts +25 -0
- package/dist/builtin/intercom/index.bundle.mjs +177 -35
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/retry-policy.ts +7 -0
- package/dist/builtin/intercom/types.ts +21 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +21 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/extension/index.bundle.mjs +152 -56
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +79 -0
- package/dist/builtin/workflows/builtin/{chunk-6901nsd6.js → chunk-6jpwc09k.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-vwx023ge.js → chunk-qhm35ntx.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-jvna28mq.js → chunk-zhyqnf5g.js} +331 -68
- package/dist/builtin/workflows/builtin/goal.js +2 -2
- package/dist/builtin/workflows/builtin/index.js +3 -3
- package/dist/builtin/workflows/builtin/ralph.js +2 -2
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/prompt-engineer/SKILL.md +1 -0
- package/dist/builtin/workflows/skills/prompt-engineer/references/gpt_6.md +2 -2
- package/dist/builtin/workflows/skills/prompt-engineer/references/gpt_6_1_sol.md +2 -2
- package/dist/builtin/workflows/skills/prompt-engineer/references/gpt_6_1_terra.md +77 -0
- package/dist/builtin/workflows/src/authoring.d.ts +1 -1
- package/dist/builtin/workflows/src/durable/journal/internal.d.ts +2 -0
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +7104 -3387
- package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +11105 -7264
- package/dist/builtin/workflows/src/index.js +6796 -3606
- package/dist/builtin/workflows/src/local-worker-entry.mjs +11105 -7264
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +19 -0
- package/dist/core/agent-session-boundaries.d.ts.map +1 -1
- package/dist/core/agent-session-boundaries.js +1 -0
- package/dist/core/agent-session-boundaries.js.map +1 -1
- package/dist/core/agent-session-events.d.ts.map +1 -1
- package/dist/core/agent-session-events.js +2 -1
- package/dist/core/agent-session-events.js.map +1 -1
- package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
- package/dist/core/agent-session-extension-bindings.js +15 -2
- package/dist/core/agent-session-extension-bindings.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +3 -2
- package/dist/core/agent-session-methods.d.ts.map +1 -1
- package/dist/core/agent-session-methods.js.map +1 -1
- package/dist/core/agent-session-prompt.d.ts +4 -2
- package/dist/core/agent-session-prompt.d.ts.map +1 -1
- package/dist/core/agent-session-prompt.js +13 -8
- package/dist/core/agent-session-prompt.js.map +1 -1
- package/dist/core/agent-session-recovery.d.ts.map +1 -1
- package/dist/core/agent-session-recovery.js +92 -1
- package/dist/core/agent-session-recovery.js.map +1 -1
- package/dist/core/agent-session-tasks.d.ts.map +1 -1
- package/dist/core/agent-session-tasks.js +8 -1
- package/dist/core/agent-session-tasks.js.map +1 -1
- package/dist/core/agent-session-transfer.d.ts.map +1 -1
- package/dist/core/agent-session-transfer.js +5 -0
- package/dist/core/agent-session-transfer.js.map +1 -1
- package/dist/core/agent-session.d.ts +2 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/copilot-errors.d.ts +6 -0
- package/dist/core/copilot-errors.d.ts.map +1 -0
- package/dist/core/copilot-errors.js +17 -0
- package/dist/core/copilot-errors.js.map +1 -0
- package/dist/core/execution-model-router.d.ts.map +1 -1
- package/dist/core/execution-model-router.js +12 -2
- package/dist/core/execution-model-router.js.map +1 -1
- package/dist/core/extensions/context-types.d.ts +7 -0
- package/dist/core/extensions/context-types.d.ts.map +1 -1
- package/dist/core/extensions/context-types.js.map +1 -1
- package/dist/core/extensions/runner-context.d.ts.map +1 -1
- package/dist/core/extensions/runner-context.js +6 -0
- package/dist/core/extensions/runner-context.js.map +1 -1
- package/dist/core/extensions/ui-types.d.ts +9 -1
- package/dist/core/extensions/ui-types.d.ts.map +1 -1
- package/dist/core/extensions/ui-types.js.map +1 -1
- package/dist/core/extensions/workflow-activity-hub.d.ts +4 -0
- package/dist/core/extensions/workflow-activity-hub.d.ts.map +1 -1
- package/dist/core/extensions/workflow-activity-hub.js +26 -5
- package/dist/core/extensions/workflow-activity-hub.js.map +1 -1
- package/dist/core/extensions/workflow-events.d.ts +5 -0
- package/dist/core/extensions/workflow-events.d.ts.map +1 -1
- package/dist/core/extensions/workflow-events.js.map +1 -1
- package/dist/core/messages.d.ts +5 -0
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +1 -1
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-fallback-failures.d.ts +1 -1
- package/dist/core/model-fallback-failures.d.ts.map +1 -1
- package/dist/core/model-fallback-failures.js +37 -3
- package/dist/core/model-fallback-failures.js.map +1 -1
- package/dist/core/model-resolver-defaults.js +2 -2
- package/dist/core/model-resolver-defaults.js.map +1 -1
- package/dist/core/model-runtime-streaming.js +2 -2
- package/dist/core/model-runtime-streaming.js.map +1 -1
- package/dist/core/package-manager-npm.d.ts.map +1 -1
- package/dist/core/package-manager-npm.js +7 -3
- package/dist/core/package-manager-npm.js.map +1 -1
- package/dist/core/provider-composer.d.ts.map +1 -1
- package/dist/core/provider-composer.js +19 -11
- package/dist/core/provider-composer.js.map +1 -1
- package/dist/core/provider-rate-limits.d.ts +55 -6
- package/dist/core/provider-rate-limits.d.ts.map +1 -1
- package/dist/core/provider-rate-limits.js +182 -29
- package/dist/core/provider-rate-limits.js.map +1 -1
- package/dist/core/provider-request-admission.d.ts.map +1 -1
- package/dist/core/provider-request-admission.js +42 -18
- package/dist/core/provider-request-admission.js.map +1 -1
- package/dist/core/storage/node-sqlite.d.ts.map +1 -1
- package/dist/core/storage/node-sqlite.js +176 -19
- package/dist/core/storage/node-sqlite.js.map +1 -1
- package/dist/core/storage/provider.d.ts +4 -2
- package/dist/core/storage/provider.d.ts.map +1 -1
- package/dist/core/storage/provider.js +94 -5
- package/dist/core/storage/provider.js.map +1 -1
- package/dist/core/structured-output/index.d.ts.map +1 -1
- package/dist/core/structured-output/index.js +15 -8
- package/dist/core/structured-output/index.js.map +1 -1
- package/dist/core/structured-output/provider-failure.d.ts +17 -0
- package/dist/core/structured-output/provider-failure.d.ts.map +1 -0
- package/dist/core/structured-output/provider-failure.js +28 -0
- package/dist/core/structured-output/provider-failure.js.map +1 -0
- package/dist/core/tasks/completion.d.ts +6 -1
- package/dist/core/tasks/completion.d.ts.map +1 -1
- package/dist/core/tasks/completion.js +33 -18
- package/dist/core/tasks/completion.js.map +1 -1
- package/dist/core/tasks/pause.d.ts +2 -1
- package/dist/core/tasks/pause.d.ts.map +1 -1
- package/dist/core/tasks/pause.js +3 -2
- package/dist/core/tasks/pause.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +13 -7
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +6 -4
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/shell-task-render.d.ts +1 -0
- package/dist/core/tools/shell-task-render.d.ts.map +1 -1
- package/dist/core/tools/shell-task-render.js +7 -2
- package/dist/core/tools/shell-task-render.js.map +1 -1
- package/dist/core/tools/shell-task-status.d.ts.map +1 -1
- package/dist/core/tools/shell-task-status.js +2 -1
- package/dist/core/tools/shell-task-status.js.map +1 -1
- package/dist/extensions/tool-search/tool.d.ts +5 -0
- package/dist/extensions/tool-search/tool.d.ts.map +1 -1
- package/dist/extensions/tool-search/tool.js +38 -5
- package/dist/extensions/tool-search/tool.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +20 -12
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-actions.js +3 -2
- package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +42 -14
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/interactive-extension-context.js +2 -1
- package/dist/modes/interactive/interactive-extension-context.js.map +1 -1
- package/dist/modes/interactive/interactive-extension-custom-ui.js +1 -1
- package/dist/modes/interactive/interactive-extension-custom-ui.js.map +1 -1
- package/dist/modes/interactive/interactive-extension-dialogs.js +10 -4
- package/dist/modes/interactive/interactive-extension-dialogs.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
- package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-startup.js +5 -6
- package/dist/modes/interactive/interactive-startup.js.map +1 -1
- package/dist/modes/interactive-engine/engine-custom-ui.d.ts +3 -0
- package/dist/modes/interactive-engine/engine-custom-ui.d.ts.map +1 -1
- package/dist/modes/interactive-engine/engine-custom-ui.js +1 -0
- package/dist/modes/interactive-engine/engine-custom-ui.js.map +1 -1
- package/dist/modes/interactive-engine/protocol.d.ts +6 -0
- package/dist/modes/interactive-engine/protocol.d.ts.map +1 -1
- package/dist/modes/interactive-engine/protocol.js +9 -0
- package/dist/modes/interactive-engine/protocol.js.map +1 -1
- package/dist/modes/interactive-engine/remote-component.d.ts.map +1 -1
- package/dist/modes/interactive-engine/remote-component.js +3 -2
- package/dist/modes/interactive-engine/remote-component.js.map +1 -1
- package/dist/modes/rpc/rpc-client-process.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client-process.js +8 -1
- package/dist/modes/rpc/rpc-client-process.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +7 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/utils/ansi.d.ts +2 -0
- package/dist/utils/ansi.d.ts.map +1 -1
- package/dist/utils/ansi.js +5 -0
- package/dist/utils/ansi.js.map +1 -1
- package/docs/codemode.md +1 -1
- package/docs/intercom/reference.md +3 -3
- package/docs/llama-cpp.md +25 -14
- package/docs/models/evals.md +31 -12
- package/docs/models/reference.md +23 -51
- package/docs/models.md +3 -4
- package/docs/providers.md +5 -15
- package/docs/settings.md +1 -1
- package/docs/storage.md +2 -2
- package/docs/tools.md +1 -1
- package/docs/tui.md +2 -0
- package/docs/workflows/api-reference.md +20 -1
- package/docs/workflows/operations.md +17 -4
- package/npm-shrinkwrap.json +64 -44
- package/package.json +4 -4
|
@@ -2,7 +2,11 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { hostname } from "node:os";
|
|
3
3
|
import { normalizeGroup } from "../group.js";
|
|
4
4
|
import { isAgentRecipient, NON_AGENT_RECIPIENT_REFUSAL, TERMINAL_CHILD_ASK_REFUSAL } from "../recipient-purpose.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DELIVERED_MESSAGE_TTL_MS,
|
|
7
|
+
RECIPIENT_RECEIPT_TIMEOUT_MS,
|
|
8
|
+
RECIPIENT_SAVE_MAX_WAIT_MS,
|
|
9
|
+
} from "../retry-policy.js";
|
|
6
10
|
import {
|
|
7
11
|
resolveSessionTarget,
|
|
8
12
|
resolveSessionTargetByHostSessionId,
|
|
@@ -66,6 +70,12 @@ export const OFFLINE_SESSION_TARGET = "offline session";
|
|
|
66
70
|
export const SHARED_STORAGE_UNAVAILABLE_REASON =
|
|
67
71
|
"Session not found on this node, and Intercom could not reach the shared storage database to look for it on other nodes. Try again once the database is available.";
|
|
68
72
|
const MAX_HELD_SENDS = 256;
|
|
73
|
+
/**
|
|
74
|
+
* #3983: how long the decision to keep a sender waiting may take before the sender is told to keep waiting while it
|
|
75
|
+
* finishes. Well inside the sender's own wait past the receipt wait (`SEND_RESPONSE_TIMEOUT_MS` minus
|
|
76
|
+
* `RECIPIENT_RECEIPT_TIMEOUT_MS`).
|
|
77
|
+
*/
|
|
78
|
+
const HOLD_CHECK_GRACE_MS = 500;
|
|
69
79
|
/** A session that moved here may find its presence lease still held by the broker it just left. */
|
|
70
80
|
const PRESENCE_HANDOVER_ATTEMPTS = 4;
|
|
71
81
|
const PRESENCE_HANDOVER_RETRY_MS = 50;
|
|
@@ -180,6 +190,12 @@ interface Waiter {
|
|
|
180
190
|
/** Told `queued` already: `delivered` follows only while the sender is connected, and it never expires. */
|
|
181
191
|
readonly queued: boolean;
|
|
182
192
|
timer: ReturnType<typeof setTimeout> | undefined;
|
|
193
|
+
/** When the sender started waiting; a recipient's save reports never keep it waiting past the save cap (#3983). */
|
|
194
|
+
readonly waitingSince: number;
|
|
195
|
+
/** Ends the wait as uncertain at the save cap, whatever the store does (#3983). */
|
|
196
|
+
capTimer?: ReturnType<typeof setTimeout>;
|
|
197
|
+
/** The hold check whose outbox read is in flight; a wait that ends meanwhile reuses it (#3983). */
|
|
198
|
+
holdCheck?: Promise<void>;
|
|
183
199
|
}
|
|
184
200
|
|
|
185
201
|
interface TrackedHold {
|
|
@@ -218,6 +234,16 @@ interface RemoteTarget {
|
|
|
218
234
|
readonly hostBrokerId?: string;
|
|
219
235
|
}
|
|
220
236
|
|
|
237
|
+
function queuedFrame(messageId: string, attemptId: string | undefined, position: number): BrokerOutboundFrame {
|
|
238
|
+
return {
|
|
239
|
+
type: "queued",
|
|
240
|
+
messageId,
|
|
241
|
+
...(attemptId === undefined ? {} : { attemptId }),
|
|
242
|
+
target: OFFLINE_SESSION_TARGET,
|
|
243
|
+
position,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
221
247
|
function returnAddressOf(identity: string): string | undefined {
|
|
222
248
|
try {
|
|
223
249
|
const parsed = JSON.parse(identity) as unknown;
|
|
@@ -404,7 +430,10 @@ export class MultiNodeRouter {
|
|
|
404
430
|
const sessionId = session.info.id;
|
|
405
431
|
this.serial(identity, async () => {
|
|
406
432
|
for (const inflight of claims) await this.state.outbox.releaseClaim(inflight.claim);
|
|
407
|
-
if (entry.lease !== undefined)
|
|
433
|
+
if (entry.lease !== undefined) {
|
|
434
|
+
await this.state.leases.release(entry.lease);
|
|
435
|
+
await this.wakeWaitingOrigins(identity);
|
|
436
|
+
}
|
|
408
437
|
await this.recordKnown(session, entry);
|
|
409
438
|
await this.state.pendingAsks.pruneSender(sessionId);
|
|
410
439
|
await this.state.pendingAsks.takeForTarget(sessionId);
|
|
@@ -426,6 +455,18 @@ export class MultiNodeRouter {
|
|
|
426
455
|
return true;
|
|
427
456
|
}
|
|
428
457
|
|
|
458
|
+
/**
|
|
459
|
+
* #3983 `saving` from a local recipient: it is still saving a row this broker claimed for it. Recorded on the
|
|
460
|
+
* row, so the sender's broker keeps its sender waiting. True when it matches such a row.
|
|
461
|
+
*/
|
|
462
|
+
saving(recipient: BrokerConnectedSession, messageId: string): boolean {
|
|
463
|
+
const inflight = this.inflight.get(messageId);
|
|
464
|
+
if (inflight === undefined || inflight.identity !== deliveryTargetIdentity(recipient)) return false;
|
|
465
|
+
const report = this.state.outbox.reportSaving?.(inflight.claim);
|
|
466
|
+
if (report !== undefined) void report.catch((error: Error) => this.report(error));
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
|
|
429
470
|
/**
|
|
430
471
|
* Sessions other brokers host now (live presence leases), keyed by session id, for `list`: a session connected
|
|
431
472
|
* through another node is a live peer exactly like a local one. Group visibility is the caller's, as for local
|
|
@@ -933,40 +974,83 @@ export class MultiNodeRouter {
|
|
|
933
974
|
targetIdentity: target.identity,
|
|
934
975
|
queued: offline,
|
|
935
976
|
timer: undefined,
|
|
977
|
+
waitingSince: Date.now(),
|
|
936
978
|
};
|
|
937
|
-
waiter
|
|
938
|
-
() => {
|
|
939
|
-
if (this.waiters.get(message.id) !== waiter) return;
|
|
940
|
-
this.dropWaiter(waiter);
|
|
941
|
-
if (!offline) {
|
|
942
|
-
this.host.write(request.socket, {
|
|
943
|
-
type: "delivery_failed",
|
|
944
|
-
messageId: message.id,
|
|
945
|
-
attemptId: request.attemptId,
|
|
946
|
-
reason: UNCERTAIN_RECEIPT_REASON,
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
},
|
|
950
|
-
offline ? DELIVERED_MESSAGE_TTL_MS : RECIPIENT_RECEIPT_TIMEOUT_MS,
|
|
951
|
-
);
|
|
952
|
-
waiter.timer.unref?.();
|
|
979
|
+
this.armWaiter(waiter, offline ? DELIVERED_MESSAGE_TTL_MS : RECIPIENT_RECEIPT_TIMEOUT_MS);
|
|
953
980
|
this.waiters.set(message.id, waiter);
|
|
954
981
|
if (offline) {
|
|
955
|
-
|
|
956
|
-
const index = listed.status === "listed" ? listed.rows.findIndex((entry) => entry.messageId === message.id) : -1;
|
|
957
|
-
this.host.write(request.socket, {
|
|
958
|
-
type: "queued",
|
|
959
|
-
messageId: message.id,
|
|
960
|
-
...(request.attemptId === undefined ? {} : { attemptId: request.attemptId }),
|
|
961
|
-
target: OFFLINE_SESSION_TARGET,
|
|
962
|
-
position: index + 1 || 1,
|
|
963
|
-
});
|
|
982
|
+
await this.writeQueued(request.socket, message.id, request.attemptId, target.identity);
|
|
964
983
|
return;
|
|
965
984
|
}
|
|
966
985
|
if (asksTarget) await this.writeQuestionTarget(request.socket, message.id, request.attemptId, target);
|
|
967
986
|
await this.bus.publish({ brokerId: target.hostBrokerId!, reason: "outbox", version: row.sequence });
|
|
987
|
+
// A clean quit recorded before this send's row was written is seen here; one recorded after it wakes this
|
|
988
|
+
// broker (`wakeWaitingOrigins`), so a send racing its recipient's quit is queued either way. The waiter's own
|
|
989
|
+
// receipt wait keeps running meanwhile, so slow reads here never hold the sender past it.
|
|
990
|
+
void this.queueIfRecipientLeft(waiter).catch((error: Error) => this.report(error));
|
|
968
991
|
}
|
|
969
992
|
|
|
993
|
+
private async writeQueued(
|
|
994
|
+
socket: BrokerConnectedSession["socket"],
|
|
995
|
+
messageId: string,
|
|
996
|
+
attemptId: string | undefined,
|
|
997
|
+
identity: string,
|
|
998
|
+
): Promise<void> {
|
|
999
|
+
this.host.write(socket, queuedFrame(messageId, attemptId, await this.queuePosition(identity, messageId)));
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
private async queuePosition(identity: string, messageId: string): Promise<number> {
|
|
1003
|
+
const listed = await this.state.outbox.listUndelivered(identity);
|
|
1004
|
+
const index = listed.status === "listed" ? listed.rows.findIndex((entry) => entry.messageId === messageId) : -1;
|
|
1005
|
+
return index + 1 || 1;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* A send routed to a live recipient that has since quit, before any broker claimed its row: the row was never
|
|
1010
|
+
* written to the recipient and no broker hosts it now, so the sender is to be told `queued`, as for a send routed
|
|
1011
|
+
* after the quit, instead of waiting out a receipt that cannot come and hearing the outcome is uncertain. Presence
|
|
1012
|
+
* is read before the row, so a row still unclaimed was never written to any recipient. Only reads: the row's queue
|
|
1013
|
+
* position when that holds, otherwise undefined.
|
|
1014
|
+
*/
|
|
1015
|
+
private async positionIfRecipientLeft(waiter: Waiter): Promise<number | undefined> {
|
|
1016
|
+
const presence = await this.state.leases.read({ kind: "presence", targetIdentity: waiter.targetIdentity });
|
|
1017
|
+
if (presence.status === "invalid") return undefined;
|
|
1018
|
+
if (presence.status === "found" && presence.lease.ageMs < BROKER_LEASE_STALE_AFTER_MS) return undefined;
|
|
1019
|
+
const read = await this.state.outbox.read(waiter.messageId);
|
|
1020
|
+
if (read.status !== "found" || read.row.state !== "queued" || read.row.claimAttempts !== 0) return undefined;
|
|
1021
|
+
return this.queuePosition(waiter.targetIdentity, waiter.messageId);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/** Tell a still-waiting sender `queued`, at once: every read was made before. True when it was told. */
|
|
1025
|
+
private settleQueued(waiter: Waiter, position: number): boolean {
|
|
1026
|
+
if (waiter.queued || this.waiters.get(waiter.messageId) !== waiter) return false;
|
|
1027
|
+
this.dropWaiter(waiter);
|
|
1028
|
+
const queued: Waiter = { ...waiter, queued: true, timer: undefined, capTimer: undefined, holdCheck: undefined };
|
|
1029
|
+
this.armWaiter(queued, DELIVERED_MESSAGE_TTL_MS);
|
|
1030
|
+
this.waiters.set(queued.messageId, queued);
|
|
1031
|
+
this.host.write(queued.socket, queuedFrame(queued.messageId, queued.attemptId, position));
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Off the receipt wait (a wake-up, the outcome poll, right after routing): the waiter's own timers keep running
|
|
1037
|
+
* while this reads, so `expireWaiter` still answers the sender in time, and an answer here after that is dropped.
|
|
1038
|
+
*/
|
|
1039
|
+
private async queueIfRecipientLeft(waiter: Waiter): Promise<void> {
|
|
1040
|
+
if (waiter.queued || this.waiters.get(waiter.messageId) !== waiter) return;
|
|
1041
|
+
const position = await this.positionIfRecipientLeft(waiter);
|
|
1042
|
+
if (position !== undefined) this.settleQueued(waiter, position);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** A recipient left: the brokers waiting on its undelivered rows re-read them, and queue the ones never claimed. */
|
|
1046
|
+
private async wakeWaitingOrigins(identity: string): Promise<void> {
|
|
1047
|
+
const listed = await this.state.outbox.listUndelivered(identity);
|
|
1048
|
+
if (listed.status !== "listed") return;
|
|
1049
|
+
for (const origin of new Set(listed.rows.map((row) => row.originBrokerId))) {
|
|
1050
|
+
if (origin === this.brokerId) void this.checkOutcomes();
|
|
1051
|
+
else await this.bus.publish({ brokerId: origin, reason: "outcome", version: 0 });
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
970
1054
|
private async writeQuestionTarget(
|
|
971
1055
|
socket: BrokerConnectedSession["socket"],
|
|
972
1056
|
messageId: string,
|
|
@@ -1266,6 +1350,9 @@ export class MultiNodeRouter {
|
|
|
1266
1350
|
attemptId: waiter.attemptId,
|
|
1267
1351
|
reason: row.failureReason ?? "Session not found",
|
|
1268
1352
|
});
|
|
1353
|
+
} else if (row.state === "queued" && row.claimAttempts === 0 && !waiter.queued) {
|
|
1354
|
+
// Not awaited: its reads must not hold back the other senders' outcomes.
|
|
1355
|
+
void this.queueIfRecipientLeft(waiter).catch((error: Error) => this.report(error));
|
|
1269
1356
|
}
|
|
1270
1357
|
}
|
|
1271
1358
|
} while (state.again && !this.closed);
|
|
@@ -1295,9 +1382,123 @@ export class MultiNodeRouter {
|
|
|
1295
1382
|
this.host.write(waiter.socket, { type: "delivered", messageId: waiter.messageId, attemptId: waiter.attemptId });
|
|
1296
1383
|
}
|
|
1297
1384
|
|
|
1385
|
+
private armWaiter(waiter: Waiter, timeoutMs: number): void {
|
|
1386
|
+
if (waiter.timer !== undefined) clearTimeout(waiter.timer);
|
|
1387
|
+
waiter.timer = setTimeout(() => {
|
|
1388
|
+
if (this.waiters.get(waiter.messageId) !== waiter) return;
|
|
1389
|
+
if (waiter.queued) {
|
|
1390
|
+
this.dropWaiter(waiter);
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
// One hold check per waiter: a wait that ends while the previous check's read is still pending reuses it.
|
|
1394
|
+
if (waiter.holdCheck !== undefined) return;
|
|
1395
|
+
waiter.holdCheck = this.expireWaiter(waiter)
|
|
1396
|
+
.catch((error: Error) => {
|
|
1397
|
+
this.report(error);
|
|
1398
|
+
this.expireUncertain(waiter);
|
|
1399
|
+
})
|
|
1400
|
+
.finally(() => {
|
|
1401
|
+
waiter.holdCheck = undefined;
|
|
1402
|
+
});
|
|
1403
|
+
}, timeoutMs);
|
|
1404
|
+
waiter.timer.unref?.();
|
|
1405
|
+
// #3983: the save cap holds from the sender's own start, whatever the store does.
|
|
1406
|
+
if (!waiter.queued && waiter.capTimer === undefined) {
|
|
1407
|
+
waiter.capTimer = setTimeout(
|
|
1408
|
+
() => this.expireUncertain(waiter),
|
|
1409
|
+
Math.max(0, waiter.waitingSince + RECIPIENT_SAVE_MAX_WAIT_MS - Date.now()),
|
|
1410
|
+
);
|
|
1411
|
+
waiter.capTimer.unref?.();
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/** The sender may wait this much longer under the save cap; 0 once the cap is reached. */
|
|
1416
|
+
private capRemaining(waiter: Waiter): number {
|
|
1417
|
+
return Math.max(0, waiter.waitingSince + RECIPIENT_SAVE_MAX_WAIT_MS - Date.now());
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* #3983: the sender's receipt wait ended. A recipient on another broker that reported, within the last receipt
|
|
1422
|
+
* wait, that it is still saving the message keeps its sender waiting another wait (told `send_held`), within the
|
|
1423
|
+
* save cap; otherwise the sender hears that the outcome is uncertain, as before. The decision reads the outbox
|
|
1424
|
+
* row, which can wait behind other writes on the shared connection: a read slower than `HOLD_CHECK_GRACE_MS`
|
|
1425
|
+
* first tells the sender to keep waiting while it finishes (within the cap), and a read that has not answered
|
|
1426
|
+
* within one receipt wait counts as no evidence, so the sender hears the outcome is uncertain. A row no broker
|
|
1427
|
+
* claimed whose recipient has quit is queued for it instead; the reads deciding that run under the same grace and
|
|
1428
|
+
* deadline. Only one decision per waiter is ever in flight, and the cap ends the wait however the store behaves.
|
|
1429
|
+
*/
|
|
1430
|
+
private async expireWaiter(waiter: Waiter): Promise<void> {
|
|
1431
|
+
const provisional = setTimeout(() => {
|
|
1432
|
+
const timeoutMs = Math.min(RECIPIENT_RECEIPT_TIMEOUT_MS, this.capRemaining(waiter));
|
|
1433
|
+
if (this.waiters.get(waiter.messageId) !== waiter || timeoutMs === 0) return;
|
|
1434
|
+
this.armWaiter(waiter, timeoutMs);
|
|
1435
|
+
this.holdSender(waiter, timeoutMs);
|
|
1436
|
+
}, HOLD_CHECK_GRACE_MS);
|
|
1437
|
+
provisional.unref?.();
|
|
1438
|
+
let deadline: ReturnType<typeof setTimeout> | undefined;
|
|
1439
|
+
const timedOut = new Promise<"timed-out">((resolveTimeout) => {
|
|
1440
|
+
deadline = setTimeout(() => resolveTimeout("timed-out"), RECIPIENT_RECEIPT_TIMEOUT_MS);
|
|
1441
|
+
deadline.unref?.();
|
|
1442
|
+
});
|
|
1443
|
+
const decide = async () => {
|
|
1444
|
+
const read = await this.state.outbox.read(waiter.messageId);
|
|
1445
|
+
const unclaimed = read.status === "found" && read.row.state === "queued" && read.row.claimAttempts === 0;
|
|
1446
|
+
return { read, queuedAt: unclaimed ? await this.positionIfRecipientLeft(waiter) : undefined };
|
|
1447
|
+
};
|
|
1448
|
+
let decision: Awaited<ReturnType<typeof decide>> | "timed-out";
|
|
1449
|
+
try {
|
|
1450
|
+
decision = await Promise.race([decide(), timedOut]);
|
|
1451
|
+
} finally {
|
|
1452
|
+
clearTimeout(provisional);
|
|
1453
|
+
clearTimeout(deadline);
|
|
1454
|
+
}
|
|
1455
|
+
if (this.waiters.get(waiter.messageId) !== waiter) return;
|
|
1456
|
+
if (decision === "timed-out") {
|
|
1457
|
+
this.expireUncertain(waiter);
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
const { read, queuedAt } = decision;
|
|
1461
|
+
if (read.status === "found" && read.row.state === "delivered") {
|
|
1462
|
+
await this.finishDelivered(waiter);
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
const remaining = this.capRemaining(waiter);
|
|
1466
|
+
const savingAge = read.status === "found" && read.row.state === "claimed" ? read.row.savingAgeMs : undefined;
|
|
1467
|
+
if (savingAge !== undefined && savingAge < RECIPIENT_RECEIPT_TIMEOUT_MS && remaining > 0) {
|
|
1468
|
+
const timeoutMs = Math.min(RECIPIENT_RECEIPT_TIMEOUT_MS, remaining);
|
|
1469
|
+
this.armWaiter(waiter, timeoutMs);
|
|
1470
|
+
this.holdSender(waiter, timeoutMs);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
if (queuedAt !== undefined && this.settleQueued(waiter, queuedAt)) return;
|
|
1474
|
+
this.expireUncertain(waiter);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
private holdSender(waiter: Waiter, timeoutMs: number): void {
|
|
1478
|
+
if (this.waiters.get(waiter.messageId) !== waiter) return;
|
|
1479
|
+
this.host.write(waiter.socket, {
|
|
1480
|
+
type: "send_held",
|
|
1481
|
+
messageId: waiter.messageId,
|
|
1482
|
+
...(waiter.attemptId === undefined ? {} : { attemptId: waiter.attemptId }),
|
|
1483
|
+
timeoutMs,
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
private expireUncertain(waiter: Waiter): void {
|
|
1488
|
+
if (this.waiters.get(waiter.messageId) !== waiter) return;
|
|
1489
|
+
this.dropWaiter(waiter);
|
|
1490
|
+
this.host.write(waiter.socket, {
|
|
1491
|
+
type: "delivery_failed",
|
|
1492
|
+
messageId: waiter.messageId,
|
|
1493
|
+
attemptId: waiter.attemptId,
|
|
1494
|
+
reason: UNCERTAIN_RECEIPT_REASON,
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1298
1498
|
private dropWaiter(waiter: Waiter): void {
|
|
1299
1499
|
if (this.waiters.get(waiter.messageId) === waiter) this.waiters.delete(waiter.messageId);
|
|
1300
1500
|
if (waiter.timer !== undefined) clearTimeout(waiter.timer);
|
|
1501
|
+
if (waiter.capTimer !== undefined) clearTimeout(waiter.capTimer);
|
|
1301
1502
|
}
|
|
1302
1503
|
|
|
1303
1504
|
// ---------------------------------------------------------------------------------------------------------
|
|
@@ -151,6 +151,11 @@ export interface OutboxRow {
|
|
|
151
151
|
readonly claimAttempts: number;
|
|
152
152
|
/** Present only when `state` is `failed`; verbatim. */
|
|
153
153
|
readonly failureReason?: string;
|
|
154
|
+
/**
|
|
155
|
+
* #3983: store-clock milliseconds since the claimed row's recipient last reported it is still saving it; absent
|
|
156
|
+
* when it never reported, and on rows from a store that does not record it.
|
|
157
|
+
*/
|
|
158
|
+
readonly savingAgeMs?: number;
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
export interface OutboxClaim {
|
|
@@ -217,6 +222,11 @@ export interface OutboxStore {
|
|
|
217
222
|
*/
|
|
218
223
|
claim(request: OutboxClaimRequest): Promise<OutboxClaimResult>;
|
|
219
224
|
renewClaim(claim: OutboxClaim): Promise<OutboxRenewClaimResult>;
|
|
225
|
+
/**
|
|
226
|
+
* #3983: the claim's recipient reports it is still saving the row, so the sender's broker keeps its sender
|
|
227
|
+
* waiting. Optional: a store without it never extends a sender's wait.
|
|
228
|
+
*/
|
|
229
|
+
reportSaving?(claim: OutboxClaim): Promise<OutboxRenewClaimResult>;
|
|
220
230
|
/** Return the row to `queued`, for example when its recipient disconnected before acknowledging. */
|
|
221
231
|
releaseClaim(claim: OutboxClaim): Promise<OutboxReleaseClaimResult>;
|
|
222
232
|
/**
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type net from "node:net";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DELIVERED_MESSAGE_TTL_MS,
|
|
4
|
+
RECIPIENT_RECEIPT_TIMEOUT_MS,
|
|
5
|
+
RECIPIENT_SAVE_MAX_WAIT_MS,
|
|
6
|
+
} from "../retry-policy.js";
|
|
3
7
|
import type { BrokerMessage } from "../types.js";
|
|
4
8
|
|
|
5
9
|
/** #3761: the sender is told this when the recipient did not confirm a save within the sender's wait. */
|
|
@@ -21,12 +25,19 @@ export interface AwaitedReceipt<Recipient> {
|
|
|
21
25
|
readonly complete: (recipient: Recipient, sender: ReceiptSender | undefined) => void;
|
|
22
26
|
/** Runs when the sender's wait ends before the recipient confirms. */
|
|
23
27
|
readonly expire: (sender: ReceiptSender) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Runs when a recipient still saving the message extends the sender's wait: tells the sender to keep waiting
|
|
30
|
+
* for up to `timeoutMs` more (a `send_held` frame).
|
|
31
|
+
*/
|
|
32
|
+
readonly hold?: (sender: ReceiptSender, timeoutMs: number) => void;
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
interface PendingReceipt<Recipient> extends AwaitedReceipt<Recipient> {
|
|
27
36
|
sender: ReceiptSender | undefined;
|
|
28
37
|
senderTimer: ReturnType<typeof setTimeout> | undefined;
|
|
29
38
|
retentionTimer: ReturnType<typeof setTimeout>;
|
|
39
|
+
/** When the sender started waiting; a save that keeps reporting progress still ends its wait after the cap. */
|
|
40
|
+
readonly waitingSince: number;
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
/**
|
|
@@ -51,15 +62,38 @@ export class RecipientReceipts<Recipient> {
|
|
|
51
62
|
retentionTimer: setTimeout(() => {
|
|
52
63
|
if (this.pending.get(receipt.messageId) === entry) this.drop(receipt.messageId);
|
|
53
64
|
}, this.retentionMs),
|
|
65
|
+
waitingSince: Date.now(),
|
|
54
66
|
};
|
|
55
67
|
entry.retentionTimer.unref?.();
|
|
68
|
+
this.armSenderWait(entry, this.senderTimeoutMs);
|
|
69
|
+
this.pending.set(receipt.messageId, entry);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private armSenderWait(entry: PendingReceipt<Recipient>, timeoutMs: number): void {
|
|
73
|
+
if (entry.senderTimer !== undefined) clearTimeout(entry.senderTimer);
|
|
56
74
|
entry.senderTimer = setTimeout(() => {
|
|
57
75
|
entry.senderTimer = undefined;
|
|
58
76
|
const waiting = entry.sender;
|
|
59
77
|
entry.sender = undefined;
|
|
60
|
-
if (waiting !== undefined && this.pending.get(
|
|
61
|
-
},
|
|
62
|
-
|
|
78
|
+
if (waiting !== undefined && this.pending.get(entry.messageId) === entry) entry.expire(waiting);
|
|
79
|
+
}, timeoutMs);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The recipient this message was written to reports it is still saving it (for example a pooled stage whose
|
|
84
|
+
* journal commit waits behind other writers): the sender keeps waiting another full receipt wait instead of
|
|
85
|
+
* hearing the outcome is uncertain. A recipient that dies stops reporting, so its wait still ends; and no save
|
|
86
|
+
* holds a sender past `RECIPIENT_SAVE_MAX_WAIT_MS` (#3983).
|
|
87
|
+
*/
|
|
88
|
+
extend(messageId: string, targetIdentity: string): boolean {
|
|
89
|
+
const entry = this.pending.get(messageId);
|
|
90
|
+
if (entry === undefined || entry.targetIdentity !== targetIdentity || entry.sender === undefined) return false;
|
|
91
|
+
const remaining = entry.waitingSince + RECIPIENT_SAVE_MAX_WAIT_MS - Date.now();
|
|
92
|
+
if (remaining <= 0) return false;
|
|
93
|
+
const timeoutMs = Math.min(this.senderTimeoutMs, remaining);
|
|
94
|
+
this.armSenderWait(entry, timeoutMs);
|
|
95
|
+
entry.hold?.(entry.sender, timeoutMs);
|
|
96
|
+
return true;
|
|
63
97
|
}
|
|
64
98
|
|
|
65
99
|
/** Settle a held message when the confirming recipient is the one it was written to. */
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
parseLegacyWorkflowStageTarget,
|
|
7
7
|
parseWorkflowStageTarget,
|
|
8
8
|
} from "../workflow-stage-target.js";
|
|
9
|
-
import { normalizeGroup } from "../group.js";
|
|
9
|
+
import { normalizeGroup, workflowInvocationRefusalFor } from "../group.js";
|
|
10
10
|
import { isMessage } from "./client-message-validation.js";
|
|
11
11
|
import {
|
|
12
12
|
resolveSessionTarget,
|
|
@@ -541,7 +541,11 @@ export function handleBrokerSend(
|
|
|
541
541
|
type: "delivery_failed",
|
|
542
542
|
messageId: message.id,
|
|
543
543
|
attemptId,
|
|
544
|
-
|
|
544
|
+
// #4006: a live workflow stage is reached through its invocation; name the group to join,
|
|
545
|
+
// unless the sender is a workflow stage that joining could never authorize.
|
|
546
|
+
reason: liveWorkflowTarget !== undefined && workflowTarget !== undefined
|
|
547
|
+
? workflowInvocationRefusalFor(fromSession.registrationGroup, `workflow:${workflowTarget.rootRunId}`)
|
|
548
|
+
: "Target session is in a different intercom group",
|
|
545
549
|
});
|
|
546
550
|
return;
|
|
547
551
|
}
|
|
@@ -744,6 +748,9 @@ export function handleBrokerSend(
|
|
|
744
748
|
expire: (waiting) => {
|
|
745
749
|
write(waiting.socket, { type: "delivery_failed", messageId: message.id, attemptId: waiting.attemptId, reason: UNCERTAIN_RECEIPT_REASON });
|
|
746
750
|
},
|
|
751
|
+
hold: (waiting, timeoutMs) => {
|
|
752
|
+
write(waiting.socket, { type: "send_held", messageId: message.id, attemptId: waiting.attemptId, timeoutMs });
|
|
753
|
+
},
|
|
747
754
|
},
|
|
748
755
|
senderWaiting,
|
|
749
756
|
);
|
|
@@ -34,6 +34,8 @@ interface OutboxRecord extends SqliteRow {
|
|
|
34
34
|
readonly claim_token: string | null;
|
|
35
35
|
/** Store-clock milliseconds since the claim was taken or renewed; null when unclaimed. */
|
|
36
36
|
readonly claim_age: SqliteValue;
|
|
37
|
+
/** Store-clock milliseconds since the claim's recipient last reported it is still saving the row; null when never. */
|
|
38
|
+
readonly saving_age: SqliteValue;
|
|
37
39
|
readonly failure_reason: string | null;
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -51,6 +53,9 @@ function toRow(record: OutboxRecord): OutboxRow {
|
|
|
51
53
|
state: record.state,
|
|
52
54
|
claimAttempts: Number(record.claim_attempts),
|
|
53
55
|
...(failureReason === null ? {} : { failureReason: JSON.parse(failureReason) as string }),
|
|
56
|
+
...(record.saving_age === null || record.saving_age === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: { savingAgeMs: Number(record.saving_age) }),
|
|
54
59
|
};
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -81,10 +86,22 @@ export async function openSqlOutboxStore(options: IntercomBrokerStateOptions): P
|
|
|
81
86
|
);
|
|
82
87
|
CREATE INDEX IF NOT EXISTS ${table}_target ON ${table}(target_identity, state, sequence);
|
|
83
88
|
`);
|
|
89
|
+
// #3983: added later; a table from an older broker gets the column, and an older broker ignores it.
|
|
90
|
+
const known = await database.all<{ name: string }>(`PRAGMA table_info(${table})`);
|
|
91
|
+
if (!known.some((column) => column.name === "saving_at")) {
|
|
92
|
+
try {
|
|
93
|
+
await database.exec(`ALTER TABLE ${table} ADD COLUMN saving_at INTEGER`);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
// Another broker added it between the check and the change.
|
|
96
|
+
if (!/duplicate column/i.test(error instanceof Error ? `${error.message} ${String(error.cause ?? "")}` : ""))
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
84
100
|
|
|
85
101
|
const columns = `sequence, message_id, target_identity, origin_broker_id, digest, frame_json, state,
|
|
86
102
|
claim_attempts, claimant_id, claim_token, failure_reason,
|
|
87
|
-
CASE WHEN claim_renewed_at IS NULL THEN NULL ELSE MAX(0, ${STORE_NOW} - claim_renewed_at) END AS claim_age
|
|
103
|
+
CASE WHEN claim_renewed_at IS NULL THEN NULL ELSE MAX(0, ${STORE_NOW} - claim_renewed_at) END AS claim_age,
|
|
104
|
+
CASE WHEN saving_at IS NULL THEN NULL ELSE MAX(0, ${STORE_NOW} - saving_at) END AS saving_age`;
|
|
88
105
|
const select = (executor: SqliteExecutor, messageId: string) =>
|
|
89
106
|
executor.get<OutboxRecord>(`SELECT ${columns} FROM ${table} WHERE message_id = ?`, messageId);
|
|
90
107
|
const undelivered = (executor: SqliteExecutor, targetIdentity: string) =>
|
|
@@ -121,7 +138,7 @@ export async function openSqlOutboxStore(options: IntercomBrokerStateOptions): P
|
|
|
121
138
|
): Promise<OutboxRow> => {
|
|
122
139
|
await executor.run(
|
|
123
140
|
`UPDATE ${table} SET state = ?, failure_reason = ?, settled_at = ${STORE_NOW},
|
|
124
|
-
claimant_id = NULL, claim_token = NULL, claim_renewed_at = NULL
|
|
141
|
+
claimant_id = NULL, claim_token = NULL, claim_renewed_at = NULL, saving_at = NULL
|
|
125
142
|
WHERE message_id = ?`,
|
|
126
143
|
state,
|
|
127
144
|
reason === null ? null : JSON.stringify(reason),
|
|
@@ -175,7 +192,7 @@ export async function openSqlOutboxStore(options: IntercomBrokerStateOptions): P
|
|
|
175
192
|
const token = randomUUID();
|
|
176
193
|
await executor.run(
|
|
177
194
|
`UPDATE ${table} SET state = 'claimed', claim_attempts = claim_attempts + 1, claimant_id = ?,
|
|
178
|
-
claim_token = ?, claim_renewed_at = ${STORE_NOW}
|
|
195
|
+
claim_token = ?, claim_renewed_at = ${STORE_NOW}, saving_at = NULL
|
|
179
196
|
WHERE message_id = ?`,
|
|
180
197
|
request.claimantId,
|
|
181
198
|
token,
|
|
@@ -201,10 +218,18 @@ export async function openSqlOutboxStore(options: IntercomBrokerStateOptions): P
|
|
|
201
218
|
});
|
|
202
219
|
},
|
|
203
220
|
|
|
221
|
+
reportSaving(claim) {
|
|
222
|
+
return withClaim(claim, async (executor) => {
|
|
223
|
+
await executor.run(`UPDATE ${table} SET saving_at = ${STORE_NOW} WHERE message_id = ?`, claim.messageId);
|
|
224
|
+
return { status: "renewed" } as const;
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
|
|
204
228
|
releaseClaim(claim) {
|
|
205
229
|
return withClaim(claim, async (executor) => {
|
|
206
230
|
await executor.run(
|
|
207
|
-
`UPDATE ${table} SET state = 'queued', claimant_id = NULL, claim_token = NULL, claim_renewed_at = NULL
|
|
231
|
+
`UPDATE ${table} SET state = 'queued', claimant_id = NULL, claim_token = NULL, claim_renewed_at = NULL,
|
|
232
|
+
saving_at = NULL
|
|
208
233
|
WHERE message_id = ?`,
|
|
209
234
|
claim.messageId,
|
|
210
235
|
);
|
|
@@ -123,3 +123,28 @@ export function resolveHomeGroup(
|
|
|
123
123
|
}
|
|
124
124
|
return DEFAULT_GROUP;
|
|
125
125
|
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* #4006: refusal for a sender outside a workflow invocation. It names the documented
|
|
129
|
+
* `workflow:<rootRunId>` group to join, never an internal identifier.
|
|
130
|
+
*/
|
|
131
|
+
export function workflowInvocationGroupRefusal(invocationGroup: string): string {
|
|
132
|
+
return `Target workflow run is in a different intercom group; join ${normalizeGroup(invocationGroup)} to send to its stages`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Refusal for a workflow stage registered outside a run's invocation group: another run's stage,
|
|
137
|
+
* or an isolated subgroup stage of the same run. Joining never grants it control, so the reason
|
|
138
|
+
* must not tell it to join.
|
|
139
|
+
*/
|
|
140
|
+
export function workflowStageInvocationRefusal(senderRegistrationGroup: string, invocationGroup: string): string {
|
|
141
|
+
return `A workflow stage in ${normalizeGroup(senderRegistrationGroup)} cannot send to stages of ${normalizeGroup(invocationGroup)}; joining that group does not grant it control`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The precise refusal for a sender registered in `senderRegistrationGroup` reaching stages of `invocationGroup`. */
|
|
145
|
+
export function workflowInvocationRefusalFor(senderRegistrationGroup: string | undefined, invocationGroup: string): string {
|
|
146
|
+
const registration = normalizeGroup(senderRegistrationGroup);
|
|
147
|
+
return registration.startsWith("workflow:") && registration !== normalizeGroup(invocationGroup)
|
|
148
|
+
? workflowStageInvocationRefusal(registration, invocationGroup)
|
|
149
|
+
: workflowInvocationGroupRefusal(invocationGroup);
|
|
150
|
+
}
|