@bastani/atomic 0.9.39-alpha.2 → 0.9.40-alpha.1
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 +42 -0
- package/dist/builtin/intercom/CHANGELOG.md +20 -0
- package/dist/builtin/intercom/broker/broker.ts +523 -72
- package/dist/builtin/intercom/broker/client.ts +136 -5
- package/dist/builtin/intercom/broker/database-wake-up-bus.ts +274 -0
- package/dist/builtin/intercom/broker/group-membership.ts +2 -2
- package/dist/builtin/intercom/broker/multinode-router.ts +1500 -0
- package/dist/builtin/intercom/broker/multinode-state-store.ts +569 -0
- package/dist/builtin/intercom/broker/open-broker-state.ts +160 -0
- package/dist/builtin/intercom/broker/paths.ts +6 -0
- package/dist/builtin/intercom/broker/pending-send-registry.ts +14 -0
- package/dist/builtin/intercom/broker/recipient-receipts.ts +93 -0
- package/dist/builtin/intercom/broker/restoring-recipients.ts +263 -0
- package/dist/builtin/intercom/broker/send-handler.ts +241 -25
- package/dist/builtin/intercom/broker/session-id-reclaim.ts +41 -0
- package/dist/builtin/intercom/broker/socket-writes.ts +47 -0
- package/dist/builtin/intercom/broker/spawn.ts +10 -2
- package/dist/builtin/intercom/broker/sql-held-send-store.ts +96 -0
- package/dist/builtin/intercom/broker/sql-known-session-store.ts +129 -0
- package/dist/builtin/intercom/broker/sql-lease-store.ts +194 -0
- package/dist/builtin/intercom/broker/sql-outbox-store.ts +249 -0
- package/dist/builtin/intercom/broker/sql-pending-ask-store.ts +105 -0
- package/dist/builtin/intercom/broker/sql-restoring-reservation-store.ts +295 -0
- package/dist/builtin/intercom/broker/sql-sent-message-authority.ts +126 -0
- package/dist/builtin/intercom/broker/supervisor-channel.ts +10 -0
- package/dist/builtin/intercom/broker/wake-up-bus.ts +60 -0
- package/dist/builtin/intercom/index.bundle.mjs +793 -91
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/retry-policy.ts +4 -0
- package/dist/builtin/intercom/session-target.ts +24 -0
- package/dist/builtin/intercom/types.ts +57 -3
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +37 -0
- package/dist/builtin/subagents/README.md +18 -12
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/skills/agent-browser/SKILL.md +1 -1
- package/dist/builtin/subagents/skills/cua-driver/BROWSER.md +55 -8
- package/dist/builtin/subagents/skills/cua-driver/EMBEDDING.md +7 -2
- package/dist/builtin/subagents/skills/cua-driver/LINUX.md +7 -1
- package/dist/builtin/subagents/skills/cua-driver/MACOS.md +151 -27
- package/dist/builtin/subagents/skills/cua-driver/RUNTIME.md +1 -1
- package/dist/builtin/subagents/skills/cua-driver/SKILL.md +33 -17
- package/dist/builtin/subagents/skills/cua-driver/WINDOWS.md +5 -0
- package/dist/builtin/subagents/skills/cua-driver/WORKFLOW.md +46 -8
- package/dist/builtin/subagents/skills/subagent/SKILL.md +18 -20
- package/dist/builtin/subagents/src/extension/index.bundle.mjs +8032 -6034
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +32 -0
- package/dist/builtin/workflows/README.md +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +8 -8
- package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/adapt.native.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/audit.md +4 -2
- package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +22 -9
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/distill.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/extract.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/harden.md +2 -4
- package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +4 -4
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +6 -6
- package/dist/builtin/workflows/skills/impeccable/reference/onboard.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +10 -0
- package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/VERSION +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/impeccable +6 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/impeccable.cmd +47 -22
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-dom.js +34 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-session.js +25 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +33 -8
- package/dist/builtin/workflows/src/durable/journal/resolution.d.ts +7 -7
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +22855 -22308
- package/dist/builtin/workflows/src/index.js +106 -35
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +16 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session-extension-bindings.d.ts +2 -0
- package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
- package/dist/core/agent-session-extension-bindings.js +6 -6
- package/dist/core/agent-session-extension-bindings.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +6 -3
- 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-persistent-custom-messages.d.ts +7 -2
- package/dist/core/agent-session-persistent-custom-messages.d.ts.map +1 -1
- package/dist/core/agent-session-persistent-custom-messages.js +29 -12
- package/dist/core/agent-session-persistent-custom-messages.js.map +1 -1
- package/dist/core/agent-session-recovery.d.ts +14 -0
- package/dist/core/agent-session-recovery.d.ts.map +1 -0
- package/dist/core/agent-session-recovery.js +83 -0
- package/dist/core/agent-session-recovery.js.map +1 -0
- package/dist/core/agent-session-tasks.d.ts.map +1 -1
- package/dist/core/agent-session-tasks.js +4 -6
- package/dist/core/agent-session-tasks.js.map +1 -1
- package/dist/core/agent-session-types.d.ts +13 -0
- package/dist/core/agent-session-types.d.ts.map +1 -1
- package/dist/core/agent-session-types.js.map +1 -1
- package/dist/core/agent-session.d.ts +2 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +4 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/api-types.d.ts +6 -2
- package/dist/core/extensions/api-types.d.ts.map +1 -1
- package/dist/core/extensions/api-types.js.map +1 -1
- package/dist/core/extensions/event-results.d.ts +21 -0
- package/dist/core/extensions/event-results.d.ts.map +1 -1
- package/dist/core/extensions/event-results.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader-api.d.ts.map +1 -1
- package/dist/core/extensions/loader-api.js +4 -0
- package/dist/core/extensions/loader-api.js.map +1 -1
- package/dist/core/extensions/runner-events.d.ts +9 -2
- package/dist/core/extensions/runner-events.d.ts.map +1 -1
- package/dist/core/extensions/runner-events.js +29 -0
- package/dist/core/extensions/runner-events.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +5 -3
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +7 -2
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/runtime-types.d.ts +1 -0
- package/dist/core/extensions/runtime-types.d.ts.map +1 -1
- package/dist/core/extensions/runtime-types.js.map +1 -1
- package/dist/core/extensions/session-events.d.ts +30 -2
- package/dist/core/extensions/session-events.d.ts.map +1 -1
- package/dist/core/extensions/session-events.js.map +1 -1
- package/dist/core/sdk-types.d.ts +3 -2
- package/dist/core/sdk-types.d.ts.map +1 -1
- package/dist/core/sdk-types.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +3 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-storage.d.ts.map +1 -1
- package/dist/core/session-storage.js +15 -9
- package/dist/core/session-storage.js.map +1 -1
- package/dist/core/storage/process-selection.d.ts +39 -0
- package/dist/core/storage/process-selection.d.ts.map +1 -0
- package/dist/core/storage/process-selection.js +154 -0
- package/dist/core/storage/process-selection.js.map +1 -0
- package/dist/core/storage/provider.d.ts +6 -1
- package/dist/core/storage/provider.d.ts.map +1 -1
- package/dist/core/storage/provider.js +3 -2
- package/dist/core/storage/provider.js.map +1 -1
- package/dist/core/storage/selection.d.ts +1 -1
- package/dist/core/storage/selection.d.ts.map +1 -1
- package/dist/core/storage/selection.js.map +1 -1
- package/dist/core/storage/session/jsonl-session-store.d.ts +2 -0
- package/dist/core/storage/session/jsonl-session-store.d.ts.map +1 -1
- package/dist/core/storage/session/jsonl-session-store.js +38 -2
- package/dist/core/storage/session/jsonl-session-store.js.map +1 -1
- package/dist/core/tasks/completion.d.ts +11 -2
- package/dist/core/tasks/completion.d.ts.map +1 -1
- package/dist/core/tasks/completion.js +40 -16
- package/dist/core/tasks/completion.js.map +1 -1
- package/dist/core/tasks/pause.d.ts +4 -1
- package/dist/core/tasks/pause.d.ts.map +1 -1
- package/dist/core/tasks/pause.js +8 -5
- package/dist/core/tasks/pause.js.map +1 -1
- package/dist/index-extensions.d.ts +1 -1
- package/dist/index-extensions.d.ts.map +1 -1
- package/dist/index-extensions.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +7 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-rendering.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-rendering.js +11 -1
- package/dist/modes/interactive/components/chat-session-host-rendering.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-state.d.ts +2 -0
- package/dist/modes/interactive/components/chat-session-host-state.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-state.js +2 -0
- package/dist/modes/interactive/components/chat-session-host-state.js.map +1 -1
- package/dist/modes/interactive/interactive-deferred-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-deferred-startup.js +4 -0
- package/dist/modes/interactive/interactive-deferred-startup.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts +2 -1
- 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-render-chat.js +3 -0
- package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
- package/dist/modes/interactive/interactive-session-routing.js +2 -0
- package/dist/modes/interactive/interactive-session-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-session-runtime.d.ts +2 -1
- package/dist/modes/interactive/interactive-session-runtime.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-session-runtime.js +29 -0
- package/dist/modes/interactive/interactive-session-runtime.js.map +1 -1
- package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-startup.js +4 -1
- package/dist/modes/interactive/interactive-startup.js.map +1 -1
- package/dist/modes/interactive-engine/engine-args.d.ts.map +1 -1
- package/dist/modes/interactive-engine/engine-args.js +4 -0
- package/dist/modes/interactive-engine/engine-args.js.map +1 -1
- package/dist/modes/interactive-engine/engine-render-service.d.ts.map +1 -1
- package/dist/modes/interactive-engine/engine-render-service.js +4 -4
- package/dist/modes/interactive-engine/engine-render-service.js.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.js +4 -0
- package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
- package/dist/modes/interactive-engine/remote-renderer.js +1 -1
- package/dist/modes/interactive-engine/remote-renderer.js.map +1 -1
- package/dist/modes/rpc/rpc-client-api.d.ts +4 -1
- package/dist/modes/rpc/rpc-client-api.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client-api.js +4 -0
- package/dist/modes/rpc/rpc-client-api.js.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.js +3 -0
- package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
- package/dist/modes/rpc/rpc-command-timeouts.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-command-timeouts.js +1 -0
- package/dist/modes/rpc/rpc-command-timeouts.js.map +1 -1
- package/dist/modes/rpc/rpc-input-scheduler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-input-scheduler.js +6 -1
- package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +12 -1
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +44 -11
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/tools-manager.js +1 -1
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/background-tasks.md +5 -3
- package/docs/computer-use.md +2 -2
- package/docs/environment-variables.md +4 -1
- package/docs/extensions/events.md +29 -2
- package/docs/getting-started/first-session.md +1 -1
- package/docs/guides/intercom.md +1 -1
- package/docs/intercom/operations.md +103 -1
- package/docs/intercom/reference.md +10 -3
- package/docs/intercom.md +8 -6
- package/docs/keybindings.md +1 -1
- package/docs/models.md +1 -1
- package/docs/providers.md +4 -2
- package/docs/reference/cli.md +5 -0
- package/docs/rpc/protocol.md +19 -0
- package/docs/rpc.md +3 -0
- package/docs/sdk/web-server.md +1 -1
- package/docs/sdk.md +5 -2
- package/docs/sessions.md +17 -1
- package/docs/settings.md +5 -3
- package/docs/skills.md +1 -1
- package/docs/storage.md +54 -3
- package/docs/subagents.md +49 -9
- package/docs/usage.md +1 -1
- package/docs/windows.md +14 -4
- package/docs/workflows/api-reference.md +1 -1
- package/docs/workflows/authoring.md +1 -1
- package/docs/workflows/operations.md +10 -6
- package/npm-shrinkwrap.json +37 -37
- package/package.json +4 -4
|
@@ -22,11 +22,14 @@ import { isMessage, isSessionInfo } from "./client-message-validation.js";
|
|
|
22
22
|
import { normalizeGroups } from "../group.js";
|
|
23
23
|
import { IntercomClientDisconnectedError } from "../recoverable-disconnect.js";
|
|
24
24
|
import { IntercomLiveRouteRefusedError, isLiveWorkflowStageRouteRefusalCode } from "../live-route-refusal.js";
|
|
25
|
+
import { SEND_RESPONSE_TIMEOUT_MS } from "../retry-policy.js";
|
|
25
26
|
|
|
26
27
|
const BROKER_SOCKET = getBrokerSocketPath();
|
|
27
28
|
const GROUP_REQUEST_TIMEOUT_MS = 5000;
|
|
28
29
|
const PRESENCE_ACK_TIMEOUT_MS = 5000;
|
|
29
|
-
|
|
30
|
+
const RESTORING_RECIPIENT_REQUEST_TIMEOUT_MS = 5000;
|
|
31
|
+
/** Extra wait past a broker hold, so the broker's own outcome for a held send always arrives first (#3760). */
|
|
32
|
+
const HELD_SEND_GRACE_MS = 5000;
|
|
30
33
|
|
|
31
34
|
export interface SendOptions {
|
|
32
35
|
text: string;
|
|
@@ -41,10 +44,17 @@ export interface SendOptions {
|
|
|
41
44
|
requirePendingReply?: true;
|
|
42
45
|
/** An explicit reply selector must resolve to this frozen recipient, never a namesake. */
|
|
43
46
|
expectedRecipientId?: string;
|
|
44
|
-
/** Broker-authorized identity for
|
|
47
|
+
/** Broker-authorized recipient identity for an ask, delivered before its recipient can reply. */
|
|
45
48
|
onReplyTarget?: (sessionId: string) => void;
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
export interface IntercomClientOptions {
|
|
52
|
+
/** Intercom session id held before a restart or reconnect; the broker grants it only when it is free (#3760). */
|
|
53
|
+
readonly preferredSessionId?: string;
|
|
54
|
+
/** #3761: this client confirms each received message with `acknowledgeReceived` once it has saved it. */
|
|
55
|
+
readonly acknowledgesReceipt?: true;
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
export interface SendResult {
|
|
49
59
|
id: string;
|
|
50
60
|
delivered: boolean;
|
|
@@ -152,6 +162,10 @@ function isWorkflowStageRosterEntries(value: unknown): value is WorkflowStageRos
|
|
|
152
162
|
|
|
153
163
|
export class IntercomClient extends EventEmitter {
|
|
154
164
|
private readonly returnAddress: string;
|
|
165
|
+
private readonly preferredSessionId: string | undefined;
|
|
166
|
+
private readonly acknowledgesReceipt: boolean;
|
|
167
|
+
/** #3761: the broker of the current registration echoed `acknowledgesReceipt`, so it understands `received`. */
|
|
168
|
+
private brokerAcknowledgesReceipt = false;
|
|
155
169
|
private socket: net.Socket | null = null;
|
|
156
170
|
private _sessionId: string | null = null;
|
|
157
171
|
private _supervisorSessionId: string | null = null;
|
|
@@ -174,13 +188,16 @@ export class IntercomClient extends EventEmitter {
|
|
|
174
188
|
resolve: (authorization: SupervisorAuthorization) => void;
|
|
175
189
|
reject: (error: Error) => void;
|
|
176
190
|
}>();
|
|
191
|
+
private pendingRestoringRecipientRequests = new Map<string, { resolve(): void; reject(error: Error): void }>();
|
|
177
192
|
private disconnecting = false;
|
|
178
193
|
private disconnectError: Error | null = null;
|
|
179
194
|
|
|
180
|
-
constructor(returnAddress: string = randomUUID()) {
|
|
195
|
+
constructor(returnAddress: string = randomUUID(), options: IntercomClientOptions = {}) {
|
|
181
196
|
super();
|
|
182
197
|
if (returnAddress.length === 0) throw new Error("Intercom return address must not be empty");
|
|
183
198
|
this.returnAddress = returnAddress;
|
|
199
|
+
this.preferredSessionId = options.preferredSessionId;
|
|
200
|
+
this.acknowledgesReceipt = options.acknowledgesReceipt === true;
|
|
184
201
|
}
|
|
185
202
|
|
|
186
203
|
private failPending(error: Error): void {
|
|
@@ -188,12 +205,14 @@ export class IntercomClient extends EventEmitter {
|
|
|
188
205
|
for (const pending of this.pendingGroupLists.values()) pending.reject(error);
|
|
189
206
|
for (const pending of this.pendingLists.values()) pending.reject(error);
|
|
190
207
|
for (const pending of this.pendingSupervisorAuthorizations.values()) pending.reject(error);
|
|
208
|
+
for (const pending of this.pendingRestoringRecipientRequests.values()) pending.reject(error);
|
|
191
209
|
for (const pending of this.pendingPresence.values()) pending.reject(error);
|
|
192
210
|
for (const pending of this.pendingMemberships.values()) pending.reject(error);
|
|
193
211
|
for (const pending of this.pendingLiveWorkflowStageRoutes.values()) pending.reject(error);
|
|
194
212
|
this.pendingLists.clear();
|
|
195
213
|
this.pendingGroupLists.clear();
|
|
196
214
|
this.pendingSupervisorAuthorizations.clear();
|
|
215
|
+
this.pendingRestoringRecipientRequests.clear();
|
|
197
216
|
this.pendingPresence.clear();
|
|
198
217
|
this.pendingMemberships.clear();
|
|
199
218
|
this.pendingLiveWorkflowStageRoutes.clear();
|
|
@@ -298,6 +317,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
298
317
|
}
|
|
299
318
|
this._sessionId = null;
|
|
300
319
|
this._supervisorSessionId = null;
|
|
320
|
+
this.brokerAcknowledgesReceipt = false;
|
|
301
321
|
this._messageSource = undefined;
|
|
302
322
|
this.disconnectError = null;
|
|
303
323
|
if (connectionEstablished && !wasDisconnecting) {
|
|
@@ -371,6 +391,8 @@ export class IntercomClient extends EventEmitter {
|
|
|
371
391
|
session,
|
|
372
392
|
...(registrationGroup !== undefined ? { registrationGroup } : {}),
|
|
373
393
|
returnAddress: this.returnAddress,
|
|
394
|
+
...(this.preferredSessionId === undefined ? {} : { preferredSessionId: this.preferredSessionId }),
|
|
395
|
+
...(this.acknowledgesReceipt ? { acknowledgesReceipt: true as const } : {}),
|
|
374
396
|
...(supervisor ? { supervisor } : {}),
|
|
375
397
|
...(supervisorOwnerToken ? { supervisorOwnerToken } : {}),
|
|
376
398
|
});
|
|
@@ -402,7 +424,8 @@ export class IntercomClient extends EventEmitter {
|
|
|
402
424
|
switch (brokerMessage.type) {
|
|
403
425
|
case "registered": {
|
|
404
426
|
if (typeof brokerMessage.sessionId !== "string"
|
|
405
|
-
|| (brokerMessage.supervisorSessionId !== undefined && typeof brokerMessage.supervisorSessionId !== "string")
|
|
427
|
+
|| (brokerMessage.supervisorSessionId !== undefined && typeof brokerMessage.supervisorSessionId !== "string")
|
|
428
|
+
|| (brokerMessage.acknowledgesReceipt !== undefined && brokerMessage.acknowledgesReceipt !== true)) {
|
|
406
429
|
throw new Error("Invalid registered message");
|
|
407
430
|
}
|
|
408
431
|
|
|
@@ -413,6 +436,8 @@ export class IntercomClient extends EventEmitter {
|
|
|
413
436
|
this._supervisorSessionId = typeof brokerMessage.supervisorSessionId === "string"
|
|
414
437
|
? brokerMessage.supervisorSessionId
|
|
415
438
|
: null;
|
|
439
|
+
// #3761: a broker from before receipts never echoes the capability and drops a client that sends `received`.
|
|
440
|
+
this.brokerAcknowledgesReceipt = this.acknowledgesReceipt && brokerMessage.acknowledgesReceipt === true;
|
|
416
441
|
this.emit("_registered", { type: "registered", sessionId: brokerMessage.sessionId });
|
|
417
442
|
break;
|
|
418
443
|
}
|
|
@@ -573,6 +598,12 @@ export class IntercomClient extends EventEmitter {
|
|
|
573
598
|
} satisfies StickyLiveDeliveredNotice);
|
|
574
599
|
break;
|
|
575
600
|
}
|
|
601
|
+
case "pending_stage_route_revoked": {
|
|
602
|
+
const { runId } = brokerMessage;
|
|
603
|
+
if (typeof runId !== "string") throw new Error("Invalid pending-stage route revocation");
|
|
604
|
+
this.emit("pending_stage_route_revoked", { runId });
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
576
607
|
case "live_workflow_stage_route_registered": {
|
|
577
608
|
const { requestId } = brokerMessage;
|
|
578
609
|
if (typeof requestId !== "string") throw new Error("Invalid live workflow-stage route registration");
|
|
@@ -606,6 +637,33 @@ export class IntercomClient extends EventEmitter {
|
|
|
606
637
|
else this.pendingSends.resolve(messageId, attemptId, result);
|
|
607
638
|
break;
|
|
608
639
|
}
|
|
640
|
+
case "send_held": {
|
|
641
|
+
const { messageId, attemptId, timeoutMs } = brokerMessage;
|
|
642
|
+
if (
|
|
643
|
+
typeof messageId !== "string" ||
|
|
644
|
+
(attemptId !== undefined && typeof attemptId !== "string") ||
|
|
645
|
+
typeof timeoutMs !== "number" ||
|
|
646
|
+
!Number.isFinite(timeoutMs) ||
|
|
647
|
+
timeoutMs <= 0
|
|
648
|
+
) {
|
|
649
|
+
throw new Error("Invalid send_held message");
|
|
650
|
+
}
|
|
651
|
+
// #3760: the recipient is a subagent being restored; the broker answers within `timeoutMs`.
|
|
652
|
+
this.pendingSends.extend(messageId, attemptId, timeoutMs + HELD_SEND_GRACE_MS);
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
case "restoring_recipient_ack": {
|
|
656
|
+
const { requestId, error } = brokerMessage;
|
|
657
|
+
if (typeof requestId !== "string" || (error !== undefined && typeof error !== "string")) {
|
|
658
|
+
throw new Error("Invalid restoring_recipient_ack message");
|
|
659
|
+
}
|
|
660
|
+
const pending = this.pendingRestoringRecipientRequests.get(requestId);
|
|
661
|
+
if (!pending) return;
|
|
662
|
+
this.pendingRestoringRecipientRequests.delete(requestId);
|
|
663
|
+
if (error === undefined) pending.resolve();
|
|
664
|
+
else pending.reject(new Error(error));
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
609
667
|
case "delivered": {
|
|
610
668
|
const { messageId, attemptId } = brokerMessage;
|
|
611
669
|
if (typeof messageId !== "string" || (attemptId !== undefined && typeof attemptId !== "string")) {
|
|
@@ -890,6 +948,66 @@ export class IntercomClient extends EventEmitter {
|
|
|
890
948
|
}
|
|
891
949
|
});
|
|
892
950
|
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* #3760: hold sends to the child that `capability` (issued to this session by `authorizeSupervisorChild`)
|
|
954
|
+
* names, and to its `aliases`, until the child registers with that capability. Calling again adds aliases.
|
|
955
|
+
* A `dormant` hold keeps the child addressable until the supervisor that restores it takes the hold over.
|
|
956
|
+
*/
|
|
957
|
+
reserveRestoringRecipient(
|
|
958
|
+
capability: string,
|
|
959
|
+
options: { group?: string; aliases?: readonly string[]; dormant?: boolean } = {},
|
|
960
|
+
): Promise<void> {
|
|
961
|
+
return this.restoringRecipientRequest({
|
|
962
|
+
type: "reserve_restoring_recipient",
|
|
963
|
+
capability,
|
|
964
|
+
...(options.group === undefined ? {} : { group: options.group }),
|
|
965
|
+
...(options.aliases === undefined ? {} : { aliases: [...options.aliases] }),
|
|
966
|
+
...(options.dormant === true ? { dormant: true } : {}),
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* #3760: end that hold. With `gone` (the reason), held and later senders are told the child is gone. With
|
|
972
|
+
* `refusal`, the senders held now are told that reason and later sends are routed normally. With neither,
|
|
973
|
+
* held sends are routed normally.
|
|
974
|
+
*/
|
|
975
|
+
releaseRestoringRecipient(capability: string, outcome?: string | { gone?: string; refusal?: string }): Promise<void> {
|
|
976
|
+
const { gone, refusal } = typeof outcome === "string" ? { gone: outcome, refusal: undefined } : (outcome ?? {});
|
|
977
|
+
return this.restoringRecipientRequest({
|
|
978
|
+
type: "release_restoring_recipient",
|
|
979
|
+
capability,
|
|
980
|
+
...(gone === undefined ? {} : { gone }),
|
|
981
|
+
...(refusal === undefined ? {} : { refusal }),
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
private restoringRecipientRequest(frame: Record<string, unknown>): Promise<void> {
|
|
986
|
+
let socket: net.Socket;
|
|
987
|
+
try {
|
|
988
|
+
socket = this.requireActiveSocket();
|
|
989
|
+
} catch (error) {
|
|
990
|
+
return Promise.reject(toError(error));
|
|
991
|
+
}
|
|
992
|
+
const requestId = randomUUID();
|
|
993
|
+
return new Promise((resolve, reject) => {
|
|
994
|
+
const timeout = setTimeout(() => {
|
|
995
|
+
if (!this.pendingRestoringRecipientRequests.delete(requestId)) return;
|
|
996
|
+
reject(new Error("Restoring-recipient request timeout"));
|
|
997
|
+
}, RESTORING_RECIPIENT_REQUEST_TIMEOUT_MS);
|
|
998
|
+
this.pendingRestoringRecipientRequests.set(requestId, {
|
|
999
|
+
resolve: () => { clearTimeout(timeout); resolve(); },
|
|
1000
|
+
reject: (error) => { clearTimeout(timeout); reject(error); },
|
|
1001
|
+
});
|
|
1002
|
+
try {
|
|
1003
|
+
writeMessage(socket, { ...frame, requestId });
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
clearTimeout(timeout);
|
|
1006
|
+
this.pendingRestoringRecipientRequests.delete(requestId);
|
|
1007
|
+
reject(toError(error));
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
893
1011
|
registerPendingStageRoute(
|
|
894
1012
|
runId: string,
|
|
895
1013
|
group: string,
|
|
@@ -897,6 +1015,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
897
1015
|
stages?: WorkflowStageRosterAnnouncement[],
|
|
898
1016
|
possibleStages?: WorkflowPossibleStageAnnouncement[],
|
|
899
1017
|
parent?: WorkflowRunParentAnnouncement,
|
|
1018
|
+
dormant?: boolean,
|
|
900
1019
|
): void {
|
|
901
1020
|
writeMessage(this.requireActiveSocket(), {
|
|
902
1021
|
type: "register_pending_stage_route",
|
|
@@ -906,6 +1025,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
906
1025
|
stages,
|
|
907
1026
|
possibleStages,
|
|
908
1027
|
...(parent === undefined ? {} : { parent }),
|
|
1028
|
+
...(dormant === true ? { dormant: true as const } : {}),
|
|
909
1029
|
});
|
|
910
1030
|
}
|
|
911
1031
|
|
|
@@ -969,6 +1089,15 @@ export class IntercomClient extends EventEmitter {
|
|
|
969
1089
|
return this.sendFrame("send", to, options);
|
|
970
1090
|
}
|
|
971
1091
|
|
|
1092
|
+
/**
|
|
1093
|
+
* #3761: tell the broker this message is saved, so it reports `delivered` to its sender. A no-op unless the
|
|
1094
|
+
* current registration's broker echoed the capability: that broker already reported `delivered` on the socket write.
|
|
1095
|
+
*/
|
|
1096
|
+
acknowledgeReceived(messageId: string): void {
|
|
1097
|
+
if (!this.brokerAcknowledgesReceipt) return;
|
|
1098
|
+
writeMessage(this.requireActiveSocket(), { type: "received", messageId });
|
|
1099
|
+
}
|
|
1100
|
+
|
|
972
1101
|
sendPendingStageNotification(
|
|
973
1102
|
runId: string,
|
|
974
1103
|
capability: string,
|
|
@@ -1015,7 +1144,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
1015
1144
|
requirePendingReply: options.requirePendingReply ?? false,
|
|
1016
1145
|
...(options.expectedRecipientId === undefined ? {} : { expectedRecipientId: options.expectedRecipientId }),
|
|
1017
1146
|
});
|
|
1018
|
-
acquired = this.pendingSends.acquire(messageId, pendingSignature,
|
|
1147
|
+
acquired = this.pendingSends.acquire(messageId, pendingSignature, SEND_RESPONSE_TIMEOUT_MS);
|
|
1019
1148
|
} catch (error) {
|
|
1020
1149
|
return Promise.reject(toError(error));
|
|
1021
1150
|
}
|
|
@@ -1040,6 +1169,8 @@ export class IntercomClient extends EventEmitter {
|
|
|
1040
1169
|
...(options.requirePendingReply === undefined ? {} : { requirePendingReply: options.requirePendingReply }),
|
|
1041
1170
|
...(options.expectedRecipientId === undefined ? {} : { expectedRecipientId: options.expectedRecipientId }),
|
|
1042
1171
|
...(options.onReplyTarget === undefined ? {} : { resolveReplyTarget: true as const }),
|
|
1172
|
+
// #3760: this client understands `send_held`, so the broker may hold the send for a subagent being restored.
|
|
1173
|
+
...(type === "send" ? { awaitRestoringRecipient: true as const } : {}),
|
|
1043
1174
|
message,
|
|
1044
1175
|
attemptId: acquired.attempt.attemptId,
|
|
1045
1176
|
...extra,
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { type SqliteDatabase, type SqliteRow, validateStoragePrefix } from "@bastani/atomic/storage";
|
|
2
|
+
import { OUTBOX_BACKUP_POLL_INTERVAL_MS } from "./multinode-state-store.js";
|
|
3
|
+
import type {
|
|
4
|
+
WakeUpBus,
|
|
5
|
+
WakeUpEvent,
|
|
6
|
+
WakeUpHint,
|
|
7
|
+
WakeUpListener,
|
|
8
|
+
WakeUpPublishResult,
|
|
9
|
+
WakeUpSubscribeResult,
|
|
10
|
+
WakeUpSubscription,
|
|
11
|
+
} from "./wake-up-bus.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* S4 wake-up bus (specs/2026-10-19-intercom-multi-node.md §5.6): a hint log in the shared storage database. Each
|
|
15
|
+
* connection polls the rows addressed to its subscribed brokers, delivers them and deletes them. A failed query drops
|
|
16
|
+
* the connection; the next poll or publish reopens it and every subscription gets a `reconnected` resync, because
|
|
17
|
+
* hints written meanwhile may have been pruned or missed. While the database stays unavailable, reopening backs off
|
|
18
|
+
* exponentially from the poll interval up to `WAKE_UP_RECONNECT_MAX_DELAY_MS`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** How often a connection with subscriptions reads its hints. */
|
|
22
|
+
export const WAKE_UP_POLL_INTERVAL_MS = 500;
|
|
23
|
+
/** Hints nobody consumed (their broker exited) are deleted after this age; the backup poll has covered them. */
|
|
24
|
+
export const WAKE_UP_HINT_RETENTION_MS = OUTBOX_BACKUP_POLL_INTERVAL_MS;
|
|
25
|
+
/**
|
|
26
|
+
* Hints read and deleted per statement; a poll repeats until a batch comes back short. Each batch's delete binds one
|
|
27
|
+
* parameter per hint, so a backlog never exceeds SQLite's bound-parameter limit (999 on old builds).
|
|
28
|
+
*/
|
|
29
|
+
export const WAKE_UP_POLL_BATCH_SIZE = 500;
|
|
30
|
+
/** Longest wait between attempts to reopen an unavailable database; the backup poll covers the gap. */
|
|
31
|
+
export const WAKE_UP_RECONNECT_MAX_DELAY_MS = OUTBOX_BACKUP_POLL_INTERVAL_MS;
|
|
32
|
+
|
|
33
|
+
/** Runs `callback` once after `delayMs` and returns a function that cancels it. */
|
|
34
|
+
export type WakeUpPollScheduler = (callback: () => void, delayMs: number) => () => void;
|
|
35
|
+
|
|
36
|
+
export interface DatabaseWakeUpBusOptions {
|
|
37
|
+
/** Opens a connection to the shared storage database; called again after every dropped connection. */
|
|
38
|
+
readonly openDatabase: () => Promise<SqliteDatabase>;
|
|
39
|
+
/** The validated storage prefix; the table is `<prefix>intercom_wakeups`. */
|
|
40
|
+
readonly prefix?: string;
|
|
41
|
+
readonly pollIntervalMs?: number;
|
|
42
|
+
readonly schedule?: WakeUpPollScheduler;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface HintRow extends SqliteRow {
|
|
46
|
+
readonly id: number;
|
|
47
|
+
readonly broker_id: string;
|
|
48
|
+
readonly reason: string;
|
|
49
|
+
readonly version: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface DatabaseSubscription {
|
|
53
|
+
readonly brokerId: string;
|
|
54
|
+
readonly listener: WakeUpListener;
|
|
55
|
+
open: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DATABASE_NOW_MS = "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
|
|
59
|
+
|
|
60
|
+
const scheduleTimeout: WakeUpPollScheduler = (callback, delayMs) => {
|
|
61
|
+
const timer = setTimeout(callback, delayMs);
|
|
62
|
+
timer.unref?.();
|
|
63
|
+
return () => clearTimeout(timer);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
async function createSchema(database: SqliteDatabase, table: string): Promise<void> {
|
|
67
|
+
await database.exec(
|
|
68
|
+
`CREATE TABLE IF NOT EXISTS ${table} (
|
|
69
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
70
|
+
broker_id TEXT NOT NULL,
|
|
71
|
+
reason TEXT NOT NULL CHECK (reason IN ('outbox', 'outcome')),
|
|
72
|
+
version INTEGER NOT NULL,
|
|
73
|
+
created_at INTEGER NOT NULL
|
|
74
|
+
);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS ${table}_broker ON ${table} (broker_id, id);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS ${table}_created ON ${table} (created_at);`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function openSchemaConnection(options: DatabaseWakeUpBusOptions, table: string): Promise<SqliteDatabase> {
|
|
81
|
+
const database = await options.openDatabase();
|
|
82
|
+
try {
|
|
83
|
+
await createSchema(database, table);
|
|
84
|
+
} catch (cause) {
|
|
85
|
+
await database.close().catch(() => {});
|
|
86
|
+
throw cause;
|
|
87
|
+
}
|
|
88
|
+
return database;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class DatabaseWakeUpBus implements WakeUpBus {
|
|
92
|
+
private readonly subscriptions = new Set<DatabaseSubscription>();
|
|
93
|
+
private readonly options: DatabaseWakeUpBusOptions;
|
|
94
|
+
private readonly table: string;
|
|
95
|
+
private readonly pollIntervalMs: number;
|
|
96
|
+
private readonly schedule: WakeUpPollScheduler;
|
|
97
|
+
private database: SqliteDatabase | undefined;
|
|
98
|
+
private opening: Promise<SqliteDatabase | undefined> | undefined;
|
|
99
|
+
private cancelPoll: (() => void) | undefined;
|
|
100
|
+
private polling: Promise<void> | undefined;
|
|
101
|
+
private closed = false;
|
|
102
|
+
private closing: Promise<void> | undefined;
|
|
103
|
+
/** Consecutive failed attempts to reopen the database; each doubles the wait before the next. */
|
|
104
|
+
private reconnectFailures = 0;
|
|
105
|
+
private reconnectNotBefore = 0;
|
|
106
|
+
|
|
107
|
+
constructor(options: DatabaseWakeUpBusOptions, table: string, database: SqliteDatabase) {
|
|
108
|
+
this.options = options;
|
|
109
|
+
this.table = table;
|
|
110
|
+
this.database = database;
|
|
111
|
+
this.pollIntervalMs = options.pollIntervalMs ?? WAKE_UP_POLL_INTERVAL_MS;
|
|
112
|
+
this.schedule = options.schedule ?? scheduleTimeout;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async publish(hint: WakeUpHint): Promise<WakeUpPublishResult> {
|
|
116
|
+
if (hint.brokerId.length === 0) return { status: "invalid", reason: "empty-broker-id" };
|
|
117
|
+
const database = await this.connection(false);
|
|
118
|
+
if (database === undefined) return { status: "unavailable" };
|
|
119
|
+
try {
|
|
120
|
+
await database.run(
|
|
121
|
+
`INSERT INTO ${this.table} (broker_id, reason, version, created_at) VALUES (?, ?, ?, ${DATABASE_NOW_MS})`,
|
|
122
|
+
hint.brokerId,
|
|
123
|
+
hint.reason,
|
|
124
|
+
hint.version,
|
|
125
|
+
);
|
|
126
|
+
await database.run(
|
|
127
|
+
`DELETE FROM ${this.table} WHERE created_at < ${DATABASE_NOW_MS} - ?`,
|
|
128
|
+
WAKE_UP_HINT_RETENTION_MS,
|
|
129
|
+
);
|
|
130
|
+
} catch {
|
|
131
|
+
this.drop(database);
|
|
132
|
+
return { status: "unavailable" };
|
|
133
|
+
}
|
|
134
|
+
return { status: "published" };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async subscribe(brokerId: string, listener: WakeUpListener): Promise<WakeUpSubscribeResult> {
|
|
138
|
+
if (brokerId.length === 0) return { status: "invalid", reason: "empty-broker-id" };
|
|
139
|
+
if (this.closed) return { status: "unavailable" };
|
|
140
|
+
const subscription: DatabaseSubscription = { brokerId, listener, open: true };
|
|
141
|
+
this.subscriptions.add(subscription);
|
|
142
|
+
this.deliver(subscription, { kind: "resync", cause: "subscribed" });
|
|
143
|
+
this.armPoll();
|
|
144
|
+
const handle: WakeUpSubscription = {
|
|
145
|
+
brokerId,
|
|
146
|
+
close: async () => {
|
|
147
|
+
subscription.open = false;
|
|
148
|
+
this.subscriptions.delete(subscription);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
return { status: "subscribed", subscription: handle };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
close(): Promise<void> {
|
|
155
|
+
this.closing ??= this.shutdown();
|
|
156
|
+
return this.closing;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async shutdown(): Promise<void> {
|
|
160
|
+
this.closed = true;
|
|
161
|
+
this.cancelPoll?.();
|
|
162
|
+
this.cancelPoll = undefined;
|
|
163
|
+
for (const subscription of this.subscriptions) subscription.open = false;
|
|
164
|
+
this.subscriptions.clear();
|
|
165
|
+
await this.polling;
|
|
166
|
+
await this.opening;
|
|
167
|
+
const database = this.database;
|
|
168
|
+
this.database = undefined;
|
|
169
|
+
await database?.close().catch(() => {});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private deliver(subscription: DatabaseSubscription, event: WakeUpEvent): void {
|
|
173
|
+
queueMicrotask(() => {
|
|
174
|
+
if (subscription.open) subscription.listener(event);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private armPoll(): void {
|
|
179
|
+
if (this.closed || this.cancelPoll !== undefined || this.polling !== undefined) return;
|
|
180
|
+
if (this.subscriptions.size === 0) return;
|
|
181
|
+
this.cancelPoll = this.schedule(() => {
|
|
182
|
+
this.cancelPoll = undefined;
|
|
183
|
+
this.polling = this.poll().finally(() => {
|
|
184
|
+
this.polling = undefined;
|
|
185
|
+
this.armPoll();
|
|
186
|
+
});
|
|
187
|
+
}, this.nextPollDelayMs());
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private nextPollDelayMs(): number {
|
|
191
|
+
if (this.database !== undefined || this.reconnectFailures === 0) return this.pollIntervalMs;
|
|
192
|
+
return Math.min(this.pollIntervalMs * 2 ** this.reconnectFailures, WAKE_UP_RECONNECT_MAX_DELAY_MS);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private async poll(): Promise<void> {
|
|
196
|
+
const database = await this.connection(true);
|
|
197
|
+
const brokerIds = [...new Set([...this.subscriptions].map((subscription) => subscription.brokerId))];
|
|
198
|
+
if (database === undefined || brokerIds.length === 0) return;
|
|
199
|
+
const placeholders = brokerIds.map(() => "?").join(", ");
|
|
200
|
+
for (;;) {
|
|
201
|
+
let rows: HintRow[];
|
|
202
|
+
try {
|
|
203
|
+
rows = await database.all<HintRow>(
|
|
204
|
+
`SELECT id, broker_id, reason, version FROM ${this.table} WHERE broker_id IN (${placeholders}) ORDER BY id LIMIT ${WAKE_UP_POLL_BATCH_SIZE}`,
|
|
205
|
+
...brokerIds,
|
|
206
|
+
);
|
|
207
|
+
if (rows.length > 0) {
|
|
208
|
+
await database.run(
|
|
209
|
+
`DELETE FROM ${this.table} WHERE id IN (${rows.map(() => "?").join(", ")})`,
|
|
210
|
+
...rows.map((row) => row.id),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
this.drop(database);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
for (const row of rows) {
|
|
218
|
+
const hint: WakeUpHint = {
|
|
219
|
+
brokerId: row.broker_id,
|
|
220
|
+
reason: row.reason === "outcome" ? "outcome" : "outbox",
|
|
221
|
+
version: row.version,
|
|
222
|
+
};
|
|
223
|
+
for (const subscription of this.subscriptions) {
|
|
224
|
+
if (subscription.brokerId === hint.brokerId) this.deliver(subscription, { kind: "hint", hint });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (rows.length < WAKE_UP_POLL_BATCH_SIZE || this.closed) return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** `force` (the poll) reopens even while backing off; a publish waits for the backoff to pass. */
|
|
232
|
+
private connection(force: boolean): Promise<SqliteDatabase | undefined> {
|
|
233
|
+
if (this.closed) return Promise.resolve(undefined);
|
|
234
|
+
if (this.database !== undefined) return Promise.resolve(this.database);
|
|
235
|
+
if (!force && this.opening === undefined && Date.now() < this.reconnectNotBefore) return Promise.resolve(undefined);
|
|
236
|
+
this.opening ??= this.reconnect().finally(() => {
|
|
237
|
+
this.opening = undefined;
|
|
238
|
+
});
|
|
239
|
+
return this.opening;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private async reconnect(): Promise<SqliteDatabase | undefined> {
|
|
243
|
+
let database: SqliteDatabase;
|
|
244
|
+
try {
|
|
245
|
+
database = await openSchemaConnection(this.options, this.table);
|
|
246
|
+
} catch {
|
|
247
|
+
this.reconnectFailures++;
|
|
248
|
+
this.reconnectNotBefore = Date.now() + this.nextPollDelayMs();
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
this.reconnectFailures = 0;
|
|
252
|
+
if (this.closed) {
|
|
253
|
+
await database.close().catch(() => {});
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
this.database = database;
|
|
257
|
+
for (const subscription of this.subscriptions) {
|
|
258
|
+
this.deliver(subscription, { kind: "resync", cause: "reconnected" });
|
|
259
|
+
}
|
|
260
|
+
return database;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private drop(database: SqliteDatabase): void {
|
|
264
|
+
if (this.database !== database) return;
|
|
265
|
+
this.database = undefined;
|
|
266
|
+
void database.close().catch(() => {});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Opens one broker's connection to the database wake-up bus; a failure to open the database is thrown. */
|
|
271
|
+
export async function openDatabaseWakeUpBus(options: DatabaseWakeUpBusOptions): Promise<WakeUpBus> {
|
|
272
|
+
const table = `${validateStoragePrefix(options.prefix)}intercom_wakeups`;
|
|
273
|
+
return new DatabaseWakeUpBus(options, table, await openSchemaConnection(options, table));
|
|
274
|
+
}
|
|
@@ -26,7 +26,7 @@ export function sessionsShareGroup(a: SessionInfo, b: SessionInfo): boolean {
|
|
|
26
26
|
|
|
27
27
|
/** Return each session visible through at least one of the requester's memberships. */
|
|
28
28
|
export function sessionsVisibleTo(
|
|
29
|
-
sessions: ReadonlyMap<string, BrokerConnectedSession
|
|
29
|
+
sessions: ReadonlyMap<string, Pick<BrokerConnectedSession, "info">>,
|
|
30
30
|
requester: SessionInfo,
|
|
31
31
|
): SessionInfo[] {
|
|
32
32
|
return [...sessions.values()].map(({ info }) => info).filter((info) => isAgentRecipient(info) && sessionsShareGroup(requester, info));
|
|
@@ -34,7 +34,7 @@ export function sessionsVisibleTo(
|
|
|
34
34
|
|
|
35
35
|
/** Return sessions that belong to one explicit group. */
|
|
36
36
|
export function sessionsInGroup(
|
|
37
|
-
sessions: ReadonlyMap<string, BrokerConnectedSession
|
|
37
|
+
sessions: ReadonlyMap<string, Pick<BrokerConnectedSession, "info">>,
|
|
38
38
|
group: string | undefined,
|
|
39
39
|
): SessionInfo[] {
|
|
40
40
|
return [...sessions.values()].map(({ info }) => info).filter((info) => isAgentRecipient(info) && hasGroup(sessionGroups(info), group));
|