@bastani/atomic 0.9.41 → 0.9.42-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 +49 -0
- package/dist/builtin/intercom/CHANGELOG.md +13 -0
- package/dist/builtin/intercom/README.md +7 -3
- package/dist/builtin/intercom/broker/broker-status.ts +48 -0
- package/dist/builtin/intercom/broker/broker.ts +90 -9
- package/dist/builtin/intercom/broker/client.ts +112 -12
- package/dist/builtin/intercom/broker/control.ts +225 -0
- package/dist/builtin/intercom/broker/multinode-router.ts +32 -39
- package/dist/builtin/intercom/broker/paths.ts +4 -0
- package/dist/builtin/intercom/broker/pending-send-registry.ts +27 -5
- package/dist/builtin/intercom/broker/shared-storage-setting.ts +60 -0
- package/dist/builtin/intercom/broker/spawn.ts +13 -4
- package/dist/builtin/intercom/broker/sql-lease-store.ts +23 -0
- package/dist/builtin/intercom/broker/watchdog.ts +105 -0
- package/dist/builtin/intercom/index.bundle.mjs +200 -22
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/types.ts +34 -2
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/extension/index.bundle.mjs +1 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +12 -0
- package/dist/builtin/workflows/README.md +10 -0
- package/dist/builtin/workflows/builtin/{chunk-txbc93t6.js → chunk-3s95f7ry.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-n2gkzkty.js → chunk-edhf30ef.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-8x61e0s7.js → chunk-frqxd4d1.js} +18 -9
- 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 +6 -1
- package/dist/builtin/workflows/src/authoring.d.ts +3 -1
- package/dist/builtin/workflows/src/durable/journal/internal.d.ts +13 -0
- package/dist/builtin/workflows/src/durable/journal/resolution.d.ts +2 -0
- package/dist/builtin/workflows/src/durable/journal/sql-run-journal.d.ts +1 -0
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +88554 -82820
- package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +72635 -0
- package/dist/builtin/workflows/src/index.js +53824 -5907
- package/dist/builtin/workflows/src/local-worker-entry.mjs +72635 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +7 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +2 -0
- package/dist/builtin/workflows/src/workers/public-options.d.ts +31 -0
- package/dist/bun/app-host-modules.d.ts +9 -0
- package/dist/bun/app-host-modules.d.ts.map +1 -0
- package/dist/bun/app-host-modules.js +10 -0
- package/dist/bun/app-host-modules.js.map +1 -0
- package/dist/bun/cli.d.ts +3 -1
- package/dist/bun/cli.d.ts.map +1 -1
- package/dist/bun/cli.js +15 -1
- package/dist/bun/cli.js.map +1 -1
- package/dist/bun/internal-intercom-broker.d.ts +17 -0
- package/dist/bun/internal-intercom-broker.d.ts.map +1 -1
- package/dist/bun/internal-intercom-broker.js +29 -0
- package/dist/bun/internal-intercom-broker.js.map +1 -1
- package/dist/bun/internal-workflow-worker.d.ts +4 -0
- package/dist/bun/internal-workflow-worker.d.ts.map +1 -0
- package/dist/bun/internal-workflow-worker.js +37 -0
- package/dist/bun/internal-workflow-worker.js.map +1 -0
- package/dist/bun/split-loader.js +10 -2
- package/dist/bun/split-loader.js.map +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +3 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/intercom-command.d.ts +15 -0
- package/dist/cli/intercom-command.d.ts.map +1 -0
- package/dist/cli/intercom-command.js +87 -0
- package/dist/cli/intercom-command.js.map +1 -0
- package/dist/cli/storage-command.d.ts +39 -0
- package/dist/cli/storage-command.d.ts.map +1 -0
- package/dist/cli/storage-command.js +367 -0
- package/dist/cli/storage-command.js.map +1 -0
- package/dist/core/agent-session-events.d.ts.map +1 -1
- package/dist/core/agent-session-events.js +5 -1
- package/dist/core/agent-session-events.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +1 -0
- 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-tasks.d.ts.map +1 -1
- package/dist/core/agent-session-tasks.js +1 -0
- package/dist/core/agent-session-tasks.js.map +1 -1
- package/dist/core/agent-session-types.d.ts +1 -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 +1 -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/builtin-install-layout.d.ts +6 -0
- package/dist/core/builtin-install-layout.d.ts.map +1 -1
- package/dist/core/builtin-install-layout.js +9 -0
- package/dist/core/builtin-install-layout.js.map +1 -1
- package/dist/core/child-session-options.d.ts.map +1 -1
- package/dist/core/child-session-options.js +3 -0
- package/dist/core/child-session-options.js.map +1 -1
- package/dist/core/exec.js +2 -2
- package/dist/core/exec.js.map +1 -1
- package/dist/core/export-html/template.css +9 -0
- package/dist/core/export-html/template.js +165 -45
- package/dist/core/extensions/loader-host-modules.d.ts +6 -0
- package/dist/core/extensions/loader-host-modules.d.ts.map +1 -1
- package/dist/core/extensions/loader-host-modules.js +12 -1
- package/dist/core/extensions/loader-host-modules.js.map +1 -1
- package/dist/core/extensions/loader-virtual-modules.d.ts +6 -0
- package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
- package/dist/core/extensions/loader-virtual-modules.js +18 -0
- package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
- package/dist/core/extensions/message-types.d.ts +4 -0
- package/dist/core/extensions/message-types.d.ts.map +1 -1
- package/dist/core/extensions/message-types.js.map +1 -1
- package/dist/core/fast-model-routing-transport.d.ts.map +1 -1
- package/dist/core/fast-model-routing-transport.js +5 -1
- package/dist/core/fast-model-routing-transport.js.map +1 -1
- package/dist/core/intercom-broker-control.d.ts +67 -0
- package/dist/core/intercom-broker-control.d.ts.map +1 -0
- package/dist/core/intercom-broker-control.js +17 -0
- package/dist/core/intercom-broker-control.js.map +1 -0
- package/dist/core/model-runtime-streaming.d.ts.map +1 -1
- package/dist/core/model-runtime-streaming.js +38 -1
- package/dist/core/model-runtime-streaming.js.map +1 -1
- package/dist/core/package-manager-git.d.ts +2 -2
- package/dist/core/package-manager-git.d.ts.map +1 -1
- package/dist/core/package-manager-git.js.map +1 -1
- package/dist/core/package-manager-manifest.d.ts.map +1 -1
- package/dist/core/package-manager-manifest.js +9 -1
- package/dist/core/package-manager-manifest.js.map +1 -1
- package/dist/core/package-manager-npm.d.ts +2 -2
- package/dist/core/package-manager-npm.d.ts.map +1 -1
- package/dist/core/package-manager-npm.js.map +1 -1
- package/dist/core/package-manager-paths.d.ts +7 -7
- package/dist/core/package-manager-paths.d.ts.map +1 -1
- package/dist/core/package-manager-paths.js.map +1 -1
- package/dist/core/package-manager-storage-adapters.d.ts +7 -0
- package/dist/core/package-manager-storage-adapters.d.ts.map +1 -0
- package/dist/core/package-manager-storage-adapters.js +76 -0
- package/dist/core/package-manager-storage-adapters.js.map +1 -0
- package/dist/core/package-manager-types.d.ts +4 -0
- package/dist/core/package-manager-types.d.ts.map +1 -1
- package/dist/core/package-manager-types.js.map +1 -1
- package/dist/core/provider-rate-limits.d.ts +23 -0
- package/dist/core/provider-rate-limits.d.ts.map +1 -0
- package/dist/core/provider-rate-limits.js +113 -0
- package/dist/core/provider-rate-limits.js.map +1 -0
- package/dist/core/provider-request-admission.d.ts +7 -0
- package/dist/core/provider-request-admission.d.ts.map +1 -0
- package/dist/core/provider-request-admission.js +91 -0
- package/dist/core/provider-request-admission.js.map +1 -0
- package/dist/core/resource-admission-scope.d.ts +4 -0
- package/dist/core/resource-admission-scope.d.ts.map +1 -0
- package/dist/core/resource-admission-scope.js +9 -0
- package/dist/core/resource-admission-scope.js.map +1 -0
- package/dist/core/sdk-types.d.ts +16 -0
- 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 +2 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +18 -2
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-lifecycle-work.d.ts +1 -0
- package/dist/core/session-lifecycle-work.d.ts.map +1 -1
- package/dist/core/session-lifecycle-work.js +1 -0
- package/dist/core/session-lifecycle-work.js.map +1 -1
- package/dist/core/settings-manager-basic-accessors.d.ts +3 -0
- package/dist/core/settings-manager-basic-accessors.d.ts.map +1 -1
- package/dist/core/settings-manager-basic-accessors.js +6 -0
- package/dist/core/settings-manager-basic-accessors.js.map +1 -1
- package/dist/core/settings-schema.d.ts +22 -0
- package/dist/core/settings-schema.d.ts.map +1 -1
- package/dist/core/settings-schema.js +18 -1
- package/dist/core/settings-schema.js.map +1 -1
- package/dist/core/storage/index.d.ts +1 -0
- package/dist/core/storage/index.d.ts.map +1 -1
- package/dist/core/storage/index.js +1 -0
- package/dist/core/storage/index.js.map +1 -1
- package/dist/core/storage/node-sqlite.d.ts +2 -1
- package/dist/core/storage/node-sqlite.d.ts.map +1 -1
- package/dist/core/storage/node-sqlite.js +5 -4
- package/dist/core/storage/node-sqlite.js.map +1 -1
- package/dist/core/storage/provider.d.ts +6 -0
- package/dist/core/storage/provider.d.ts.map +1 -1
- package/dist/core/storage/provider.js +27 -7
- package/dist/core/storage/provider.js.map +1 -1
- package/dist/core/storage/sqlite-facade.d.ts +49 -0
- package/dist/core/storage/sqlite-facade.d.ts.map +1 -0
- package/dist/core/storage/sqlite-facade.js +180 -0
- package/dist/core/storage/sqlite-facade.js.map +1 -0
- package/dist/core/storage/storage-status.d.ts +81 -0
- package/dist/core/storage/storage-status.d.ts.map +1 -0
- package/dist/core/storage/storage-status.js +409 -0
- package/dist/core/storage/storage-status.js.map +1 -0
- package/dist/core/tasks/supervisor.d.ts +2 -1
- package/dist/core/tasks/supervisor.d.ts.map +1 -1
- package/dist/core/tasks/supervisor.js +105 -4
- package/dist/core/tasks/supervisor.js.map +1 -1
- package/dist/core/tools/bash-pty-native.d.ts.map +1 -1
- package/dist/core/tools/bash-pty-native.js +1 -1
- package/dist/core/tools/bash-pty-native.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +28 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +7 -3
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/chat-message-renderer.d.ts +9 -1
- package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-message-renderer.js +16 -8
- package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +5 -0
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +69 -2
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/markdown-transform.d.ts +1 -1
- package/dist/modes/interactive/components/markdown-transform.d.ts.map +1 -1
- package/dist/modes/interactive/components/markdown-transform.js +2 -2
- package/dist/modes/interactive/components/markdown-transform.js.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.js +80 -22
- package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts +3 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +6 -2
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-deps.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode-deps.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-deps.js.map +1 -1
- package/dist/modes/interactive/interactive-render-chat.js +6 -2
- package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
- package/dist/modes/interactive-engine/engine-health.d.ts +8 -4
- package/dist/modes/interactive-engine/engine-health.d.ts.map +1 -1
- package/dist/modes/interactive-engine/engine-health.js +41 -6
- package/dist/modes/interactive-engine/engine-health.js.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.js +19 -17
- package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
- package/dist/modes/interactive-engine/remote-queue-pause.js +1 -1
- package/dist/modes/interactive-engine/remote-queue-pause.js.map +1 -1
- package/dist/modes/rpc/rpc-input-scheduler.d.ts +1 -0
- package/dist/modes/rpc/rpc-input-scheduler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-input-scheduler.js +4 -1
- package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
- package/dist/modes/rpc/rpc-input.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-input.js +29 -7
- package/dist/modes/rpc/rpc-input.js.map +1 -1
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +14 -0
- package/dist/utils/shell.js.map +1 -1
- package/docs/custom-provider/oauth.md +8 -1
- package/docs/extensions/api-reference.md +2 -0
- package/docs/intercom/operations.md +17 -8
- package/docs/intercom/reference.md +1 -1
- package/docs/keybindings.md +1 -1
- package/docs/packages/authoring.md +15 -0
- package/docs/packages.md +3 -2
- package/docs/providers.md +2 -0
- package/docs/reference/cli.md +17 -0
- package/docs/sdk.md +37 -0
- package/docs/settings.md +33 -2
- package/docs/storage.md +191 -219
- package/docs/usage.md +1 -1
- package/docs/workflows/api-reference.md +50 -0
- package/docs/workflows/operations.md +44 -1
- package/docs/workflows.md +25 -0
- package/examples/extensions/gondolin/package-lock.json +14 -24
- package/examples/extensions/gondolin/package.json +1 -1
- package/npm-shrinkwrap.json +35 -35
- package/package.json +4 -4
- package/schemas/settings.schema.json +87 -1
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
2
|
import net from "net";
|
|
3
3
|
import { randomUUID } from "crypto";
|
|
4
|
-
import { writeMessage, createMessageReader } from "./framing.js";
|
|
4
|
+
import { writeMessage, createMessageReader, type JsonWireValue } from "./framing.js";
|
|
5
|
+
import { isBrokerStatusReport } from "./broker-status.js";
|
|
5
6
|
import { getBrokerSocketPath } from "./paths.js";
|
|
6
7
|
import type {
|
|
7
8
|
SessionInfo,
|
|
8
9
|
Message,
|
|
9
10
|
Attachment,
|
|
11
|
+
BrokerStatusReport,
|
|
10
12
|
GroupSummary,
|
|
11
13
|
SupervisorRegistration,
|
|
12
14
|
SessionDirectory,
|
|
@@ -16,7 +18,7 @@ import type {
|
|
|
16
18
|
WorkflowPossibleStageAnnouncement,
|
|
17
19
|
WorkflowRunParentAnnouncement,
|
|
18
20
|
} from "../types.js";
|
|
19
|
-
import { buildSendSignature, PendingSendRegistry } from "./pending-send-registry.js";
|
|
21
|
+
import { buildSendSignature, PendingSendRegistry, PendingSendTimeoutError } from "./pending-send-registry.js";
|
|
20
22
|
import { readSubagentMessageSource } from "../source-ownership.js";
|
|
21
23
|
import { isMessage, isSessionInfo } from "./client-message-validation.js";
|
|
22
24
|
import { normalizeGroups } from "../group.js";
|
|
@@ -175,12 +177,14 @@ export class IntercomClient extends EventEmitter {
|
|
|
175
177
|
private brokerAcknowledgesReceipt = false;
|
|
176
178
|
/** The broker of the current registration answers a pending-stage route registration that carries a `requestId`. */
|
|
177
179
|
private brokerAcknowledgesPendingStageRoutes = false;
|
|
180
|
+
private brokerAnswersStatus = false;
|
|
178
181
|
private socket: net.Socket | null = null;
|
|
179
182
|
private _sessionId: string | null = null;
|
|
180
183
|
private _supervisorSessionId: string | null = null;
|
|
181
184
|
/** Source identity is captured once at connect, never read from env per message. */
|
|
182
185
|
private _messageSource: Message["source"] | undefined;
|
|
183
186
|
private pendingSends = new PendingSendRegistry();
|
|
187
|
+
private readonly sendResults = new WeakMap<Promise<SendResult>, Promise<SendResult>>();
|
|
184
188
|
private pendingReplyTargets = new Map<string, { messageId: string; bind(sessionId: string): void }>();
|
|
185
189
|
private pendingGroupLists = new Map<string, { resolve: (groups: GroupSummary[]) => void; reject: (error: Error) => void }>();
|
|
186
190
|
private pendingLists = new Map<string, { resolve: (directory: SessionDirectory) => void; reject: (e: Error) => void }>();
|
|
@@ -190,6 +194,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
190
194
|
groups?: string[];
|
|
191
195
|
}>();
|
|
192
196
|
private pendingMemberships = new Map<string, { resolve: (groups: string[]) => void; reject: (error: Error) => void }>();
|
|
197
|
+
private pendingBrokerStatus = new Map<string, { resolve: (report: BrokerStatusReport) => void; reject: (error: Error) => void }>();
|
|
193
198
|
private _groups = normalizeGroups();
|
|
194
199
|
private pendingLiveWorkflowStageRoutes = new Map<string, { resolve(): void; reject(error: Error): void }>();
|
|
195
200
|
private pendingStageRouteRegistrations = new Map<string, { resolve(): void; reject(error: Error): void }>();
|
|
@@ -201,6 +206,43 @@ export class IntercomClient extends EventEmitter {
|
|
|
201
206
|
private pendingRestoringRecipientRequests = new Map<string, { resolve(): void; reject(error: Error): void }>();
|
|
202
207
|
private disconnecting = false;
|
|
203
208
|
private disconnectError: Error | null = null;
|
|
209
|
+
private healthCheck: { socket: net.Socket; result: Promise<Error | undefined> } | undefined;
|
|
210
|
+
|
|
211
|
+
private async requestTimeout(socket: net.Socket, error: Error): Promise<Error> {
|
|
212
|
+
if (this.socket !== socket || socket.destroyed) return this.disconnectError ?? new IntercomClientDisconnectedError();
|
|
213
|
+
if (this.healthCheck?.socket !== socket) {
|
|
214
|
+
const requestId = randomUUID();
|
|
215
|
+
const result = new Promise<Error | undefined>((resolve) => {
|
|
216
|
+
const finish = (failure?: Error) => {
|
|
217
|
+
clearTimeout(timer);
|
|
218
|
+
this.pendingGroupLists.delete(requestId);
|
|
219
|
+
resolve(failure);
|
|
220
|
+
};
|
|
221
|
+
const timer = setTimeout(() => finish(new IntercomBrokerTimeoutError("Broker health check timed out")), GROUP_REQUEST_TIMEOUT_MS);
|
|
222
|
+
this.pendingGroupLists.set(requestId, { resolve: () => finish(), reject: finish });
|
|
223
|
+
try {
|
|
224
|
+
writeMessage(socket, { type: "list_groups", requestId });
|
|
225
|
+
} catch (cause) {
|
|
226
|
+
finish(new IntercomClientDisconnectedError({ cause }));
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
this.healthCheck = { socket, result };
|
|
230
|
+
}
|
|
231
|
+
const check = this.healthCheck;
|
|
232
|
+
const failure = await check.result;
|
|
233
|
+
if (this.healthCheck === check) this.healthCheck = undefined;
|
|
234
|
+
if (failure === undefined) return error;
|
|
235
|
+
const reported = failure instanceof IntercomBrokerTimeoutError
|
|
236
|
+
? new IntercomBrokerTimeoutError(error.message, { cause: failure })
|
|
237
|
+
: failure;
|
|
238
|
+
if (this.socket === socket && !this.disconnecting) {
|
|
239
|
+
this.disconnectError ??= reported;
|
|
240
|
+
this.failPending(this.disconnectError);
|
|
241
|
+
socket.destroy();
|
|
242
|
+
return this.disconnectError;
|
|
243
|
+
}
|
|
244
|
+
return reported;
|
|
245
|
+
}
|
|
204
246
|
|
|
205
247
|
constructor(returnAddress: string = randomUUID(), options: IntercomClientOptions = {}) {
|
|
206
248
|
super();
|
|
@@ -221,6 +263,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
221
263
|
for (const pending of this.pendingMemberships.values()) pending.reject(error);
|
|
222
264
|
for (const pending of this.pendingLiveWorkflowStageRoutes.values()) pending.reject(error);
|
|
223
265
|
for (const pending of this.pendingStageRouteRegistrations.values()) pending.reject(error);
|
|
266
|
+
for (const pending of this.pendingBrokerStatus.values()) pending.reject(error);
|
|
224
267
|
this.pendingLists.clear();
|
|
225
268
|
this.pendingGroupLists.clear();
|
|
226
269
|
this.pendingSupervisorAuthorizations.clear();
|
|
@@ -229,6 +272,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
229
272
|
this.pendingMemberships.clear();
|
|
230
273
|
this.pendingLiveWorkflowStageRoutes.clear();
|
|
231
274
|
this.pendingStageRouteRegistrations.clear();
|
|
275
|
+
this.pendingBrokerStatus.clear();
|
|
232
276
|
}
|
|
233
277
|
|
|
234
278
|
|
|
@@ -332,6 +376,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
332
376
|
this._supervisorSessionId = null;
|
|
333
377
|
this.brokerAcknowledgesReceipt = false;
|
|
334
378
|
this.brokerAcknowledgesPendingStageRoutes = false;
|
|
379
|
+
this.brokerAnswersStatus = false;
|
|
335
380
|
this._messageSource = undefined;
|
|
336
381
|
this.disconnectError = null;
|
|
337
382
|
if (connectionEstablished && !wasDisconnecting) {
|
|
@@ -440,7 +485,8 @@ export class IntercomClient extends EventEmitter {
|
|
|
440
485
|
if (typeof brokerMessage.sessionId !== "string"
|
|
441
486
|
|| (brokerMessage.supervisorSessionId !== undefined && typeof brokerMessage.supervisorSessionId !== "string")
|
|
442
487
|
|| (brokerMessage.acknowledgesReceipt !== undefined && brokerMessage.acknowledgesReceipt !== true)
|
|
443
|
-
|| (brokerMessage.acknowledgesPendingStageRoutes !== undefined && brokerMessage.acknowledgesPendingStageRoutes !== true)
|
|
488
|
+
|| (brokerMessage.acknowledgesPendingStageRoutes !== undefined && brokerMessage.acknowledgesPendingStageRoutes !== true)
|
|
489
|
+
|| (brokerMessage.brokerStatus !== undefined && brokerMessage.brokerStatus !== true)) {
|
|
444
490
|
throw new Error("Invalid registered message");
|
|
445
491
|
}
|
|
446
492
|
|
|
@@ -454,6 +500,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
454
500
|
// #3761: a broker from before receipts never echoes the capability and drops a client that sends `received`.
|
|
455
501
|
this.brokerAcknowledgesReceipt = this.acknowledgesReceipt && brokerMessage.acknowledgesReceipt === true;
|
|
456
502
|
this.brokerAcknowledgesPendingStageRoutes = brokerMessage.acknowledgesPendingStageRoutes === true;
|
|
503
|
+
this.brokerAnswersStatus = brokerMessage.brokerStatus === true;
|
|
457
504
|
this.emit("_registered", { type: "registered", sessionId: brokerMessage.sessionId });
|
|
458
505
|
break;
|
|
459
506
|
}
|
|
@@ -539,6 +586,16 @@ export class IntercomClient extends EventEmitter {
|
|
|
539
586
|
pending.resolve(groups as GroupSummary[]);
|
|
540
587
|
break;
|
|
541
588
|
}
|
|
589
|
+
case "broker_status": {
|
|
590
|
+
const { requestId } = brokerMessage;
|
|
591
|
+
const report = brokerMessage.report as JsonWireValue | undefined;
|
|
592
|
+
if (typeof requestId !== "string" || !isBrokerStatusReport(report)) throw new Error("Invalid broker_status message");
|
|
593
|
+
const pending = this.pendingBrokerStatus.get(requestId);
|
|
594
|
+
if (!pending) return;
|
|
595
|
+
this.pendingBrokerStatus.delete(requestId);
|
|
596
|
+
pending.resolve(report);
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
542
599
|
case "supervisor_authorized": {
|
|
543
600
|
const { requestId, capability, supervisorSessionId, childName } = brokerMessage;
|
|
544
601
|
if (typeof requestId !== "string" || typeof capability !== "string"
|
|
@@ -861,7 +918,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
861
918
|
const requestId = randomUUID();
|
|
862
919
|
const timeout = setTimeout(() => {
|
|
863
920
|
if (!this.pendingLists.delete(requestId)) return;
|
|
864
|
-
|
|
921
|
+
void this.requestTimeout(socket, new Error("List sessions timeout")).then(reject);
|
|
865
922
|
}, 5000);
|
|
866
923
|
this.pendingLists.set(requestId, {
|
|
867
924
|
resolve: (directory) => {
|
|
@@ -893,7 +950,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
893
950
|
return new Promise((resolve, reject) => {
|
|
894
951
|
const timeout = setTimeout(() => {
|
|
895
952
|
if (!this.pendingGroupLists.delete(requestId)) return;
|
|
896
|
-
|
|
953
|
+
void this.requestTimeout(socket, new Error("List groups timeout")).then(reject);
|
|
897
954
|
}, GROUP_REQUEST_TIMEOUT_MS);
|
|
898
955
|
this.pendingGroupLists.set(requestId, {
|
|
899
956
|
resolve: (groups) => { clearTimeout(timeout); resolve(groups); },
|
|
@@ -909,6 +966,35 @@ export class IntercomClient extends EventEmitter {
|
|
|
909
966
|
});
|
|
910
967
|
}
|
|
911
968
|
|
|
969
|
+
/** The broker's own status, including multi-node routing; `undefined` from a broker older than `broker_status`. */
|
|
970
|
+
brokerStatus(): Promise<BrokerStatusReport | undefined> {
|
|
971
|
+
let socket: net.Socket;
|
|
972
|
+
try {
|
|
973
|
+
socket = this.requireActiveSocket();
|
|
974
|
+
} catch (error) {
|
|
975
|
+
return Promise.reject(toError(error));
|
|
976
|
+
}
|
|
977
|
+
if (!this.brokerAnswersStatus) return Promise.resolve(undefined);
|
|
978
|
+
const requestId = randomUUID();
|
|
979
|
+
return new Promise((resolve, reject) => {
|
|
980
|
+
const timeout = setTimeout(() => {
|
|
981
|
+
if (!this.pendingBrokerStatus.delete(requestId)) return;
|
|
982
|
+
reject(new Error("Broker status timeout"));
|
|
983
|
+
}, GROUP_REQUEST_TIMEOUT_MS);
|
|
984
|
+
this.pendingBrokerStatus.set(requestId, {
|
|
985
|
+
resolve: (report) => { clearTimeout(timeout); resolve(report); },
|
|
986
|
+
reject: (error) => { clearTimeout(timeout); reject(error); },
|
|
987
|
+
});
|
|
988
|
+
try {
|
|
989
|
+
writeMessage(socket, { type: "broker_status", requestId });
|
|
990
|
+
} catch (error) {
|
|
991
|
+
clearTimeout(timeout);
|
|
992
|
+
this.pendingBrokerStatus.delete(requestId);
|
|
993
|
+
reject(toError(error));
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
|
|
912
998
|
joinGroup(group: string): Promise<string[]> {
|
|
913
999
|
return this.updateMembership("join_group", group);
|
|
914
1000
|
}
|
|
@@ -928,7 +1014,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
928
1014
|
return new Promise((resolve, reject) => {
|
|
929
1015
|
const timeout = setTimeout(() => {
|
|
930
1016
|
if (!this.pendingMemberships.delete(requestId)) return;
|
|
931
|
-
|
|
1017
|
+
void this.requestTimeout(socket, new Error("Membership update timeout")).then(reject);
|
|
932
1018
|
}, GROUP_REQUEST_TIMEOUT_MS);
|
|
933
1019
|
this.pendingMemberships.set(requestId, {
|
|
934
1020
|
resolve: (groups) => { clearTimeout(timeout); resolve(groups); },
|
|
@@ -958,7 +1044,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
958
1044
|
const requestId = randomUUID();
|
|
959
1045
|
const timeout = setTimeout(() => {
|
|
960
1046
|
if (!this.pendingSupervisorAuthorizations.delete(requestId)) return;
|
|
961
|
-
|
|
1047
|
+
void this.requestTimeout(socket, new Error("Supervisor authorization timeout")).then(reject);
|
|
962
1048
|
}, 5000);
|
|
963
1049
|
this.pendingSupervisorAuthorizations.set(requestId, {
|
|
964
1050
|
resolve: (authorization) => { clearTimeout(timeout); resolve(authorization); },
|
|
@@ -1020,7 +1106,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
1020
1106
|
return new Promise((resolve, reject) => {
|
|
1021
1107
|
const timeout = setTimeout(() => {
|
|
1022
1108
|
if (!this.pendingRestoringRecipientRequests.delete(requestId)) return;
|
|
1023
|
-
|
|
1109
|
+
void this.requestTimeout(socket, new Error("Restoring-recipient request timeout")).then(reject);
|
|
1024
1110
|
}, RESTORING_RECIPIENT_REQUEST_TIMEOUT_MS);
|
|
1025
1111
|
this.pendingRestoringRecipientRequests.set(requestId, {
|
|
1026
1112
|
resolve: () => { clearTimeout(timeout); resolve(); },
|
|
@@ -1088,7 +1174,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
1088
1174
|
const requestId = randomUUID();
|
|
1089
1175
|
const timeout = setTimeout(() => {
|
|
1090
1176
|
if (!this.pendingStageRouteRegistrations.delete(requestId)) return;
|
|
1091
|
-
|
|
1177
|
+
void this.requestTimeout(socket, new IntercomBrokerTimeoutError("Pending-stage route registration timeout")).then(reject);
|
|
1092
1178
|
}, PENDING_STAGE_ROUTE_ACK_TIMEOUT_MS);
|
|
1093
1179
|
this.pendingStageRouteRegistrations.set(requestId, {
|
|
1094
1180
|
resolve: () => {
|
|
@@ -1239,7 +1325,21 @@ export class IntercomClient extends EventEmitter {
|
|
|
1239
1325
|
} catch (error) {
|
|
1240
1326
|
return Promise.reject(toError(error));
|
|
1241
1327
|
}
|
|
1242
|
-
|
|
1328
|
+
let result = this.sendResults.get(acquired.attempt.promise);
|
|
1329
|
+
if (result === undefined) {
|
|
1330
|
+
result = acquired.attempt.promise.catch(async (error: Error) => {
|
|
1331
|
+
if (error instanceof PendingSendTimeoutError) {
|
|
1332
|
+
try {
|
|
1333
|
+
throw await this.requestTimeout(socket, error);
|
|
1334
|
+
} finally {
|
|
1335
|
+
this.pendingSends.release(acquired.attempt);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
throw error;
|
|
1339
|
+
});
|
|
1340
|
+
this.sendResults.set(acquired.attempt.promise, result);
|
|
1341
|
+
}
|
|
1342
|
+
if (!acquired.owner) return result;
|
|
1243
1343
|
const message: Message = {
|
|
1244
1344
|
id: messageId,
|
|
1245
1345
|
timestamp: Date.now(),
|
|
@@ -1271,7 +1371,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
1271
1371
|
}
|
|
1272
1372
|
const clearReplyTarget = () => { this.pendingReplyTargets.delete(acquired.attempt.attemptId); };
|
|
1273
1373
|
void acquired.attempt.promise.then(clearReplyTarget, clearReplyTarget);
|
|
1274
|
-
return
|
|
1374
|
+
return result;
|
|
1275
1375
|
}
|
|
1276
1376
|
updatePresence(updates: PresenceUpdates): boolean {
|
|
1277
1377
|
if (this.disconnecting) {
|
|
@@ -1306,7 +1406,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
1306
1406
|
};
|
|
1307
1407
|
const timeout = setTimeout(() => {
|
|
1308
1408
|
if (!this.pendingPresence.delete(requestId)) return;
|
|
1309
|
-
|
|
1409
|
+
void this.requestTimeout(socket, new Error("Presence update timeout")).then(reject);
|
|
1310
1410
|
}, PRESENCE_ACK_TIMEOUT_MS);
|
|
1311
1411
|
this.pendingPresence.set(requestId, {
|
|
1312
1412
|
resolve: wrappedResolve,
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import type { SqliteExecutor } from "@bastani/atomic/storage";
|
|
5
|
+
import type { BrokerStatusReport } from "../types.js";
|
|
6
|
+
import { describeMultiNodeStatus, isBrokerStatusReport } from "./broker-status.js";
|
|
7
|
+
import { createMessageReader, type JsonWireValue, writeMessage } from "./framing.js";
|
|
8
|
+
import { BROKER_LEASE_STALE_AFTER_MS } from "./multinode-state-store.js";
|
|
9
|
+
import { getBrokerPidPath, getBrokerSocketPath } from "./paths.js";
|
|
10
|
+
import {
|
|
11
|
+
describeSharedStorage,
|
|
12
|
+
readSharedStorageSetting,
|
|
13
|
+
sharedStorageDigest,
|
|
14
|
+
sharedStorageSettingSource,
|
|
15
|
+
} from "./shared-storage-setting.js";
|
|
16
|
+
import { readLiveBrokerNodes } from "./sql-lease-store.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Broker control for the Atomic CLI (`atomic intercom restart`, `atomic storage status`): it asks the agent
|
|
20
|
+
* directory's running broker about itself, or to shut down, over the broker's own socket, without registering a
|
|
21
|
+
* session. The CLI loads this module from the bundled Intercom package.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export { describeMultiNodeStatus };
|
|
25
|
+
|
|
26
|
+
/** How long a control request waits for its answer; a status report may wait up to 2 s for the shared database. */
|
|
27
|
+
export const BROKER_CONTROL_TIMEOUT_MS = 5_000;
|
|
28
|
+
/** How long `restartIntercomBroker` waits for the old broker to exit; its multi-node close is bounded at 2 s. */
|
|
29
|
+
export const BROKER_STOP_TIMEOUT_MS = 10_000;
|
|
30
|
+
/** How long `restartIntercomBroker` waits for a reconnecting session to start a new broker (first retry after 1 s). */
|
|
31
|
+
export const BROKER_RECONNECT_TIMEOUT_MS = 15_000;
|
|
32
|
+
const POLL_INTERVAL_MS = 100;
|
|
33
|
+
|
|
34
|
+
export type BrokerQuery =
|
|
35
|
+
| { readonly state: "stopped" }
|
|
36
|
+
| { readonly state: "running"; readonly report: BrokerStatusReport }
|
|
37
|
+
/** A broker older than `broker_status`: it answers on its socket but drops a connection that asks. */
|
|
38
|
+
| { readonly state: "legacy"; readonly pid?: number };
|
|
39
|
+
|
|
40
|
+
/** The shared storage a broker started now from this environment would read: what `broker_status` compares with. */
|
|
41
|
+
export interface ExpectedBrokerStorage {
|
|
42
|
+
readonly digest: string;
|
|
43
|
+
readonly source: "env" | "settings";
|
|
44
|
+
readonly adapter: string;
|
|
45
|
+
readonly prefix?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type RestartOutcome =
|
|
49
|
+
| { readonly status: "not-running" }
|
|
50
|
+
/** Stopped, and no broker came back: no session was connected, or none reconnected in time. */
|
|
51
|
+
| { readonly status: "stopped"; readonly previous: { readonly pid: number; readonly sessions?: number } }
|
|
52
|
+
| {
|
|
53
|
+
readonly status: "restarted";
|
|
54
|
+
readonly previous: { readonly pid: number; readonly sessions?: number };
|
|
55
|
+
readonly current: Exclude<BrokerQuery, { state: "stopped" }>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type ControlReply = { readonly connected: false } | { readonly connected: true; readonly reply?: JsonWireValue };
|
|
59
|
+
|
|
60
|
+
function sleep(ms: number): Promise<void> {
|
|
61
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Send one control frame and wait for the reply of `replyType`; a broker that closes without one yields no reply. */
|
|
65
|
+
function controlRequest(
|
|
66
|
+
socketPath: string,
|
|
67
|
+
type: "broker_status" | "shutdown",
|
|
68
|
+
replyType: "broker_status" | "shutdown_ack",
|
|
69
|
+
timeoutMs: number,
|
|
70
|
+
): Promise<ControlReply> {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
const requestId = randomUUID();
|
|
73
|
+
const socket = net.connect(socketPath);
|
|
74
|
+
let connected = false;
|
|
75
|
+
let settled = false;
|
|
76
|
+
const finish = (result: ControlReply): void => {
|
|
77
|
+
if (settled) return;
|
|
78
|
+
settled = true;
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
socket.destroy();
|
|
81
|
+
resolve(result);
|
|
82
|
+
};
|
|
83
|
+
const timer = setTimeout(() => finish(connected ? { connected } : { connected: false }), timeoutMs);
|
|
84
|
+
socket.on("connect", () => {
|
|
85
|
+
connected = true;
|
|
86
|
+
writeMessage(socket, { type, requestId });
|
|
87
|
+
});
|
|
88
|
+
socket.on(
|
|
89
|
+
"data",
|
|
90
|
+
createMessageReader(
|
|
91
|
+
(message) => {
|
|
92
|
+
if (
|
|
93
|
+
typeof message === "object" &&
|
|
94
|
+
message !== null &&
|
|
95
|
+
!Array.isArray(message) &&
|
|
96
|
+
message.type === replyType &&
|
|
97
|
+
message.requestId === requestId
|
|
98
|
+
)
|
|
99
|
+
finish({ connected: true, reply: message });
|
|
100
|
+
},
|
|
101
|
+
() => finish({ connected }),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
socket.on("error", () => finish(connected ? { connected } : { connected: false }));
|
|
105
|
+
socket.on("close", () => finish(connected ? { connected } : { connected: false }));
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readBrokerPid(agentDir: string): number | undefined {
|
|
110
|
+
try {
|
|
111
|
+
const pid = Number.parseInt(readFileSync(getBrokerPidPath(agentDir), "utf8").trim(), 10);
|
|
112
|
+
return Number.isFinite(pid) ? pid : undefined;
|
|
113
|
+
} catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isProcessAlive(pid: number): boolean {
|
|
119
|
+
try {
|
|
120
|
+
process.kill(pid, 0);
|
|
121
|
+
return true;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Ask the agent directory's broker for its status. */
|
|
128
|
+
export async function queryIntercomBroker(
|
|
129
|
+
agentDir: string,
|
|
130
|
+
timeoutMs: number = BROKER_CONTROL_TIMEOUT_MS,
|
|
131
|
+
): Promise<BrokerQuery> {
|
|
132
|
+
const answer = await controlRequest(
|
|
133
|
+
getBrokerSocketPath(process.platform, agentDir),
|
|
134
|
+
"broker_status",
|
|
135
|
+
"broker_status",
|
|
136
|
+
timeoutMs,
|
|
137
|
+
);
|
|
138
|
+
if (!answer.connected) return { state: "stopped" };
|
|
139
|
+
const reply = answer.reply;
|
|
140
|
+
if (typeof reply === "object" && reply !== null && !Array.isArray(reply) && isBrokerStatusReport(reply.report)) {
|
|
141
|
+
return { state: "running", report: reply.report };
|
|
142
|
+
}
|
|
143
|
+
const pid = readBrokerPid(agentDir);
|
|
144
|
+
// Windows can briefly accept a named-pipe connection after its broker has terminated.
|
|
145
|
+
if (pid !== undefined && !isProcessAlive(pid)) return { state: "stopped" };
|
|
146
|
+
return pid === undefined ? { state: "legacy" } : { state: "legacy", pid };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The shared selection a broker started from this environment and agent directory would read, if any. */
|
|
150
|
+
export function expectedBrokerStorage(
|
|
151
|
+
agentDir: string,
|
|
152
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
153
|
+
): ExpectedBrokerStorage | undefined {
|
|
154
|
+
const setting = readSharedStorageSetting(agentDir, env);
|
|
155
|
+
if (setting === undefined) return undefined;
|
|
156
|
+
return { digest: sharedStorageDigest(setting), source: sharedStorageSettingSource(env), ...describeSharedStorage(setting) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Host names of the brokers live in a shared database, without creating Intercom's tables. */
|
|
160
|
+
export function readLiveBrokers(database: SqliteExecutor, prefix: string | undefined): Promise<string[] | undefined> {
|
|
161
|
+
return readLiveBrokerNodes(database, prefix, BROKER_LEASE_STALE_AFTER_MS);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface RestartOptions {
|
|
165
|
+
readonly stopTimeoutMs?: number;
|
|
166
|
+
readonly reconnectTimeoutMs?: number;
|
|
167
|
+
/** Progress lines, for example `Stopping the Intercom broker (pid 123, 2 sessions)`. */
|
|
168
|
+
readonly onProgress?: (line: string) => void;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Restart the agent directory's broker so it reads the current storage selection: ask it to shut down (a broker
|
|
173
|
+
* older than this request gets SIGTERM, which it handles the same way), wait for it to exit, then wait for its
|
|
174
|
+
* sessions to reconnect, which starts a new broker. Without connected sessions the next session starts one.
|
|
175
|
+
*/
|
|
176
|
+
export async function restartIntercomBroker(agentDir: string, options: RestartOptions = {}): Promise<RestartOutcome> {
|
|
177
|
+
const stopTimeoutMs = options.stopTimeoutMs ?? BROKER_STOP_TIMEOUT_MS;
|
|
178
|
+
const reconnectTimeoutMs = options.reconnectTimeoutMs ?? BROKER_RECONNECT_TIMEOUT_MS;
|
|
179
|
+
const before = await queryIntercomBroker(agentDir);
|
|
180
|
+
if (before.state === "stopped") return { status: "not-running" };
|
|
181
|
+
const pid = before.state === "running" ? before.report.pid : before.pid;
|
|
182
|
+
if (pid === undefined) {
|
|
183
|
+
throw new Error(`The Intercom broker answers, but ${getBrokerPidPath(agentDir)} does not name its process.`);
|
|
184
|
+
}
|
|
185
|
+
const sessions = before.state === "running" ? before.report.sessions : undefined;
|
|
186
|
+
const previous = sessions === undefined ? { pid } : { pid, sessions };
|
|
187
|
+
options.onProgress?.(
|
|
188
|
+
`Stopping the Intercom broker (pid ${pid}${sessions === undefined ? "" : `, ${sessions} session${sessions === 1 ? "" : "s"} connected`}).`,
|
|
189
|
+
);
|
|
190
|
+
const answer =
|
|
191
|
+
before.state === "running"
|
|
192
|
+
? await controlRequest(
|
|
193
|
+
getBrokerSocketPath(process.platform, agentDir),
|
|
194
|
+
"shutdown",
|
|
195
|
+
"shutdown_ack",
|
|
196
|
+
BROKER_CONTROL_TIMEOUT_MS,
|
|
197
|
+
)
|
|
198
|
+
: undefined;
|
|
199
|
+
if (answer === undefined || !answer.connected || answer.reply === undefined) {
|
|
200
|
+
try {
|
|
201
|
+
process.kill(pid, "SIGTERM");
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const stopDeadline = Date.now() + stopTimeoutMs;
|
|
207
|
+
while (isProcessAlive(pid)) {
|
|
208
|
+
if (Date.now() >= stopDeadline) {
|
|
209
|
+
throw new Error(`The Intercom broker (pid ${pid}) did not exit within ${Math.round(stopTimeoutMs / 1000)} s.`);
|
|
210
|
+
}
|
|
211
|
+
await sleep(POLL_INTERVAL_MS);
|
|
212
|
+
}
|
|
213
|
+
if (sessions === 0) return { status: "stopped", previous };
|
|
214
|
+
options.onProgress?.("Stopped. Waiting for its sessions to reconnect and start a new broker.");
|
|
215
|
+
const reconnectDeadline = Date.now() + reconnectTimeoutMs;
|
|
216
|
+
let current: BrokerQuery = { state: "stopped" };
|
|
217
|
+
while (Date.now() < reconnectDeadline) {
|
|
218
|
+
current = await queryIntercomBroker(agentDir);
|
|
219
|
+
const settled =
|
|
220
|
+
current.state === "legacy" || (current.state === "running" && current.report.multiNode.status !== "starting");
|
|
221
|
+
if (settled) break;
|
|
222
|
+
await sleep(POLL_INTERVAL_MS * 2);
|
|
223
|
+
}
|
|
224
|
+
return current.state === "stopped" ? { status: "stopped", previous } : { status: "restarted", previous, current };
|
|
225
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
2
|
import { hostname } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
3
|
import { normalizeGroup } from "../group.js";
|
|
6
4
|
import { isAgentRecipient, NON_AGENT_RECIPIENT_REFUSAL, TERMINAL_CHILD_ASK_REFUSAL } from "../recipient-purpose.js";
|
|
7
5
|
import { DELIVERED_MESSAGE_TTL_MS, RECIPIENT_RECEIPT_TIMEOUT_MS } from "../retry-policy.js";
|
|
@@ -34,7 +32,6 @@ import {
|
|
|
34
32
|
type RestoringReservationLease,
|
|
35
33
|
reservationKeyFor,
|
|
36
34
|
} from "./multinode-state-store.js";
|
|
37
|
-
import { BROKER_STORAGE_ENV } from "./paths.js";
|
|
38
35
|
import type { PendingQuestionIndex } from "./pending-question-index.js";
|
|
39
36
|
import { UNCERTAIN_RECEIPT_REASON } from "./recipient-receipts.js";
|
|
40
37
|
import { RESTORING_RECIPIENT_HOLD_MS, restoringRecipientRetryReason } from "./restoring-recipients.js";
|
|
@@ -49,6 +46,8 @@ import type { BrokerOutboundFrame } from "./socket-writes.js";
|
|
|
49
46
|
import type { SupervisorChannelCache } from "./supervisor-channel.js";
|
|
50
47
|
import type { WakeUpBus, WakeUpEvent, WakeUpSubscription } from "./wake-up-bus.js";
|
|
51
48
|
|
|
49
|
+
export { readSharedStorageSetting } from "./shared-storage-setting.js";
|
|
50
|
+
|
|
52
51
|
/**
|
|
53
52
|
* Multi-node S5 (specs/2026-10-19-intercom-multi-node.md §4–§8, §14): routes sends to sessions hosted by other
|
|
54
53
|
* brokers through the shared state, and delivers the shared outbox to this broker's own sessions. Local delivery
|
|
@@ -91,39 +90,6 @@ const MAX_SUPERSEDED_HOPS = 8;
|
|
|
91
90
|
export const KNOWN_SESSION_REFRESH_INTERVAL_MS = 60 * 60 * 1000;
|
|
92
91
|
const TARGET_GROUP_REFUSAL = "Target session is in a different intercom group";
|
|
93
92
|
|
|
94
|
-
/**
|
|
95
|
-
* The raw shared `storage` selection, read without the host package so the broker stays self-contained: the
|
|
96
|
-
* selection the launching process handed this broker in `ATOMIC_STORAGE` (its `--storage`, `--storage-file` or own
|
|
97
|
-
* `ATOMIC_STORAGE`), otherwise the top-level `storage` value of the agent directory's settings. Relative paths in
|
|
98
|
-
* either resolve against the agent directory's parent, as for sessions and the journal. `undefined` leaves
|
|
99
|
-
* multi-node routing off: no selection, a per-process selection that is not a JSON object (the launching process
|
|
100
|
-
* validated it; settings are never consulted instead), and the selections the storage layer reads as in-memory
|
|
101
|
-
* (`memory: true` without a path, or `path: ":memory:"`), which no other broker can share. `path: ""` is not one of
|
|
102
|
-
* them: it selects the default storage file.
|
|
103
|
-
*/
|
|
104
|
-
export function readSharedStorageSetting(
|
|
105
|
-
agentDir: string,
|
|
106
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
107
|
-
): Record<string, unknown> | undefined {
|
|
108
|
-
const processSelection = env[BROKER_STORAGE_ENV];
|
|
109
|
-
let parsed: unknown;
|
|
110
|
-
try {
|
|
111
|
-
parsed =
|
|
112
|
-
processSelection === undefined || processSelection.length === 0
|
|
113
|
-
? (JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf8")) as Record<string, unknown>).storage
|
|
114
|
-
: JSON.parse(processSelection);
|
|
115
|
-
} catch {
|
|
116
|
-
return undefined;
|
|
117
|
-
}
|
|
118
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
|
|
119
|
-
const selection = parsed as Record<string, unknown>;
|
|
120
|
-
if (selection.adapter === undefined) {
|
|
121
|
-
if (selection.memory === true && !selection.path) return undefined;
|
|
122
|
-
if (selection.path === ":memory:") return undefined;
|
|
123
|
-
}
|
|
124
|
-
return selection;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
93
|
/** What the broker uses of `open-broker-state.ts`, which it loads lazily (replaceable in tests). */
|
|
128
94
|
export interface BrokerStateOpener<Source> {
|
|
129
95
|
resolveBrokerStateSource(
|
|
@@ -139,9 +105,9 @@ export type MultiNodeStartup<Source> =
|
|
|
139
105
|
|
|
140
106
|
/**
|
|
141
107
|
* Decide whether a top-level `storage` value gives this broker a shared database. Multi-node routing is off, and
|
|
142
|
-
* Intercom behaves exactly as a single-node broker, unless it does: also when the host storage module
|
|
143
|
-
*
|
|
144
|
-
*
|
|
108
|
+
* Intercom behaves exactly as a single-node broker, unless it does: also when the host storage module or the adapter
|
|
109
|
+
* cannot be loaded, or the selection cannot be resolved to a database. Only a selection that resolved to a database
|
|
110
|
+
* whose connection then fails makes cross-node sends fail explicitly.
|
|
145
111
|
*/
|
|
146
112
|
export async function resolveMultiNodeStartup<Source>(
|
|
147
113
|
setting: Record<string, unknown>,
|
|
@@ -490,6 +456,33 @@ export class MultiNodeRouter {
|
|
|
490
456
|
});
|
|
491
457
|
}
|
|
492
458
|
|
|
459
|
+
/**
|
|
460
|
+
* The host names of the other brokers whose membership lease is live, for status reports. `undefined` when the
|
|
461
|
+
* database fails or does not answer within `REMOTE_SESSIONS_BOUND_MS`.
|
|
462
|
+
*/
|
|
463
|
+
peerBrokers(): Promise<string[] | undefined> {
|
|
464
|
+
if (this.closed) return Promise.resolve(undefined);
|
|
465
|
+
return new Promise((resolve) => {
|
|
466
|
+
const bound = setTimeout(() => resolve(undefined), REMOTE_SESSIONS_BOUND_MS);
|
|
467
|
+
bound.unref?.();
|
|
468
|
+
void this.state.leases.listLive("broker", BROKER_LEASE_STALE_AFTER_MS).then(
|
|
469
|
+
(live) => {
|
|
470
|
+
clearTimeout(bound);
|
|
471
|
+
resolve(
|
|
472
|
+
live.flatMap((lease) =>
|
|
473
|
+
lease.subject.kind === "broker" && lease.holderId !== this.brokerId ? [lease.subject.nodeId] : [],
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
},
|
|
477
|
+
(error: Error) => {
|
|
478
|
+
clearTimeout(bound);
|
|
479
|
+
this.report(error);
|
|
480
|
+
resolve(undefined);
|
|
481
|
+
},
|
|
482
|
+
);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
493
486
|
// ---------------------------------------------------------------------------------------------------------
|
|
494
487
|
// Restoring reservations (§5.7): mirror this broker's reservations so senders on any host hold their sends.
|
|
495
488
|
|
|
@@ -75,6 +75,10 @@ export function getBrokerLogPath(agentDir: string = getAgentDir()): string {
|
|
|
75
75
|
return join(getIntercomDirPath(agentDir), "broker.log");
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
export function getBrokerWatchdogLogPath(agentDir: string = getAgentDir()): string {
|
|
79
|
+
return join(getIntercomDirPath(agentDir), "broker-watchdog.log");
|
|
80
|
+
}
|
|
81
|
+
|
|
78
82
|
export function getBrokerSocketPath(
|
|
79
83
|
platform: NodeJS.Platform = process.platform,
|
|
80
84
|
agentDir: string = getAgentDir(),
|