@bastani/atomic 0.9.39 → 0.9.40-alpha.2
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 +35 -0
- package/dist/builtin/intercom/CHANGELOG.md +14 -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 +31 -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/CHANGELOG.md +6 -0
- package/dist/builtin/web-access/README.md +1 -1
- package/dist/builtin/web-access/index.bundle.mjs +2 -5
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +24 -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 +22715 -22159
- 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 +17 -1
- 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/export-html/index.d.ts.map +1 -1
- package/dist/core/export-html/index.js +3 -2
- package/dist/core/export-html/index.js.map +1 -1
- package/dist/core/export-html/template.html +1 -0
- package/dist/core/export-html/template.js +37 -20
- package/dist/core/export-html/vendor/marked-cjk-friendly.min.js +6 -0
- package/dist/core/export-html/vendor/marked.min.js +76 -3
- 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 +5 -3
- 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 +6 -4
- 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 +40 -40
- package/package.json +6 -6
- package/dist/core/export-html/template-js/data-tree.js +0 -287
- package/dist/core/export-html/template-js/entries-navigation.js +0 -434
- package/dist/core/export-html/template-js/initialization.js +0 -335
- package/dist/core/export-html/template-js/message-tools.js +0 -428
- package/dist/core/export-html/template-js/tree-filter-render.js +0 -419
- package/dist/core/export-html/template-script.d.ts +0 -3
- package/dist/core/export-html/template-script.d.ts.map +0 -1
- package/dist/core/export-html/template-script.js +0 -13
- package/dist/core/export-html/template-script.js.map +0 -1
|
@@ -3,9 +3,18 @@
|
|
|
3
3
|
import "./bounded-stderr-install.js";
|
|
4
4
|
import net from "net";
|
|
5
5
|
import { chmodSync, writeFileSync, unlinkSync, mkdirSync, readFileSync, statSync } from "fs";
|
|
6
|
+
import { dirname } from "path";
|
|
6
7
|
import { randomUUID } from "crypto";
|
|
7
8
|
import { createMessageReader, type JsonWireValue } from "./framing.js";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
discardHeldWrites,
|
|
11
|
+
flushHeldWrites,
|
|
12
|
+
type HeldWrite,
|
|
13
|
+
holdWrites,
|
|
14
|
+
releaseHeldWrites,
|
|
15
|
+
writeMessageIfOpen,
|
|
16
|
+
writeMessageWithOutcome,
|
|
17
|
+
} from "./socket-writes.js";
|
|
9
18
|
import { isSocketAnswering } from "./socket-liveness.js";
|
|
10
19
|
import { acquireSocketReplacementLock } from "./socket-replacement-lock.js";
|
|
11
20
|
import {
|
|
@@ -39,9 +48,24 @@ import {
|
|
|
39
48
|
handleBrokerSend,
|
|
40
49
|
type BrokerConnectedSession,
|
|
41
50
|
type PendingStageRoute,
|
|
51
|
+
type RemoteSendRequest,
|
|
52
|
+
type RemoteSendRouter,
|
|
53
|
+
type RestoringRecipientHold,
|
|
54
|
+
type SendClientMessage,
|
|
42
55
|
senderGroupIdentity,
|
|
43
56
|
} from "./send-handler.js";
|
|
57
|
+
import {
|
|
58
|
+
type BrokerStateOpener,
|
|
59
|
+
LOCAL_RETRY_LOOKUP_BOUND_MS,
|
|
60
|
+
MultiNodeRouter,
|
|
61
|
+
readSharedStorageSetting,
|
|
62
|
+
resolveMultiNodeStartup,
|
|
63
|
+
SHARED_STORAGE_UNAVAILABLE_REASON,
|
|
64
|
+
} from "./multinode-router.js";
|
|
65
|
+
import { OUTBOX_BACKUP_POLL_INTERVAL_MS } from "./multinode-state-store.js";
|
|
44
66
|
import { SupervisorChannelCache } from "./supervisor-channel.js";
|
|
67
|
+
import { SessionIdReclaim } from "./session-id-reclaim.js";
|
|
68
|
+
import { type HeldSend, RestoringRecipients } from "./restoring-recipients.js";
|
|
45
69
|
import { hasGroup, normalizeGroup, normalizeGroups } from "../group.js";
|
|
46
70
|
import { handleBrokerPresence } from "./presence-handler.js";
|
|
47
71
|
import {
|
|
@@ -52,6 +76,7 @@ import {
|
|
|
52
76
|
setSessionGroups,
|
|
53
77
|
} from "./group-membership.js";
|
|
54
78
|
import { PendingQuestionIndex } from "./pending-question-index.js";
|
|
79
|
+
import { type ReceiptSender, RecipientReceipts, UNCERTAIN_RECEIPT_REASON } from "./recipient-receipts.js";
|
|
55
80
|
import { matchStagePathSegments } from "../workflow-stage-path-matching.js";
|
|
56
81
|
import { DELIVERED_MESSAGE_TTL_MS } from "../retry-policy.js";
|
|
57
82
|
import { isAgentRecipient } from "../recipient-purpose.js";
|
|
@@ -69,6 +94,7 @@ const PENDING_STAGE_MESSAGE_TIMEOUT_MS = 10_000;
|
|
|
69
94
|
const LISTEN_ATTEMPTS = 5;
|
|
70
95
|
const LISTEN_RETRY_DELAY_MS = 100;
|
|
71
96
|
const SOCKET_OWNERSHIP_CHECK_MS = 1_000;
|
|
97
|
+
const MULTI_NODE_CLOSE_BOUND_MS = 2_000;
|
|
72
98
|
|
|
73
99
|
function exitBecauseLiveBrokerOwnsSocket(): never {
|
|
74
100
|
console.error(`Intercom broker not started: a live broker already answers on ${SOCKET_PATH}`);
|
|
@@ -79,6 +105,8 @@ interface PendingStageRouteRegistration {
|
|
|
79
105
|
readonly sessionId: string;
|
|
80
106
|
readonly group: string;
|
|
81
107
|
readonly capability: string;
|
|
108
|
+
/** Registered by a restarted owner for an interrupted run; yields to the process that resumes it (#3760). */
|
|
109
|
+
readonly dormant?: true;
|
|
82
110
|
}
|
|
83
111
|
|
|
84
112
|
interface WorkflowRosterRegistration {
|
|
@@ -173,6 +201,22 @@ function isSupervisorRegistration(value: unknown): value is SupervisorRegistrati
|
|
|
173
201
|
&& typeof registration.supervisorSessionId === "string";
|
|
174
202
|
}
|
|
175
203
|
|
|
204
|
+
const RESTORING_RECIPIENT_ALIASES_MAX = 4;
|
|
205
|
+
const RESTORING_RECIPIENT_GONE_REASON_MAX = 1_000;
|
|
206
|
+
const SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
207
|
+
|
|
208
|
+
function isRestoringRecipientAliasList(value: unknown): value is string[] {
|
|
209
|
+
return Array.isArray(value) && value.length <= RESTORING_RECIPIENT_ALIASES_MAX && value.every((alias) => typeof alias === "string");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A restoring subagent's extra targets are ids only: its Atomic session id and its pre-restart Intercom id (#3760).
|
|
214
|
+
* Its name comes from the supervisor capability, so no reservation can claim another child's name.
|
|
215
|
+
*/
|
|
216
|
+
function isRestoringRecipientAlias(alias: string): boolean {
|
|
217
|
+
return SESSION_ID_PATTERN.test(alias.trim());
|
|
218
|
+
}
|
|
219
|
+
|
|
176
220
|
function invocationOwnsGroup(invocationGroup: string, candidateGroup: string): boolean {
|
|
177
221
|
const owner = normalizeGroup(invocationGroup);
|
|
178
222
|
const candidate = normalizeGroup(candidateGroup);
|
|
@@ -269,13 +313,24 @@ class IntercomBroker {
|
|
|
269
313
|
getBrokerDeliveredMessagesPath(),
|
|
270
314
|
);
|
|
271
315
|
private supervisorChannel = new SupervisorChannelCache();
|
|
316
|
+
private sessionIdReclaim = new SessionIdReclaim();
|
|
272
317
|
private pendingQuestions = new PendingQuestionIndex();
|
|
318
|
+
private receipts = new RecipientReceipts<ConnectedSession>();
|
|
319
|
+
private restoringRecipients = new RestoringRecipients((held, reason) => this.failHeldSend(held, reason));
|
|
273
320
|
private pendingStageRoutes = new Map<string, PendingStageRouteRegistration>();
|
|
274
321
|
private pendingStageAcknowledgments = new Map<string, PendingStageAcknowledgment>();
|
|
275
322
|
private pendingStageNotificationAcknowledgments = new Map<string, PendingStageNotificationAcknowledgment>();
|
|
276
323
|
private liveWorkflowStageRoutes = new Map<string, LiveWorkflowStageRouteRegistration>();
|
|
277
324
|
private workflowRosters = new Map<string, WorkflowRosterRegistration>();
|
|
278
325
|
private liveWorkflowStageRouteActivations = new Map<string, LiveWorkflowStageRouteActivation>();
|
|
326
|
+
/** Multi-node S5: set only when the agent directory's settings select a top-level shared `storage`. */
|
|
327
|
+
private readonly sharedStorageSetting = readSharedStorageSetting(dirname(INTERCOM_DIR));
|
|
328
|
+
private readonly routeRemote: RemoteSendRouter | undefined =
|
|
329
|
+
this.sharedStorageSetting === undefined ? undefined : (request) => this.routeRemoteSend(request);
|
|
330
|
+
private multiNode: MultiNodeRouter | undefined;
|
|
331
|
+
private multiNodeStatus: "starting" | "on" | "off" | "unavailable" = "off";
|
|
332
|
+
private multiNodeFirstAttempt: Promise<void> = Promise.resolve();
|
|
333
|
+
private shuttingDown = false;
|
|
279
334
|
|
|
280
335
|
constructor() {
|
|
281
336
|
mkdirSync(INTERCOM_DIR, { recursive: true, mode: 0o700 });
|
|
@@ -285,6 +340,7 @@ class IntercomBroker {
|
|
|
285
340
|
|
|
286
341
|
start(): void {
|
|
287
342
|
this.listen(1);
|
|
343
|
+
if (this.sharedStorageSetting !== undefined) this.startMultiNode(this.sharedStorageSetting);
|
|
288
344
|
process.on("SIGTERM", () => this.shutdown());
|
|
289
345
|
process.on("SIGINT", () => this.shutdown());
|
|
290
346
|
}
|
|
@@ -957,7 +1013,9 @@ class IntercomBroker {
|
|
|
957
1013
|
pending.signature,
|
|
958
1014
|
targetIdentity,
|
|
959
1015
|
);
|
|
960
|
-
|
|
1016
|
+
// #3761: a recipient that confirms saves ignores a copy it already saved, so a reserved message is sent again.
|
|
1017
|
+
const confirmsSaves = target.acknowledgesReceipt === true;
|
|
1018
|
+
if (deliveredMatch !== "miss" && !(deliveredMatch === "uncertain" && confirmsSaves)) {
|
|
961
1019
|
if (deliveredMatch === "match") {
|
|
962
1020
|
if (pending.message.expectsReply === true) {
|
|
963
1021
|
const acceptedQuestionTarget = this.deliveredMessages.lookupQuestionTarget(
|
|
@@ -1041,7 +1099,7 @@ class IntercomBroker {
|
|
|
1041
1099
|
});
|
|
1042
1100
|
return;
|
|
1043
1101
|
}
|
|
1044
|
-
if (reservation !== "recorded") {
|
|
1102
|
+
if (reservation !== "recorded" && !(reservation === "uncertain" && confirmsSaves)) {
|
|
1045
1103
|
writeMessageIfOpen(pending.senderSocket, {
|
|
1046
1104
|
type: "delivery_failed",
|
|
1047
1105
|
messageId: pending.messageId,
|
|
@@ -1058,49 +1116,81 @@ class IntercomBroker {
|
|
|
1058
1116
|
}
|
|
1059
1117
|
if (pending.message.expectsReply === true && pending.resolveReplyTarget === true && pending.attemptId !== undefined)
|
|
1060
1118
|
writeMessageIfOpen(pending.senderSocket, { type: "question_target", messageId: pending.messageId, attemptId: pending.attemptId, sessionId: target.info.id });
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1119
|
+
const frame: BrokerMessage = { type: "message", from: pending.sender, message: pending.message };
|
|
1120
|
+
const signature = pending.signature;
|
|
1121
|
+
const finish = (recipient: ConnectedSession, sender: ReceiptSender | undefined): void => {
|
|
1122
|
+
const notify = (message: BrokerMessage): void => {
|
|
1123
|
+
if (sender !== undefined) writeMessageIfOpen(sender.socket, message);
|
|
1124
|
+
};
|
|
1125
|
+
const senderAttempt = sender?.attemptId === undefined ? {} : { attemptId: sender.attemptId };
|
|
1126
|
+
let accepted: ReturnType<DeliveredMessageCache["accept"]>;
|
|
1127
|
+
try {
|
|
1128
|
+
accepted = this.deliveredMessages.accept(pending.messageId, signature, targetIdentity);
|
|
1129
|
+
} catch {
|
|
1130
|
+
notify({
|
|
1131
|
+
type: "delivery_failed",
|
|
1132
|
+
messageId: pending.messageId,
|
|
1133
|
+
...senderAttempt,
|
|
1134
|
+
reason: "Intercom could not durably confirm the forwarded operation; refusing a possibly unsafe retry",
|
|
1135
|
+
});
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (accepted !== "accepted") {
|
|
1139
|
+
notify({
|
|
1140
|
+
type: "delivery_failed",
|
|
1141
|
+
messageId: pending.messageId,
|
|
1142
|
+
...senderAttempt,
|
|
1143
|
+
reason: "Intercom could not durably confirm the forwarded operation; refusing a possibly unsafe retry",
|
|
1144
|
+
...(accepted === "conflict" ? { reasonCode: "message_id_conflict" as const } : {}),
|
|
1145
|
+
});
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
if (pending.message.expectsReply === true && sender !== undefined) {
|
|
1149
|
+
this.pendingQuestions.record(from.info.id, recipient.info.id, pending.messageId);
|
|
1150
|
+
// #3761: a recipient that reconnected before confirming answers under its new session id.
|
|
1151
|
+
if (recipient.info.id !== target.info.id && pending.resolveReplyTarget === true && sender.attemptId !== undefined)
|
|
1152
|
+
notify({ type: "question_target", messageId: pending.messageId, attemptId: sender.attemptId, sessionId: recipient.info.id });
|
|
1153
|
+
}
|
|
1154
|
+
if (pending.message.replyTo !== undefined && this.pendingQuestions.clearReply(from.info.id, recipient.info.id, pending.message.replyTo)) {
|
|
1155
|
+
this.deliveredMessages.settleQuestion(pending.message.replyTo);
|
|
1156
|
+
}
|
|
1157
|
+
notify({ type: "delivered", messageId: pending.messageId, ...senderAttempt });
|
|
1158
|
+
};
|
|
1159
|
+
const sender: ReceiptSender = {
|
|
1160
|
+
socket: pending.senderSocket,
|
|
1161
|
+
...(pending.attemptId ? { attemptId: pending.attemptId } : {}),
|
|
1162
|
+
};
|
|
1163
|
+
if (confirmsSaves) {
|
|
1164
|
+
this.receipts.await(
|
|
1165
|
+
{
|
|
1166
|
+
messageId: pending.messageId,
|
|
1167
|
+
targetIdentity,
|
|
1168
|
+
frame,
|
|
1169
|
+
complete: finish,
|
|
1170
|
+
expire: (waiting) => {
|
|
1171
|
+
writeMessageIfOpen(waiting.socket, {
|
|
1172
|
+
type: "delivery_failed",
|
|
1173
|
+
messageId: pending.messageId,
|
|
1174
|
+
...(waiting.attemptId === undefined ? {} : { attemptId: waiting.attemptId }),
|
|
1175
|
+
reason: UNCERTAIN_RECEIPT_REASON,
|
|
1176
|
+
});
|
|
1177
|
+
},
|
|
1178
|
+
},
|
|
1179
|
+
sender,
|
|
1180
|
+
);
|
|
1082
1181
|
}
|
|
1083
|
-
if (
|
|
1182
|
+
if (!writeMessageIfOpen(target.socket, frame)) {
|
|
1183
|
+
if (confirmsSaves) this.receipts.drop(pending.messageId);
|
|
1184
|
+
if (reservation === "recorded") this.deliveredMessages.forget(pending.messageId, pending.signature);
|
|
1084
1185
|
writeMessageIfOpen(pending.senderSocket, {
|
|
1085
1186
|
type: "delivery_failed",
|
|
1086
1187
|
messageId: pending.messageId,
|
|
1087
1188
|
...(pending.attemptId ? { attemptId: pending.attemptId } : {}),
|
|
1088
|
-
reason: "
|
|
1089
|
-
...(accepted === "conflict" ? { reasonCode: "message_id_conflict" as const } : {}),
|
|
1189
|
+
reason: "Session not found",
|
|
1090
1190
|
});
|
|
1091
1191
|
return;
|
|
1092
1192
|
}
|
|
1093
|
-
if (
|
|
1094
|
-
this.pendingQuestions.record(from.info.id, target.info.id, pending.messageId);
|
|
1095
|
-
}
|
|
1096
|
-
if (pending.message.replyTo !== undefined && this.pendingQuestions.clearReply(from.info.id, target.info.id, pending.message.replyTo)) {
|
|
1097
|
-
this.deliveredMessages.settleQuestion(pending.message.replyTo);
|
|
1098
|
-
}
|
|
1099
|
-
writeMessageIfOpen(pending.senderSocket, {
|
|
1100
|
-
type: "delivered",
|
|
1101
|
-
messageId: pending.messageId,
|
|
1102
|
-
...(pending.attemptId ? { attemptId: pending.attemptId } : {}),
|
|
1103
|
-
});
|
|
1193
|
+
if (!confirmsSaves) finish(target, sender);
|
|
1104
1194
|
}
|
|
1105
1195
|
|
|
1106
1196
|
|
|
@@ -1405,6 +1495,12 @@ class IntercomBroker {
|
|
|
1405
1495
|
&& (typeof clientMessage.supervisorOwnerToken !== "string" || !clientMessage.supervisorOwnerToken)) {
|
|
1406
1496
|
throw new Error("Invalid supervisor owner token");
|
|
1407
1497
|
}
|
|
1498
|
+
if (clientMessage.preferredSessionId !== undefined && typeof clientMessage.preferredSessionId !== "string") {
|
|
1499
|
+
throw new Error("Invalid preferred session id");
|
|
1500
|
+
}
|
|
1501
|
+
if (clientMessage.acknowledgesReceipt !== undefined && clientMessage.acknowledgesReceipt !== true) {
|
|
1502
|
+
throw new Error("Invalid receipt acknowledgement capability");
|
|
1503
|
+
}
|
|
1408
1504
|
|
|
1409
1505
|
if (currentId) {
|
|
1410
1506
|
throw new Error("Received duplicate register message");
|
|
@@ -1424,7 +1520,12 @@ class IntercomBroker {
|
|
|
1424
1520
|
supervisorId = claimedSupervisorId;
|
|
1425
1521
|
}
|
|
1426
1522
|
|
|
1427
|
-
|
|
1523
|
+
// #3760: a restored or reconnected session keeps the id peers know when it is free; never a live one.
|
|
1524
|
+
const id = this.sessionIdReclaim.grant(
|
|
1525
|
+
clientMessage.preferredSessionId,
|
|
1526
|
+
clientMessage.returnAddress,
|
|
1527
|
+
(sessionId) => this.sessions.has(sessionId),
|
|
1528
|
+
) ?? randomUUID();
|
|
1428
1529
|
setId(id);
|
|
1429
1530
|
const initialGroups = normalizeGroups(clientMessage.session.groups, clientMessage.session.group);
|
|
1430
1531
|
const legacyGroup = normalizeGroup(clientMessage.session.group ?? initialGroups.values().next().value);
|
|
@@ -1448,6 +1549,7 @@ class IntercomBroker {
|
|
|
1448
1549
|
...(typeof clientMessage.supervisorOwnerToken === "string"
|
|
1449
1550
|
? { supervisorOwnerToken: clientMessage.supervisorOwnerToken }
|
|
1450
1551
|
: {}),
|
|
1552
|
+
...(clientMessage.acknowledgesReceipt === true ? { acknowledgesReceipt: true as const } : {}),
|
|
1451
1553
|
});
|
|
1452
1554
|
|
|
1453
1555
|
if (this.shutdownTimer) {
|
|
@@ -1455,10 +1557,63 @@ class IntercomBroker {
|
|
|
1455
1557
|
this.shutdownTimer = null;
|
|
1456
1558
|
}
|
|
1457
1559
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1560
|
+
const rejoined = this.sessions.get(id);
|
|
1561
|
+
const consumedCapability =
|
|
1562
|
+
supervisorId && isSupervisorRegistration(clientMessage.supervisor) ? clientMessage.supervisor.capability : undefined;
|
|
1563
|
+
const completeRegistration = (held: readonly HeldWrite[] = []): void => {
|
|
1564
|
+
// #3761: echoing the capability tells the client this broker understands `received`; an old broker never does.
|
|
1565
|
+
writeMessageIfOpen(socket, {
|
|
1566
|
+
type: "registered",
|
|
1567
|
+
sessionId: id,
|
|
1568
|
+
...(supervisorId ? { supervisorSessionId: supervisorId } : {}),
|
|
1569
|
+
...(clientMessage.acknowledgesReceipt === true ? { acknowledgesReceipt: true as const } : {}),
|
|
1570
|
+
});
|
|
1571
|
+
if (isAgentRecipient(info)) this.broadcastToMemberships({ type: "session_joined", session: info }, sessionGroups(info), id);
|
|
1572
|
+
// #3761: messages written to this recipient's previous connection that it never confirmed saving go out again.
|
|
1573
|
+
if (rejoined?.acknowledgesReceipt === true) {
|
|
1574
|
+
const heldFrames = new Set(held.map((write) => write.message));
|
|
1575
|
+
for (const frame of this.receipts.framesFor(deliveryTargetIdentity(rejoined))) {
|
|
1576
|
+
if (!heldFrames.has(frame)) writeMessageIfOpen(socket, frame);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
// Multi-node S5: frames written while registration waited for the presence lease follow `registered`.
|
|
1580
|
+
flushHeldWrites(socket, held);
|
|
1581
|
+
// #3760: a restored subagent is back under the capability its owner reserved it with; the sends held
|
|
1582
|
+
// for it while it was being restored go out now, once each and in the order they arrived.
|
|
1583
|
+
if (consumedCapability !== undefined) {
|
|
1584
|
+
const registered = info.name?.trim();
|
|
1585
|
+
for (const send of this.restoringRecipients.consume(
|
|
1586
|
+
consumedCapability,
|
|
1587
|
+
registered ? { childName: registered, groups: sessionGroups(info) } : undefined,
|
|
1588
|
+
))
|
|
1589
|
+
this.dispatchSend(send.socket, send.clientMessage, send.senderId);
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
// Multi-node S5: brokers on other hosts find this session through its presence lease, so registration
|
|
1593
|
+
// completes once the lease is written (or bounded attempts found it still held elsewhere). The session is
|
|
1594
|
+
// routable meanwhile, so its frames are held until `registered` is written.
|
|
1595
|
+
const presence =
|
|
1596
|
+
rejoined === undefined
|
|
1597
|
+
? undefined
|
|
1598
|
+
: this.multiNode?.sessionRegistered(
|
|
1599
|
+
rejoined,
|
|
1600
|
+
consumedCapability === undefined
|
|
1601
|
+
? undefined
|
|
1602
|
+
: {
|
|
1603
|
+
capability: consumedCapability,
|
|
1604
|
+
...(info.name?.trim() ? { childName: info.name.trim() } : {}),
|
|
1605
|
+
groups: sessionGroups(info),
|
|
1606
|
+
},
|
|
1607
|
+
);
|
|
1608
|
+
if (presence === undefined) completeRegistration();
|
|
1609
|
+
else {
|
|
1610
|
+
holdWrites(socket);
|
|
1611
|
+
void presence.then(() => {
|
|
1612
|
+
const held = releaseHeldWrites(socket);
|
|
1613
|
+
if (this.sessions.get(id) === rejoined) completeRegistration(held);
|
|
1614
|
+
else discardHeldWrites(held);
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1462
1617
|
break;
|
|
1463
1618
|
}
|
|
1464
1619
|
|
|
@@ -1479,19 +1634,27 @@ class IntercomBroker {
|
|
|
1479
1634
|
|
|
1480
1635
|
const requester = currentId ? this.sessions.get(currentId) : undefined;
|
|
1481
1636
|
if (requester === undefined) throw new Error("Session not found");
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1637
|
+
const requestId = clientMessage.requestId;
|
|
1638
|
+
const selectedGroup = clientMessage.group;
|
|
1639
|
+
const visible = (sessions: ReadonlyMap<string, Pick<ConnectedSession, "info">>): SessionInfo[] =>
|
|
1640
|
+
typeof selectedGroup === "string"
|
|
1641
|
+
? this.canInspectSelectedGroup(requester, selectedGroup)
|
|
1642
|
+
? sessionsInGroup(sessions, selectedGroup)
|
|
1486
1643
|
: []
|
|
1487
|
-
: sessionsVisibleTo(
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1644
|
+
: sessionsVisibleTo(sessions, requester.info);
|
|
1645
|
+
const respond = (sessions: SessionInfo[]): void => {
|
|
1646
|
+
writeMessageIfOpen(socket, {
|
|
1647
|
+
type: "sessions",
|
|
1648
|
+
requestId,
|
|
1649
|
+
sessions,
|
|
1650
|
+
workflowStages: this.workflowStagesVisibleTo(requester, selectedGroup),
|
|
1651
|
+
workflowFutureStages: this.workflowFutureStagesVisibleTo(requester, selectedGroup),
|
|
1652
|
+
});
|
|
1653
|
+
};
|
|
1654
|
+
const multiNode = this.multiNode;
|
|
1655
|
+
if (multiNode === undefined) respond(visible(this.sessions));
|
|
1656
|
+
// Multi-node S5: a session connected through another node is a live peer too, under the same visibility.
|
|
1657
|
+
else void multiNode.remoteSessions().then((remote) => respond([...visible(this.sessions), ...visible(remote)]));
|
|
1495
1658
|
break;
|
|
1496
1659
|
}
|
|
1497
1660
|
|
|
@@ -1531,6 +1694,78 @@ class IntercomBroker {
|
|
|
1531
1694
|
break;
|
|
1532
1695
|
}
|
|
1533
1696
|
|
|
1697
|
+
case "reserve_restoring_recipient": {
|
|
1698
|
+
const owner = this.sessions.get(currentId);
|
|
1699
|
+
if (
|
|
1700
|
+
owner === undefined || typeof clientMessage.requestId !== "string" ||
|
|
1701
|
+
typeof clientMessage.capability !== "string" || !clientMessage.capability ||
|
|
1702
|
+
(clientMessage.group !== undefined && typeof clientMessage.group !== "string") ||
|
|
1703
|
+
(clientMessage.dormant !== undefined && typeof clientMessage.dormant !== "boolean") ||
|
|
1704
|
+
(clientMessage.aliases !== undefined && !isRestoringRecipientAliasList(clientMessage.aliases))
|
|
1705
|
+
) {
|
|
1706
|
+
throw new Error("Invalid reserve_restoring_recipient message");
|
|
1707
|
+
}
|
|
1708
|
+
// Only the supervisor the capability was issued to may hold sends for that child (#3760).
|
|
1709
|
+
const childName = this.supervisorChannel.supervisedChildName(clientMessage.capability, currentId);
|
|
1710
|
+
const aliases = (clientMessage.aliases ?? []) as string[];
|
|
1711
|
+
if (childName === undefined || !aliases.every(isRestoringRecipientAlias)) {
|
|
1712
|
+
writeMessageIfOpen(socket, {
|
|
1713
|
+
type: "restoring_recipient_ack",
|
|
1714
|
+
requestId: clientMessage.requestId,
|
|
1715
|
+
error: childName === undefined
|
|
1716
|
+
? "Only the supervisor authorized for this subagent can reserve its Intercom targets"
|
|
1717
|
+
: "A restoring subagent can be reserved only under its name and session ids",
|
|
1718
|
+
});
|
|
1719
|
+
break;
|
|
1720
|
+
}
|
|
1721
|
+
this.restoringRecipients.reserve(
|
|
1722
|
+
clientMessage.capability,
|
|
1723
|
+
currentId,
|
|
1724
|
+
childName,
|
|
1725
|
+
normalizeGroup(typeof clientMessage.group === "string" ? clientMessage.group : owner.registrationGroup),
|
|
1726
|
+
aliases,
|
|
1727
|
+
{ dormant: clientMessage.dormant === true },
|
|
1728
|
+
);
|
|
1729
|
+
this.multiNode?.restoringReserved(
|
|
1730
|
+
clientMessage.capability,
|
|
1731
|
+
currentId,
|
|
1732
|
+
childName,
|
|
1733
|
+
normalizeGroup(typeof clientMessage.group === "string" ? clientMessage.group : owner.registrationGroup),
|
|
1734
|
+
aliases,
|
|
1735
|
+
clientMessage.dormant === true,
|
|
1736
|
+
);
|
|
1737
|
+
writeMessageIfOpen(socket, { type: "restoring_recipient_ack", requestId: clientMessage.requestId });
|
|
1738
|
+
break;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
case "release_restoring_recipient": {
|
|
1742
|
+
if (
|
|
1743
|
+
typeof clientMessage.requestId !== "string" || typeof clientMessage.capability !== "string" ||
|
|
1744
|
+
(clientMessage.gone !== undefined && typeof clientMessage.gone !== "string") ||
|
|
1745
|
+
(clientMessage.refusal !== undefined && typeof clientMessage.refusal !== "string")
|
|
1746
|
+
) {
|
|
1747
|
+
throw new Error("Invalid release_restoring_recipient message");
|
|
1748
|
+
}
|
|
1749
|
+
const released = this.restoringRecipients.release(
|
|
1750
|
+
clientMessage.capability,
|
|
1751
|
+
currentId,
|
|
1752
|
+
typeof clientMessage.gone === "string" ? clientMessage.gone.slice(0, RESTORING_RECIPIENT_GONE_REASON_MAX) : undefined,
|
|
1753
|
+
typeof clientMessage.refusal === "string" && clientMessage.refusal.trim()
|
|
1754
|
+
? clientMessage.refusal.slice(0, RESTORING_RECIPIENT_GONE_REASON_MAX)
|
|
1755
|
+
: undefined,
|
|
1756
|
+
);
|
|
1757
|
+
if (released !== undefined) {
|
|
1758
|
+
this.multiNode?.restoringReleased(clientMessage.capability, currentId, released.gone, released.refusal);
|
|
1759
|
+
}
|
|
1760
|
+
writeMessageIfOpen(socket, { type: "restoring_recipient_ack", requestId: clientMessage.requestId });
|
|
1761
|
+
for (const send of released?.held ?? []) {
|
|
1762
|
+
if (released?.gone !== undefined) this.failHeldSend(send, released.gone);
|
|
1763
|
+
else if (released?.refusal !== undefined) this.failHeldSend(send, released.refusal);
|
|
1764
|
+
else this.dispatchSend(send.socket, send.clientMessage, send.senderId);
|
|
1765
|
+
}
|
|
1766
|
+
break;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1534
1769
|
case "register_pending_stage_route": {
|
|
1535
1770
|
if (
|
|
1536
1771
|
typeof clientMessage.runId !== "string" ||
|
|
@@ -1544,7 +1779,21 @@ class IntercomBroker {
|
|
|
1544
1779
|
const owner = this.sessions.get(currentId);
|
|
1545
1780
|
const existing = this.pendingStageRoutes.get(clientMessage.runId);
|
|
1546
1781
|
const ownerGroup = normalizeGroup(owner?.registrationGroup);
|
|
1547
|
-
const
|
|
1782
|
+
const dormant = clientMessage.dormant === true;
|
|
1783
|
+
const liveExisting = existing !== undefined && this.sessions.has(existing.sessionId) ? existing : undefined;
|
|
1784
|
+
// #3760: a restarted owner queues for its interrupted run under a dormant route. The process that
|
|
1785
|
+
// resumes the run holds the run's lease and a capability of its own, so it takes the route over and
|
|
1786
|
+
// the dormant owner is told to stop answering for it; a dormant route never takes over another.
|
|
1787
|
+
const yieldedDormant =
|
|
1788
|
+
liveExisting?.dormant === true &&
|
|
1789
|
+
!dormant &&
|
|
1790
|
+
liveExisting.sessionId !== currentId &&
|
|
1791
|
+
liveExisting.capability !== clientMessage.capability &&
|
|
1792
|
+
normalizeGroup(clientMessage.group) === ownerGroup &&
|
|
1793
|
+
liveExisting.group === ownerGroup
|
|
1794
|
+
? liveExisting
|
|
1795
|
+
: undefined;
|
|
1796
|
+
const activeExisting = yieldedDormant === undefined ? liveExisting : undefined;
|
|
1548
1797
|
if (
|
|
1549
1798
|
owner === undefined ||
|
|
1550
1799
|
normalizeGroup(clientMessage.group) !== ownerGroup ||
|
|
@@ -1611,7 +1860,13 @@ class IntercomBroker {
|
|
|
1611
1860
|
sessionId: currentId,
|
|
1612
1861
|
group: ownerGroup,
|
|
1613
1862
|
capability: clientMessage.capability,
|
|
1863
|
+
...(dormant ? { dormant: true as const } : {}),
|
|
1614
1864
|
});
|
|
1865
|
+
if (yieldedDormant !== undefined) {
|
|
1866
|
+
const revoked = this.sessions.get(yieldedDormant.sessionId);
|
|
1867
|
+
if (revoked !== undefined)
|
|
1868
|
+
writeMessageIfOpen(revoked.socket, { type: "pending_stage_route_revoked", runId: clientMessage.runId });
|
|
1869
|
+
}
|
|
1615
1870
|
if (clientMessage.stages !== undefined || possibleStages !== undefined) {
|
|
1616
1871
|
this.workflowRosters.set(clientMessage.runId, {
|
|
1617
1872
|
ownerSessionId: currentId,
|
|
@@ -1728,22 +1983,17 @@ class IntercomBroker {
|
|
|
1728
1983
|
|
|
1729
1984
|
case "send":
|
|
1730
1985
|
case "supervisor_send": {
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
this.
|
|
1740
|
-
this.
|
|
1741
|
-
|
|
1742
|
-
this.canControlLiveWorkflowStage,
|
|
1743
|
-
this.resolveLegacyWorkflowStageTarget,
|
|
1744
|
-
writeMessageWithOutcome,
|
|
1745
|
-
this.isNonAgentWorkflowTarget,
|
|
1746
|
-
);
|
|
1986
|
+
this.dispatchSend(socket, clientMessage, currentId);
|
|
1987
|
+
break;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
case "received": {
|
|
1991
|
+
if (typeof clientMessage.messageId !== "string") throw new Error("Invalid received message");
|
|
1992
|
+
const recipient = this.sessions.get(currentId);
|
|
1993
|
+
if (recipient?.acknowledgesReceipt === true) {
|
|
1994
|
+
this.multiNode?.received(recipient, clientMessage.messageId);
|
|
1995
|
+
this.receipts.settle(clientMessage.messageId, deliveryTargetIdentity(recipient), recipient);
|
|
1996
|
+
}
|
|
1747
1997
|
break;
|
|
1748
1998
|
}
|
|
1749
1999
|
|
|
@@ -1771,6 +2021,8 @@ class IntercomBroker {
|
|
|
1771
2021
|
(target, message) => writeMessageIfOpen(target, message),
|
|
1772
2022
|
this.pendingQuestions,
|
|
1773
2023
|
);
|
|
2024
|
+
const updated = this.sessions.get(currentId);
|
|
2025
|
+
if (updated !== undefined) this.multiNode?.sessionUpdated(updated);
|
|
1774
2026
|
break;
|
|
1775
2027
|
}
|
|
1776
2028
|
|
|
@@ -1779,10 +2031,191 @@ class IntercomBroker {
|
|
|
1779
2031
|
}
|
|
1780
2032
|
}
|
|
1781
2033
|
|
|
2034
|
+
/** Route one send or ask frame, as received or again after a restoring subagent's hold ends (#3760). */
|
|
2035
|
+
private dispatchSend(socket: net.Socket, clientMessage: SendClientMessage, senderId: string | null): void {
|
|
2036
|
+
handleBrokerSend(
|
|
2037
|
+
socket,
|
|
2038
|
+
clientMessage,
|
|
2039
|
+
senderId,
|
|
2040
|
+
this.sessions,
|
|
2041
|
+
this.deliveredMessages,
|
|
2042
|
+
writeMessageIfOpen,
|
|
2043
|
+
this.supervisorChannel,
|
|
2044
|
+
this.pendingQuestions,
|
|
2045
|
+
this.routePendingStage,
|
|
2046
|
+
this.resolveLiveWorkflowStage,
|
|
2047
|
+
this.canControlLiveWorkflowStage,
|
|
2048
|
+
this.resolveLegacyWorkflowStageTarget,
|
|
2049
|
+
writeMessageWithOutcome,
|
|
2050
|
+
this.isNonAgentWorkflowTarget,
|
|
2051
|
+
this.holdForRestoringRecipient,
|
|
2052
|
+
this.receipts,
|
|
2053
|
+
this.routeRemote,
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
/** Multi-node S5: a send no local session resolved goes to the shared state once it is open. */
|
|
2058
|
+
private routeRemoteSend(request: RemoteSendRequest): void {
|
|
2059
|
+
if (this.multiNode !== undefined) {
|
|
2060
|
+
this.multiNode.routeSend(request);
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
// Off, or a send to a session on this broker: local delivery never depends on the shared database (spec §8).
|
|
2064
|
+
if (this.multiNodeStatus === "off" || (this.multiNodeStatus === "unavailable" && request.localTarget !== undefined)) {
|
|
2065
|
+
request.notFound();
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
if (this.multiNodeStatus === "starting") {
|
|
2069
|
+
if (request.localTarget === undefined) {
|
|
2070
|
+
void this.multiNodeFirstAttempt.then(() => this.routeRemoteSend(request));
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
// A send to a session on this broker waits for the first opening at most as long as the router's own retry
|
|
2074
|
+
// lookup would, then is delivered locally; whichever comes second does nothing, so it is answered once.
|
|
2075
|
+
let answered = false;
|
|
2076
|
+
const bound = setTimeout(() => {
|
|
2077
|
+
answered = true;
|
|
2078
|
+
request.notFound();
|
|
2079
|
+
}, LOCAL_RETRY_LOOKUP_BOUND_MS);
|
|
2080
|
+
void this.multiNodeFirstAttempt.then(() => {
|
|
2081
|
+
if (answered) return;
|
|
2082
|
+
answered = true;
|
|
2083
|
+
clearTimeout(bound);
|
|
2084
|
+
this.routeRemoteSend(request);
|
|
2085
|
+
});
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
writeMessageIfOpen(request.socket, {
|
|
2089
|
+
type: "delivery_failed",
|
|
2090
|
+
messageId: request.message.id,
|
|
2091
|
+
...(request.attemptId === undefined ? {} : { attemptId: request.attemptId }),
|
|
2092
|
+
reason: SHARED_STORAGE_UNAVAILABLE_REASON,
|
|
2093
|
+
...(request.requirePendingReply ? { reasonCode: "session_not_found" as const } : {}),
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
/**
|
|
2098
|
+
* The shared state is loaded lazily, so a single-node broker never loads the host storage layer. A database that
|
|
2099
|
+
* cannot be opened is retried with exponential backoff; meanwhile local delivery continues.
|
|
2100
|
+
*/
|
|
2101
|
+
private startMultiNode(setting: Record<string, unknown>): void {
|
|
2102
|
+
this.multiNodeStatus = "starting";
|
|
2103
|
+
let settleFirstAttempt: () => void = () => {};
|
|
2104
|
+
this.multiNodeFirstAttempt = new Promise<void>((resolve) => {
|
|
2105
|
+
settleFirstAttempt = resolve;
|
|
2106
|
+
});
|
|
2107
|
+
const settle = (status: "on" | "off" | "unavailable"): void => {
|
|
2108
|
+
this.multiNodeStatus = status;
|
|
2109
|
+
settleFirstAttempt();
|
|
2110
|
+
};
|
|
2111
|
+
const report = (error: unknown): void => {
|
|
2112
|
+
console.error(`Intercom multi-node routing is unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
2113
|
+
};
|
|
2114
|
+
void (async () => {
|
|
2115
|
+
const startup = await resolveMultiNodeStartup(setting, dirname(INTERCOM_DIR), async () => {
|
|
2116
|
+
const loaded = await import("./open-broker-state.js");
|
|
2117
|
+
type Source = Parameters<typeof loaded.openIntercomBrokerState>[0];
|
|
2118
|
+
const opener: BrokerStateOpener<Source> = {
|
|
2119
|
+
resolveBrokerStateSource: (value, agentDir) =>
|
|
2120
|
+
loaded.resolveBrokerStateSource(value as Parameters<typeof loaded.resolveBrokerStateSource>[0], agentDir),
|
|
2121
|
+
openIntercomBrokerState: (source) => loaded.openIntercomBrokerState(source),
|
|
2122
|
+
};
|
|
2123
|
+
return opener;
|
|
2124
|
+
});
|
|
2125
|
+
if (startup.status === "off") {
|
|
2126
|
+
// A standalone binary has no host storage module, and a selection may not resolve to a database: both
|
|
2127
|
+
// leave this broker single-node, exactly as without a shared `storage`.
|
|
2128
|
+
if (startup.error !== undefined) console.error(`Intercom multi-node routing is off: ${startup.reason}`);
|
|
2129
|
+
settle("off");
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
const { opener, source } = startup;
|
|
2133
|
+
for (let attempt = 0; !this.shuttingDown; attempt++) {
|
|
2134
|
+
try {
|
|
2135
|
+
const opened = await opener.openIntercomBrokerState(source);
|
|
2136
|
+
try {
|
|
2137
|
+
this.multiNode = await MultiNodeRouter.start({
|
|
2138
|
+
...opened,
|
|
2139
|
+
host: {
|
|
2140
|
+
sessions: this.sessions,
|
|
2141
|
+
write: writeMessageIfOpen,
|
|
2142
|
+
writeConfirmed: writeMessageWithOutcome,
|
|
2143
|
+
supervisorCache: this.supervisorChannel,
|
|
2144
|
+
pendingQuestions: this.pendingQuestions,
|
|
2145
|
+
deliveredMessages: this.deliveredMessages,
|
|
2146
|
+
dispatch: (socket, clientMessage, senderId) => this.dispatchSend(socket, clientMessage, senderId),
|
|
2147
|
+
},
|
|
2148
|
+
});
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
await opened.bus.close().catch(() => {});
|
|
2151
|
+
await opened.state.close().catch(() => {});
|
|
2152
|
+
throw error;
|
|
2153
|
+
}
|
|
2154
|
+
// Reservations acknowledged before routing started (or while an earlier opening failed) were kept only
|
|
2155
|
+
// here; mirror the ones still in force so other brokers hold sends for them too. Releases and owner
|
|
2156
|
+
// departures from now on reach the router behind these reserves.
|
|
2157
|
+
for (const reservation of this.restoringRecipients.active()) {
|
|
2158
|
+
this.multiNode.restoringReserved(
|
|
2159
|
+
reservation.capability,
|
|
2160
|
+
reservation.ownerSessionId,
|
|
2161
|
+
reservation.childName,
|
|
2162
|
+
reservation.group,
|
|
2163
|
+
reservation.aliases,
|
|
2164
|
+
reservation.dormant,
|
|
2165
|
+
);
|
|
2166
|
+
if (reservation.gone !== undefined) {
|
|
2167
|
+
this.multiNode.restoringReleased(reservation.capability, reservation.ownerSessionId, reservation.gone);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
if (this.shuttingDown) await this.multiNode.close().catch(() => {});
|
|
2171
|
+
settle("on");
|
|
2172
|
+
return;
|
|
2173
|
+
} catch (error) {
|
|
2174
|
+
report(error);
|
|
2175
|
+
settle("unavailable");
|
|
2176
|
+
await new Promise<void>((resolve) => {
|
|
2177
|
+
setTimeout(resolve, Math.min(1_000 * 2 ** attempt, OUTBOX_BACKUP_POLL_INTERVAL_MS)).unref();
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
})();
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
/**
|
|
2185
|
+
* #3760: a send to a subagent its owner is restoring is held, by whichever sender, instead of failing with
|
|
2186
|
+
* `Session not found`; a send to one its owner reported gone gets the gone refusal.
|
|
2187
|
+
*/
|
|
2188
|
+
private readonly holdForRestoringRecipient: RestoringRecipientHold = ({ socket, from, clientMessage, target, messageId, attemptId }) => {
|
|
2189
|
+
const held: HeldSend = { socket, senderId: from.info.id, messageId, clientMessage, ...(attemptId === undefined ? {} : { attemptId }) };
|
|
2190
|
+
const admission = this.restoringRecipients.admit(
|
|
2191
|
+
target,
|
|
2192
|
+
{ id: from.info.id, info: from.info },
|
|
2193
|
+
held,
|
|
2194
|
+
clientMessage.awaitRestoringRecipient === true,
|
|
2195
|
+
);
|
|
2196
|
+
if (admission.kind === "none") return false;
|
|
2197
|
+
if (admission.kind === "refused") this.failHeldSend(held, admission.reason);
|
|
2198
|
+
else writeMessageIfOpen(socket, { type: "send_held", messageId, ...(attemptId === undefined ? {} : { attemptId }), timeoutMs: admission.timeoutMs });
|
|
2199
|
+
return true;
|
|
2200
|
+
};
|
|
2201
|
+
|
|
2202
|
+
private failHeldSend(held: HeldSend, reason: string): void {
|
|
2203
|
+
writeMessageIfOpen(held.socket, {
|
|
2204
|
+
type: "delivery_failed",
|
|
2205
|
+
messageId: held.messageId,
|
|
2206
|
+
...(held.attemptId === undefined ? {} : { attemptId: held.attemptId }),
|
|
2207
|
+
reason,
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
|
|
1782
2211
|
private disconnectSession(sessionId: string): void {
|
|
1783
2212
|
const departed = this.sessions.get(sessionId);
|
|
1784
2213
|
if (!departed) return;
|
|
1785
2214
|
|
|
2215
|
+
this.restoringRecipients.dropSender(sessionId);
|
|
2216
|
+
// #3760: a departed owner restores nothing more; what it held goes through ordinary routing once it is gone.
|
|
2217
|
+
const unreserved = this.restoringRecipients.dropOwner(sessionId);
|
|
2218
|
+
|
|
1786
2219
|
this.pendingQuestions.pruneSender(sessionId);
|
|
1787
2220
|
for (const question of this.pendingQuestions.takeForTarget(sessionId)) {
|
|
1788
2221
|
const asker = this.sessions.get(question.senderSessionId);
|
|
@@ -1832,7 +2265,10 @@ class IntercomBroker {
|
|
|
1832
2265
|
if (activation.sessionId === sessionId) this.liveWorkflowStageRouteActivations.delete(requestId);
|
|
1833
2266
|
}
|
|
1834
2267
|
this.sessions.delete(sessionId);
|
|
2268
|
+
this.multiNode?.sessionLeft(departed);
|
|
2269
|
+
this.sessionIdReclaim.retire(sessionId, departed.registrationReturnAddress);
|
|
1835
2270
|
if (isAgentRecipient(departed.info)) this.broadcastToMemberships({ type: "session_left", sessionId }, sessionGroups(departed.info), sessionId);
|
|
2271
|
+
for (const send of unreserved) this.dispatchSend(send.socket, send.clientMessage, send.senderId);
|
|
1836
2272
|
}
|
|
1837
2273
|
/** Deliver a broadcast once to each session represented in any given group. */
|
|
1838
2274
|
private broadcastToMemberships(msg: BrokerMessage, groups: ReadonlySet<string>, exclude?: string): void {
|
|
@@ -1863,6 +2299,8 @@ class IntercomBroker {
|
|
|
1863
2299
|
}
|
|
1864
2300
|
|
|
1865
2301
|
private shutdown(): void {
|
|
2302
|
+
if (this.shuttingDown) return;
|
|
2303
|
+
this.shuttingDown = true;
|
|
1866
2304
|
console.log("Broker shutting down");
|
|
1867
2305
|
|
|
1868
2306
|
for (const session of this.sessions.values()) {
|
|
@@ -1873,9 +2311,22 @@ class IntercomBroker {
|
|
|
1873
2311
|
this.pendingStageAcknowledgments.clear();
|
|
1874
2312
|
this.liveWorkflowStageRoutes.clear();
|
|
1875
2313
|
this.liveWorkflowStageRouteActivations.clear();
|
|
2314
|
+
this.restoringRecipients.clear();
|
|
2315
|
+
this.receipts.clear();
|
|
1876
2316
|
for (const pending of this.pendingStageNotificationAcknowledgments.values()) clearTimeout(pending.timeout);
|
|
1877
2317
|
this.pendingStageNotificationAcknowledgments.clear();
|
|
1878
2318
|
this.deliveredMessages.close();
|
|
2319
|
+
const multiNode = this.multiNode;
|
|
2320
|
+
if (multiNode === undefined) {
|
|
2321
|
+
this.exitAfterShutdown();
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
// Give this broker's leases up so other brokers see its sessions offline at once, within a bound.
|
|
2325
|
+
const bound = new Promise<void>((resolve) => setTimeout(resolve, MULTI_NODE_CLOSE_BOUND_MS).unref());
|
|
2326
|
+
void Promise.race([multiNode.close().catch(() => {}), bound]).finally(() => this.exitAfterShutdown());
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
private exitAfterShutdown(): never {
|
|
1879
2330
|
const ownsRuntime = this.readPidFile() === process.pid;
|
|
1880
2331
|
this.unlinkRuntimeFilesIfOwned();
|
|
1881
2332
|
// Node unlinks a Unix socket path on server.close() even after a successor
|