@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
|
@@ -8,8 +8,13 @@ import {
|
|
|
8
8
|
} from "../workflow-stage-target.js";
|
|
9
9
|
import { normalizeGroup } from "../group.js";
|
|
10
10
|
import { isMessage } from "./client-message-validation.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
resolveSessionTarget,
|
|
13
|
+
resolveSessionTargetByHostSessionId,
|
|
14
|
+
sessionTargetFailureReason,
|
|
15
|
+
} from "../session-target.js";
|
|
12
16
|
import { DeliveredMessageCache } from "./delivered-message-cache.js";
|
|
17
|
+
import { type ReceiptSender, type RecipientReceipts, UNCERTAIN_RECEIPT_REASON } from "./recipient-receipts.js";
|
|
13
18
|
import { buildMessageSendSignature } from "./send-signature.js";
|
|
14
19
|
import { SupervisorChannelCache } from "./supervisor-channel.js";
|
|
15
20
|
import { isVerticalBypass, sameGroup } from "./group-isolation.js";
|
|
@@ -34,6 +39,8 @@ export interface BrokerConnectedSession {
|
|
|
34
39
|
supervisorId?: string;
|
|
35
40
|
/** Private issuer identity used to restore child capabilities after reconnects. */
|
|
36
41
|
supervisorOwnerToken?: string;
|
|
42
|
+
/** #3761: the recipient confirms each message once it has saved it; `delivered` waits for that confirmation. */
|
|
43
|
+
readonly acknowledgesReceipt?: true;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
/** Immutable registration authority plus normalized memberships used when an accepted ask is rebound. */
|
|
@@ -80,12 +87,56 @@ export type LegacyWorkflowStageTargetResolver = (runId: string, stageKey: string
|
|
|
80
87
|
export const PENDING_STAGE_ASK_REFUSAL =
|
|
81
88
|
"Cannot ask a workflow stage whose session has not initialized. Use send; Atomic will queue the message until the stage session initializes.";
|
|
82
89
|
|
|
90
|
+
/**
|
|
91
|
+
* #3760: offered every send or ask that found no live recipient, before it fails with `Session not found`.
|
|
92
|
+
* Returns true when it answered or held the send (a subagent that its owner is restoring).
|
|
93
|
+
*/
|
|
94
|
+
export type RestoringRecipientHold = (send: {
|
|
95
|
+
readonly socket: net.Socket;
|
|
96
|
+
readonly from: BrokerConnectedSession;
|
|
97
|
+
readonly clientMessage: SendClientMessage & { readonly to: string };
|
|
98
|
+
readonly target: string;
|
|
99
|
+
readonly messageId: string;
|
|
100
|
+
readonly attemptId?: string;
|
|
101
|
+
}) => boolean;
|
|
83
102
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
interface SendClientMessage extends Record<string, unknown> {
|
|
103
|
+
export interface SendClientMessage extends Record<string, unknown> {
|
|
87
104
|
type: string;
|
|
88
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Multi-node S5: a validated send whose target no local session, local hold or workflow route resolved. The
|
|
109
|
+
* router answers it through the shared state (another broker's recipient, a shared hold or an offline session),
|
|
110
|
+
* or calls `notFound`, which answers exactly as a single-node broker would.
|
|
111
|
+
*/
|
|
112
|
+
export interface RemoteSendRequest {
|
|
113
|
+
readonly socket: net.Socket;
|
|
114
|
+
readonly from: BrokerConnectedSession;
|
|
115
|
+
readonly clientMessage: SendClientMessage & { readonly to: string };
|
|
116
|
+
readonly message: Message;
|
|
117
|
+
/** The trimmed `to`. */
|
|
118
|
+
readonly target: string;
|
|
119
|
+
/** The canonical send whose unkeyed SHA-256 is the cross-node digest. */
|
|
120
|
+
readonly signature: string;
|
|
121
|
+
readonly attemptId?: string;
|
|
122
|
+
readonly supervisorSend: boolean;
|
|
123
|
+
readonly requirePendingReply: boolean;
|
|
124
|
+
readonly expectedRecipientId?: string;
|
|
125
|
+
readonly resolveReplyTarget: boolean;
|
|
126
|
+
readonly awaitRestoringRecipient: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Set when the target resolved to a session on this broker: the router answers a same-id retry of a send that
|
|
129
|
+
* already went through the shared outbox from that row, and calls `notFound` to deliver anything else locally.
|
|
130
|
+
*/
|
|
131
|
+
readonly localTarget?: BrokerConnectedSession;
|
|
132
|
+
/** Without `localTarget`, nothing on any node: answer exactly as a single-node broker does. */
|
|
133
|
+
readonly notFound: () => void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Local sends the multi-node router has already checked against the shared outbox. */
|
|
137
|
+
const checkedAgainstSharedOutbox = new WeakSet<SendClientMessage>();
|
|
138
|
+
|
|
139
|
+
export type RemoteSendRouter = (request: RemoteSendRequest) => void;
|
|
89
140
|
function wireMessageId(value: unknown): string {
|
|
90
141
|
if (typeof value !== "object" || value === null) return "unknown";
|
|
91
142
|
const id = (value as Record<string, unknown>).id;
|
|
@@ -115,6 +166,9 @@ export function handleBrokerSend(
|
|
|
115
166
|
resolveLegacyWorkflowStageTarget?: LegacyWorkflowStageTargetResolver,
|
|
116
167
|
writeConfirmed?: ConfirmedMessageWriter,
|
|
117
168
|
isNonAgentWorkflowTarget?: (target: string) => boolean,
|
|
169
|
+
holdForRestoringRecipient?: RestoringRecipientHold,
|
|
170
|
+
receipts?: RecipientReceipts<BrokerConnectedSession>,
|
|
171
|
+
routeRemote?: RemoteSendRouter,
|
|
118
172
|
): void {
|
|
119
173
|
const message = clientMessage.message;
|
|
120
174
|
const messageId = wireMessageId(message);
|
|
@@ -286,9 +340,76 @@ export function handleBrokerSend(
|
|
|
286
340
|
return;
|
|
287
341
|
}
|
|
288
342
|
}
|
|
289
|
-
const
|
|
343
|
+
const nameOrIdResolution = exactIdTarget
|
|
290
344
|
? ({ kind: "resolved", session: exactIdTarget.info } as const)
|
|
291
345
|
: resolveSessionTarget(candidates, trimmedTo);
|
|
346
|
+
// #3760: a host session id (a resumed session's or a restored subagent's own session id) reaches the one
|
|
347
|
+
// visible session it registered, under the same group visibility as names.
|
|
348
|
+
const resolution = nameOrIdResolution.kind === "not_found"
|
|
349
|
+
? resolveSessionTargetByHostSessionId(
|
|
350
|
+
candidates,
|
|
351
|
+
trimmedTo,
|
|
352
|
+
(info) => sessions.get(info.id)?.registrationReturnAddress,
|
|
353
|
+
)
|
|
354
|
+
: nameOrIdResolution;
|
|
355
|
+
if (
|
|
356
|
+
routeRemote !== undefined &&
|
|
357
|
+
resolution.kind === "not_found" &&
|
|
358
|
+
workflowTarget === undefined &&
|
|
359
|
+
!visibleCandidates.some((info) => !isAgentRecipient(info) && info.name?.toLowerCase() === trimmedTo.toLowerCase())
|
|
360
|
+
) {
|
|
361
|
+
const send = clientMessage as SendClientMessage & { readonly to: string };
|
|
362
|
+
if (
|
|
363
|
+
!supervisorSend &&
|
|
364
|
+
!requirePendingReply &&
|
|
365
|
+
expectedRecipientId === undefined &&
|
|
366
|
+
holdForRestoringRecipient?.({
|
|
367
|
+
socket,
|
|
368
|
+
from: fromSession,
|
|
369
|
+
clientMessage: send,
|
|
370
|
+
target: trimmedTo,
|
|
371
|
+
messageId: message.id,
|
|
372
|
+
...(attemptId ? { attemptId } : {}),
|
|
373
|
+
})
|
|
374
|
+
) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
routeRemote({
|
|
378
|
+
socket,
|
|
379
|
+
from: fromSession,
|
|
380
|
+
clientMessage: send,
|
|
381
|
+
message,
|
|
382
|
+
target: trimmedTo,
|
|
383
|
+
signature,
|
|
384
|
+
...(attemptId === undefined ? {} : { attemptId }),
|
|
385
|
+
supervisorSend,
|
|
386
|
+
requirePendingReply,
|
|
387
|
+
...(expectedRecipientId === undefined ? {} : { expectedRecipientId }),
|
|
388
|
+
resolveReplyTarget: clientMessage.resolveReplyTarget === true,
|
|
389
|
+
awaitRestoringRecipient: clientMessage.awaitRestoringRecipient === true,
|
|
390
|
+
// Nothing on any node: answer exactly as a single-node broker does.
|
|
391
|
+
notFound: () =>
|
|
392
|
+
handleBrokerSend(
|
|
393
|
+
socket,
|
|
394
|
+
clientMessage,
|
|
395
|
+
currentId,
|
|
396
|
+
sessions,
|
|
397
|
+
deliveredMessages,
|
|
398
|
+
write,
|
|
399
|
+
supervisorCache,
|
|
400
|
+
pendingQuestions,
|
|
401
|
+
routePendingStage,
|
|
402
|
+
resolveLiveWorkflowStage,
|
|
403
|
+
canControlLiveWorkflowStage,
|
|
404
|
+
resolveLegacyWorkflowStageTarget,
|
|
405
|
+
writeConfirmed,
|
|
406
|
+
isNonAgentWorkflowTarget,
|
|
407
|
+
holdForRestoringRecipient,
|
|
408
|
+
receipts,
|
|
409
|
+
),
|
|
410
|
+
});
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
292
413
|
if (expectedRecipientId !== undefined && (resolution.kind !== "resolved" || resolution.session.id !== expectedRecipientId)) {
|
|
293
414
|
write(socket, { type: "delivery_failed", messageId, attemptId,
|
|
294
415
|
reason: resolution.kind === "resolved" ? "Reply target does not match the expected recipient" : sessionTargetFailureReason(clientMessage.to, resolution),
|
|
@@ -315,8 +436,52 @@ export function handleBrokerSend(
|
|
|
315
436
|
write(socket, { type: "delivery_failed", messageId: message.id, attemptId, reason: "Cannot message the current session" });
|
|
316
437
|
return;
|
|
317
438
|
}
|
|
439
|
+
if (routeRemote !== undefined && !checkedAgainstSharedOutbox.delete(clientMessage)) {
|
|
440
|
+
// Multi-node: a retry whose first attempt reached this recipient through another broker is answered from the
|
|
441
|
+
// shared outbox row; `DeliveredMessageCache` never saw that delivery.
|
|
442
|
+
routeRemote({
|
|
443
|
+
socket,
|
|
444
|
+
from: fromSession,
|
|
445
|
+
clientMessage: clientMessage as SendClientMessage & { readonly to: string },
|
|
446
|
+
message,
|
|
447
|
+
target: trimmedTo,
|
|
448
|
+
signature,
|
|
449
|
+
...(attemptId === undefined ? {} : { attemptId }),
|
|
450
|
+
supervisorSend,
|
|
451
|
+
requirePendingReply,
|
|
452
|
+
...(expectedRecipientId === undefined ? {} : { expectedRecipientId }),
|
|
453
|
+
resolveReplyTarget: clientMessage.resolveReplyTarget === true,
|
|
454
|
+
awaitRestoringRecipient: clientMessage.awaitRestoringRecipient === true,
|
|
455
|
+
localTarget: target,
|
|
456
|
+
notFound: () => {
|
|
457
|
+
checkedAgainstSharedOutbox.add(clientMessage);
|
|
458
|
+
handleBrokerSend(
|
|
459
|
+
socket,
|
|
460
|
+
clientMessage,
|
|
461
|
+
currentId,
|
|
462
|
+
sessions,
|
|
463
|
+
deliveredMessages,
|
|
464
|
+
write,
|
|
465
|
+
supervisorCache,
|
|
466
|
+
pendingQuestions,
|
|
467
|
+
routePendingStage,
|
|
468
|
+
resolveLiveWorkflowStage,
|
|
469
|
+
canControlLiveWorkflowStage,
|
|
470
|
+
resolveLegacyWorkflowStageTarget,
|
|
471
|
+
writeConfirmed,
|
|
472
|
+
isNonAgentWorkflowTarget,
|
|
473
|
+
holdForRestoringRecipient,
|
|
474
|
+
receipts,
|
|
475
|
+
routeRemote,
|
|
476
|
+
);
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
318
481
|
const targetIdentity = deliveryTargetIdentity(target);
|
|
319
482
|
const deliveredMatch = deliveredMessages.lookupForTarget(message.id, signature, targetIdentity);
|
|
483
|
+
// #3761: a recipient that confirms saves ignores a copy it already saved, so a reserved message is sent again.
|
|
484
|
+
const confirmsSaves = target.acknowledgesReceipt === true && receipts !== undefined;
|
|
320
485
|
if (requirePendingReply && deliveredMatch === "match") {
|
|
321
486
|
write(socket, { type: "delivered", messageId: message.id, attemptId });
|
|
322
487
|
return;
|
|
@@ -340,7 +505,7 @@ export function handleBrokerSend(
|
|
|
340
505
|
});
|
|
341
506
|
return;
|
|
342
507
|
}
|
|
343
|
-
if (requirePendingReply && deliveredMatch === "uncertain") {
|
|
508
|
+
if (requirePendingReply && deliveredMatch === "uncertain" && !confirmsSaves) {
|
|
344
509
|
write(socket, {
|
|
345
510
|
type: "delivery_failed",
|
|
346
511
|
messageId: message.id,
|
|
@@ -427,7 +592,7 @@ export function handleBrokerSend(
|
|
|
427
592
|
});
|
|
428
593
|
return;
|
|
429
594
|
}
|
|
430
|
-
if (deliveredMatch === "uncertain") {
|
|
595
|
+
if (deliveredMatch === "uncertain" && !confirmsSaves) {
|
|
431
596
|
write(socket, {
|
|
432
597
|
type: "delivery_failed",
|
|
433
598
|
messageId: message.id,
|
|
@@ -501,7 +666,7 @@ export function handleBrokerSend(
|
|
|
501
666
|
write(socket, { type: "delivered", messageId: message.id, attemptId });
|
|
502
667
|
return;
|
|
503
668
|
}
|
|
504
|
-
if (reservation !== "recorded") {
|
|
669
|
+
if (reservation !== "recorded" && !(reservation === "uncertain" && confirmsSaves)) {
|
|
505
670
|
write(socket, {
|
|
506
671
|
type: "delivery_failed",
|
|
507
672
|
messageId: message.id,
|
|
@@ -517,24 +682,28 @@ export function handleBrokerSend(
|
|
|
517
682
|
}
|
|
518
683
|
if (message.expectsReply === true && clientMessage.resolveReplyTarget === true && attemptId !== undefined)
|
|
519
684
|
write(socket, { type: "question_target", messageId: message.id, attemptId, sessionId: target.info.id });
|
|
520
|
-
const finishDelivery = (): void => {
|
|
685
|
+
const finishDelivery = (recipient: BrokerConnectedSession, sender: ReceiptSender | undefined): void => {
|
|
686
|
+
const notify = (frame: BrokerMessage): void => {
|
|
687
|
+
if (sender !== undefined) write(sender.socket, frame);
|
|
688
|
+
};
|
|
689
|
+
const senderAttemptId = sender?.attemptId;
|
|
521
690
|
let accepted: ReturnType<DeliveredMessageCache["accept"]>;
|
|
522
691
|
try {
|
|
523
692
|
accepted = deliveredMessages.accept(message.id, signature, targetIdentity);
|
|
524
693
|
} catch {
|
|
525
|
-
|
|
694
|
+
notify({
|
|
526
695
|
type: "delivery_failed",
|
|
527
696
|
messageId: message.id,
|
|
528
|
-
attemptId,
|
|
697
|
+
attemptId: senderAttemptId,
|
|
529
698
|
reason: "Intercom could not durably confirm the forwarded operation; refusing a possibly unsafe retry",
|
|
530
699
|
});
|
|
531
700
|
return;
|
|
532
701
|
}
|
|
533
702
|
if (accepted !== "accepted") {
|
|
534
|
-
|
|
703
|
+
notify({
|
|
535
704
|
type: "delivery_failed",
|
|
536
705
|
messageId: message.id,
|
|
537
|
-
attemptId,
|
|
706
|
+
attemptId: senderAttemptId,
|
|
538
707
|
reason: "Intercom could not durably confirm the forwarded operation; refusing a possibly unsafe retry",
|
|
539
708
|
...(accepted === "conflict" ? { reasonCode: "message_id_conflict" as const } : {}),
|
|
540
709
|
});
|
|
@@ -542,27 +711,58 @@ export function handleBrokerSend(
|
|
|
542
711
|
}
|
|
543
712
|
// A terminal presence frame can arrive while the socket write callback is pending.
|
|
544
713
|
// Retain accepted-delivery authority, but never open a reply route after termination.
|
|
545
|
-
if (message.expectsReply === true &&
|
|
546
|
-
|
|
714
|
+
if (message.expectsReply === true && recipient.info.replyCapability === "terminal") {
|
|
715
|
+
notify({ type: "delivery_failed", messageId: message.id, attemptId: senderAttemptId, reason: TERMINAL_CHILD_ASK_REFUSAL });
|
|
547
716
|
return;
|
|
548
717
|
}
|
|
549
|
-
if (message.expectsReply === true && sessions.get(
|
|
550
|
-
|
|
551
|
-
reason: `Session "${
|
|
718
|
+
if (message.expectsReply === true && sessions.get(recipient.info.id) !== recipient) {
|
|
719
|
+
notify({ type: "delivery_failed", messageId: message.id, attemptId: senderAttemptId,
|
|
720
|
+
reason: `Session "${recipient.info.name ?? recipient.info.id}" disconnected before replying` });
|
|
552
721
|
return;
|
|
553
722
|
}
|
|
554
|
-
|
|
555
|
-
|
|
723
|
+
// A sender whose wait already ended was told the outcome is uncertain; its retry opens the reply route.
|
|
724
|
+
if (message.expectsReply === true && sender !== undefined) {
|
|
725
|
+
pendingQuestions.record(fromSession.info.id, recipient.info.id, message.id);
|
|
726
|
+
// #3761: a recipient that reconnected before confirming answers under its new session id.
|
|
727
|
+
if (recipient.info.id !== target.info.id && clientMessage.resolveReplyTarget === true && senderAttemptId !== undefined)
|
|
728
|
+
notify({ type: "question_target", messageId: message.id, attemptId: senderAttemptId, sessionId: recipient.info.id });
|
|
556
729
|
}
|
|
557
|
-
if (message.replyTo !== undefined && pendingQuestions.clearReply(fromSession.info.id,
|
|
730
|
+
if (message.replyTo !== undefined && pendingQuestions.clearReply(fromSession.info.id, recipient.info.id, message.replyTo)) {
|
|
558
731
|
deliveredMessages.settleQuestion(message.replyTo);
|
|
559
732
|
}
|
|
560
|
-
if (supervisorSend) supervisorCache.record(message.id, fromSession.info.id,
|
|
561
|
-
|
|
733
|
+
if (supervisorSend) supervisorCache.record(message.id, fromSession.info.id, recipient.info.id);
|
|
734
|
+
notify({ type: "delivered", messageId: message.id, attemptId: senderAttemptId });
|
|
562
735
|
};
|
|
736
|
+
const senderWaiting: ReceiptSender = { socket, ...(attemptId === undefined ? {} : { attemptId }) };
|
|
737
|
+
if (confirmsSaves && receipts !== undefined) {
|
|
738
|
+
receipts.await(
|
|
739
|
+
{
|
|
740
|
+
messageId: message.id,
|
|
741
|
+
targetIdentity,
|
|
742
|
+
frame: outbound,
|
|
743
|
+
complete: finishDelivery,
|
|
744
|
+
expire: (waiting) => {
|
|
745
|
+
write(waiting.socket, { type: "delivery_failed", messageId: message.id, attemptId: waiting.attemptId, reason: UNCERTAIN_RECEIPT_REASON });
|
|
746
|
+
},
|
|
747
|
+
},
|
|
748
|
+
senderWaiting,
|
|
749
|
+
);
|
|
750
|
+
const unwritten = (): void => {
|
|
751
|
+
receipts.drop(message.id);
|
|
752
|
+
failDelivery();
|
|
753
|
+
};
|
|
754
|
+
if (writeConfirmed !== undefined) {
|
|
755
|
+
writeConfirmed(target.socket, outbound, (written) => {
|
|
756
|
+
if (!written) unwritten();
|
|
757
|
+
});
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (!write(target.socket, outbound)) unwritten();
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
563
763
|
if (writeConfirmed !== undefined) {
|
|
564
764
|
writeConfirmed(target.socket, outbound, (written) => {
|
|
565
|
-
if (written) finishDelivery();
|
|
765
|
+
if (written) finishDelivery(target, senderWaiting);
|
|
566
766
|
else failDelivery();
|
|
567
767
|
});
|
|
568
768
|
return;
|
|
@@ -573,13 +773,29 @@ export function handleBrokerSend(
|
|
|
573
773
|
failDelivery();
|
|
574
774
|
return;
|
|
575
775
|
}
|
|
576
|
-
finishDelivery();
|
|
776
|
+
finishDelivery(target, senderWaiting);
|
|
577
777
|
return;
|
|
578
778
|
}
|
|
579
779
|
if (resolution.kind === "not_found" && visibleCandidates.some((info) => !isAgentRecipient(info) && info.name?.toLowerCase() === trimmedTo.toLowerCase())) {
|
|
580
780
|
write(socket, { type: "delivery_failed", messageId: message.id, attemptId, reason: NON_AGENT_RECIPIENT_REFUSAL });
|
|
581
781
|
return;
|
|
582
782
|
}
|
|
783
|
+
if (
|
|
784
|
+
resolution.kind === "not_found" &&
|
|
785
|
+
!supervisorSend &&
|
|
786
|
+
!requirePendingReply &&
|
|
787
|
+
workflowTarget === undefined &&
|
|
788
|
+
holdForRestoringRecipient?.({
|
|
789
|
+
socket,
|
|
790
|
+
from: fromSession,
|
|
791
|
+
clientMessage: clientMessage as SendClientMessage & { readonly to: string },
|
|
792
|
+
target: trimmedTo,
|
|
793
|
+
messageId: message.id,
|
|
794
|
+
...(attemptId ? { attemptId } : {}),
|
|
795
|
+
})
|
|
796
|
+
) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
583
799
|
if (
|
|
584
800
|
resolution.kind === "not_found" &&
|
|
585
801
|
!supervisorSend &&
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Broker session ids are lowercase canonical UUIDs; only that form may be requested back. */
|
|
2
|
+
const CANONICAL_SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
3
|
+
const DEFAULT_MAX_ENTRIES = 10_000;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Lets a restarted or reconnected session register under the Intercom id peers already know (#3760).
|
|
7
|
+
*
|
|
8
|
+
* A requested id is granted only when no live session holds it, so a registration never displaces a
|
|
9
|
+
* live session. While this broker remembers who last held an id, only a registration with that same
|
|
10
|
+
* return address (the host's session identity) gets it back. A broker that restarted remembers
|
|
11
|
+
* nothing; its sessions and their routing state are gone too, so a free id carries no authority.
|
|
12
|
+
*/
|
|
13
|
+
export class SessionIdReclaim {
|
|
14
|
+
private readonly retired = new Map<string, string | undefined>();
|
|
15
|
+
|
|
16
|
+
constructor(private readonly maxEntries = DEFAULT_MAX_ENTRIES) {}
|
|
17
|
+
|
|
18
|
+
/** Remember who held a departed session id. */
|
|
19
|
+
retire(sessionId: string, returnAddress: string | undefined): void {
|
|
20
|
+
this.retired.delete(sessionId);
|
|
21
|
+
this.retired.set(sessionId, returnAddress);
|
|
22
|
+
while (this.retired.size > this.maxEntries) {
|
|
23
|
+
const oldest = this.retired.keys().next().value as string | undefined;
|
|
24
|
+
if (oldest === undefined) break;
|
|
25
|
+
this.retired.delete(oldest);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The requested id when it can be granted, otherwise undefined (the caller assigns a fresh id). */
|
|
30
|
+
grant(
|
|
31
|
+
preferredSessionId: string | undefined,
|
|
32
|
+
returnAddress: string | undefined,
|
|
33
|
+
isLive: (sessionId: string) => boolean,
|
|
34
|
+
): string | undefined {
|
|
35
|
+
if (preferredSessionId === undefined || returnAddress === undefined) return undefined;
|
|
36
|
+
if (!CANONICAL_SESSION_ID.test(preferredSessionId) || isLive(preferredSessionId)) return undefined;
|
|
37
|
+
if (this.retired.has(preferredSessionId) && this.retired.get(preferredSessionId) !== returnAddress) return undefined;
|
|
38
|
+
this.retired.delete(preferredSessionId);
|
|
39
|
+
return preferredSessionId;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -58,6 +58,15 @@ export type BrokerOutboundFrame =
|
|
|
58
58
|
notInKnownSet?: true;
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
/** A frame written while its socket's writes were held, with the outcome callback of a confirmed write. */
|
|
62
|
+
export interface HeldWrite {
|
|
63
|
+
readonly message: BrokerOutboundFrame;
|
|
64
|
+
readonly onSettled?: (written: boolean) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Multi-node S5: sockets whose frames are held until their deferred `registered` is written. */
|
|
68
|
+
const heldWrites = new WeakMap<net.Socket, HeldWrite[]>();
|
|
69
|
+
|
|
61
70
|
/**
|
|
62
71
|
* Write one broker frame unless the socket's writable side is already gone.
|
|
63
72
|
*
|
|
@@ -69,6 +78,11 @@ export type BrokerOutboundFrame =
|
|
|
69
78
|
*/
|
|
70
79
|
export function writeMessageIfOpen(socket: net.Socket, message: BrokerOutboundFrame): boolean {
|
|
71
80
|
if (!isSocketOpenForWrite(socket)) return false;
|
|
81
|
+
const held = heldWrites.get(socket);
|
|
82
|
+
if (held !== undefined) {
|
|
83
|
+
held.push({ message });
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
72
86
|
writeMessage(socket, message);
|
|
73
87
|
return true;
|
|
74
88
|
}
|
|
@@ -89,6 +103,11 @@ export function writeMessageWithOutcome(
|
|
|
89
103
|
onSettled(false);
|
|
90
104
|
return;
|
|
91
105
|
}
|
|
106
|
+
const held = heldWrites.get(socket);
|
|
107
|
+
if (held !== undefined) {
|
|
108
|
+
held.push({ message, onSettled });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
92
111
|
let settled = false;
|
|
93
112
|
const finish = (written: boolean): void => {
|
|
94
113
|
if (settled) return;
|
|
@@ -101,3 +120,31 @@ export function writeMessageWithOutcome(
|
|
|
101
120
|
finish(false);
|
|
102
121
|
}
|
|
103
122
|
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Multi-node S5: hold every frame written to `socket` until `releaseHeldWrites`. A session whose registration
|
|
126
|
+
* completes asynchronously is already routable, but its client accepts no frame before `registered`.
|
|
127
|
+
*/
|
|
128
|
+
export function holdWrites(socket: net.Socket): void {
|
|
129
|
+
if (!heldWrites.has(socket)) heldWrites.set(socket, []);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Stop holding frames for `socket`; returns the frames held, in the order they were written. */
|
|
133
|
+
export function releaseHeldWrites(socket: net.Socket): HeldWrite[] {
|
|
134
|
+
const held = heldWrites.get(socket) ?? [];
|
|
135
|
+
heldWrites.delete(socket);
|
|
136
|
+
return held;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Write released frames in order; a confirmed write to a closed socket reports `false`, as any write does. */
|
|
140
|
+
export function flushHeldWrites(socket: net.Socket, held: readonly HeldWrite[]): void {
|
|
141
|
+
for (const { message, onSettled } of held) {
|
|
142
|
+
if (onSettled === undefined) writeMessageIfOpen(socket, message);
|
|
143
|
+
else writeMessageWithOutcome(socket, message, onSettled);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Drop released frames that will never be written; each confirmed write reports `false`. */
|
|
148
|
+
export function discardHeldWrites(held: readonly HeldWrite[]): void {
|
|
149
|
+
for (const { onSettled } of held) onSettled?.(false);
|
|
150
|
+
}
|
|
@@ -4,8 +4,9 @@ import { join, dirname } from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { createRequire } from "module";
|
|
6
6
|
import { isSocketAnswering } from "./socket-liveness.js";
|
|
7
|
-
import { createChildProcessEnvironment, isBunBinary } from "@bastani/atomic";
|
|
7
|
+
import { createChildProcessEnvironment, isBunBinary, processSharedStorageSelection } from "@bastani/atomic";
|
|
8
8
|
import {
|
|
9
|
+
BROKER_STORAGE_ENV,
|
|
9
10
|
getBrokerLogPath,
|
|
10
11
|
getBrokerPidPath,
|
|
11
12
|
getBrokerSocketPath,
|
|
@@ -243,6 +244,9 @@ export function getBrokerLaunchSpec(
|
|
|
243
244
|
* The broker is detached and outlives this process, so its stderr goes to an already-open file
|
|
244
245
|
* descriptor rather than a pipe: a long-lived pipe would keep the parent's event loop attached
|
|
245
246
|
* and break once the parent exits.
|
|
247
|
+
*
|
|
248
|
+
* The broker routes through the shared storage this process was launched with (`--storage`,
|
|
249
|
+
* `--storage-file` or `ATOMIC_STORAGE`), passed as `ATOMIC_STORAGE`; without one it reads settings.
|
|
246
250
|
*/
|
|
247
251
|
export function getBrokerSpawnOptions(
|
|
248
252
|
extensionDir: string = EXTENSION_DIR,
|
|
@@ -254,11 +258,15 @@ export function getBrokerSpawnOptions(
|
|
|
254
258
|
env: NodeJS.ProcessEnv;
|
|
255
259
|
windowsHide: true;
|
|
256
260
|
} {
|
|
261
|
+
const storage = processSharedStorageSelection();
|
|
257
262
|
return {
|
|
258
263
|
detached: true,
|
|
259
264
|
stdio: ["ignore", "ignore", stderr],
|
|
260
265
|
cwd: extensionDir,
|
|
261
|
-
env: createChildProcessEnvironment({
|
|
266
|
+
env: createChildProcessEnvironment({
|
|
267
|
+
NODE_NO_WARNINGS: "1",
|
|
268
|
+
...(storage === undefined ? {} : { [BROKER_STORAGE_ENV]: storage }),
|
|
269
|
+
}),
|
|
262
270
|
windowsHide: true,
|
|
263
271
|
};
|
|
264
272
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type SqliteRow, validateStoragePrefix } from "@bastani/atomic/storage";
|
|
2
|
+
import type {
|
|
3
|
+
HeldSend,
|
|
4
|
+
HeldSendDropResult,
|
|
5
|
+
HeldSendHoldResult,
|
|
6
|
+
HeldSendListResult,
|
|
7
|
+
HeldSendRequest,
|
|
8
|
+
HeldSendStore,
|
|
9
|
+
HeldSendTakeResult,
|
|
10
|
+
IntercomBrokerStateOptions,
|
|
11
|
+
InvalidStateInput,
|
|
12
|
+
StateStoreInvalidReason,
|
|
13
|
+
} from "./multinode-state-store.js";
|
|
14
|
+
|
|
15
|
+
type HeldRow = SqliteRow & {
|
|
16
|
+
reservation_key: string;
|
|
17
|
+
origin_broker_id: string;
|
|
18
|
+
sender_session_id: string;
|
|
19
|
+
request_json: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const STORE_NOW = "CAST(unixepoch('subsec') * 1000 AS INTEGER)";
|
|
23
|
+
|
|
24
|
+
const invalid = (reason: StateStoreInvalidReason): InvalidStateInput => ({ status: "invalid", reason });
|
|
25
|
+
|
|
26
|
+
function heldSendOf(row: HeldRow): HeldSend {
|
|
27
|
+
return {
|
|
28
|
+
reservationKey: row.reservation_key,
|
|
29
|
+
originBrokerId: row.origin_broker_id,
|
|
30
|
+
senderSessionId: row.sender_session_id,
|
|
31
|
+
request: JSON.parse(row.request_json) as HeldSendRequest,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* S3: `HeldSendStore` on the shared storage adapter's database, table `<prefix>intercom_held_sends`. Sends are keyed
|
|
37
|
+
* by `reservationKeyFor(capability)`; the capability itself is never stored.
|
|
38
|
+
*/
|
|
39
|
+
export async function openSqlHeldSendStore({ database, prefix }: IntercomBrokerStateOptions): Promise<HeldSendStore> {
|
|
40
|
+
const table = `${validateStoragePrefix(prefix)}intercom_held_sends`;
|
|
41
|
+
await database.transaction((tx) =>
|
|
42
|
+
tx.exec(`
|
|
43
|
+
CREATE TABLE IF NOT EXISTS ${table}(
|
|
44
|
+
"order" INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
45
|
+
message_id TEXT NOT NULL UNIQUE,
|
|
46
|
+
reservation_key TEXT NOT NULL,
|
|
47
|
+
origin_broker_id TEXT NOT NULL,
|
|
48
|
+
sender_session_id TEXT NOT NULL,
|
|
49
|
+
request_json TEXT NOT NULL,
|
|
50
|
+
held_at INTEGER NOT NULL
|
|
51
|
+
);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS ${table}_reservation ON ${table}(reservation_key, "order");
|
|
53
|
+
`),
|
|
54
|
+
);
|
|
55
|
+
const selectForReservation = `SELECT reservation_key, origin_broker_id, sender_session_id, request_json FROM ${table}
|
|
56
|
+
WHERE reservation_key = ? ORDER BY "order"`;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
async hold(send): Promise<HeldSendHoldResult> {
|
|
60
|
+
if (send.reservationKey.length === 0) return invalid("empty-reservation-key");
|
|
61
|
+
if (send.originBrokerId.length === 0) return invalid("empty-broker-id");
|
|
62
|
+
if (send.senderSessionId.length === 0) return invalid("empty-session-id");
|
|
63
|
+
const messageId = send.request.message.id;
|
|
64
|
+
if (messageId.length === 0) return invalid("empty-message-id");
|
|
65
|
+
const { changes } = await database.run(
|
|
66
|
+
`INSERT INTO ${table}(message_id, reservation_key, origin_broker_id, sender_session_id, request_json, held_at)
|
|
67
|
+
VALUES (?, ?, ?, ?, ?, ${STORE_NOW}) ON CONFLICT(message_id) DO NOTHING`,
|
|
68
|
+
messageId,
|
|
69
|
+
send.reservationKey,
|
|
70
|
+
send.originBrokerId,
|
|
71
|
+
send.senderSessionId,
|
|
72
|
+
JSON.stringify(send.request),
|
|
73
|
+
);
|
|
74
|
+
return changes === 0 ? { status: "duplicate" } : { status: "held" };
|
|
75
|
+
},
|
|
76
|
+
async list(reservationKey): Promise<HeldSendListResult> {
|
|
77
|
+
if (reservationKey.length === 0) return invalid("empty-reservation-key");
|
|
78
|
+
const rows = await database.all<HeldRow>(selectForReservation, reservationKey);
|
|
79
|
+
return { status: "listed", sends: rows.map(heldSendOf) };
|
|
80
|
+
},
|
|
81
|
+
async take(reservationKey): Promise<HeldSendTakeResult> {
|
|
82
|
+
if (reservationKey.length === 0) return invalid("empty-reservation-key");
|
|
83
|
+
const rows = await database.transaction(async (tx) => {
|
|
84
|
+
const taken = await tx.all<HeldRow>(selectForReservation, reservationKey);
|
|
85
|
+
await tx.run(`DELETE FROM ${table} WHERE reservation_key = ?`, reservationKey);
|
|
86
|
+
return taken;
|
|
87
|
+
});
|
|
88
|
+
return { status: "taken", sends: rows.map(heldSendOf) };
|
|
89
|
+
},
|
|
90
|
+
async drop(messageId): Promise<HeldSendDropResult> {
|
|
91
|
+
if (messageId.length === 0) return invalid("empty-message-id");
|
|
92
|
+
const { changes } = await database.run(`DELETE FROM ${table} WHERE message_id = ?`, messageId);
|
|
93
|
+
return changes === 0 ? { status: "missing" } : { status: "dropped" };
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|