@bastani/atomic 0.9.39 → 0.9.40-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -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/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +18 -0
- package/dist/builtin/workflows/README.md +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +8 -8
- package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/adapt.native.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/audit.md +4 -2
- package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +22 -9
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/distill.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/extract.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/harden.md +2 -4
- package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +4 -4
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +6 -6
- package/dist/builtin/workflows/skills/impeccable/reference/onboard.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +10 -0
- package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/VERSION +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/impeccable +6 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/impeccable.cmd +47 -22
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-dom.js +34 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-session.js +25 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +33 -8
- package/dist/builtin/workflows/src/durable/journal/resolution.d.ts +7 -7
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +22855 -22308
- package/dist/builtin/workflows/src/index.js +106 -35
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +16 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session-extension-bindings.d.ts +2 -0
- package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
- package/dist/core/agent-session-extension-bindings.js +6 -6
- package/dist/core/agent-session-extension-bindings.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +6 -3
- package/dist/core/agent-session-methods.d.ts.map +1 -1
- package/dist/core/agent-session-methods.js.map +1 -1
- package/dist/core/agent-session-persistent-custom-messages.d.ts +7 -2
- package/dist/core/agent-session-persistent-custom-messages.d.ts.map +1 -1
- package/dist/core/agent-session-persistent-custom-messages.js +29 -12
- package/dist/core/agent-session-persistent-custom-messages.js.map +1 -1
- package/dist/core/agent-session-recovery.d.ts +14 -0
- package/dist/core/agent-session-recovery.d.ts.map +1 -0
- package/dist/core/agent-session-recovery.js +83 -0
- package/dist/core/agent-session-recovery.js.map +1 -0
- package/dist/core/agent-session-tasks.d.ts.map +1 -1
- package/dist/core/agent-session-tasks.js +4 -6
- package/dist/core/agent-session-tasks.js.map +1 -1
- package/dist/core/agent-session-types.d.ts +13 -0
- package/dist/core/agent-session-types.d.ts.map +1 -1
- package/dist/core/agent-session-types.js.map +1 -1
- package/dist/core/agent-session.d.ts +2 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +4 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/api-types.d.ts +6 -2
- package/dist/core/extensions/api-types.d.ts.map +1 -1
- package/dist/core/extensions/api-types.js.map +1 -1
- package/dist/core/extensions/event-results.d.ts +21 -0
- package/dist/core/extensions/event-results.d.ts.map +1 -1
- package/dist/core/extensions/event-results.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader-api.d.ts.map +1 -1
- package/dist/core/extensions/loader-api.js +4 -0
- package/dist/core/extensions/loader-api.js.map +1 -1
- package/dist/core/extensions/runner-events.d.ts +9 -2
- package/dist/core/extensions/runner-events.d.ts.map +1 -1
- package/dist/core/extensions/runner-events.js +29 -0
- package/dist/core/extensions/runner-events.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +5 -3
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +7 -2
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/runtime-types.d.ts +1 -0
- package/dist/core/extensions/runtime-types.d.ts.map +1 -1
- package/dist/core/extensions/runtime-types.js.map +1 -1
- package/dist/core/extensions/session-events.d.ts +30 -2
- package/dist/core/extensions/session-events.d.ts.map +1 -1
- package/dist/core/extensions/session-events.js.map +1 -1
- package/dist/core/sdk-types.d.ts +3 -2
- package/dist/core/sdk-types.d.ts.map +1 -1
- package/dist/core/sdk-types.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +3 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-storage.d.ts.map +1 -1
- package/dist/core/session-storage.js +15 -9
- package/dist/core/session-storage.js.map +1 -1
- package/dist/core/storage/process-selection.d.ts +39 -0
- package/dist/core/storage/process-selection.d.ts.map +1 -0
- package/dist/core/storage/process-selection.js +154 -0
- package/dist/core/storage/process-selection.js.map +1 -0
- package/dist/core/storage/provider.d.ts +6 -1
- package/dist/core/storage/provider.d.ts.map +1 -1
- package/dist/core/storage/provider.js +3 -2
- package/dist/core/storage/provider.js.map +1 -1
- package/dist/core/storage/selection.d.ts +1 -1
- package/dist/core/storage/selection.d.ts.map +1 -1
- package/dist/core/storage/selection.js.map +1 -1
- package/dist/core/storage/session/jsonl-session-store.d.ts +2 -0
- package/dist/core/storage/session/jsonl-session-store.d.ts.map +1 -1
- package/dist/core/storage/session/jsonl-session-store.js +38 -2
- package/dist/core/storage/session/jsonl-session-store.js.map +1 -1
- package/dist/core/tasks/completion.d.ts +11 -2
- package/dist/core/tasks/completion.d.ts.map +1 -1
- package/dist/core/tasks/completion.js +40 -16
- package/dist/core/tasks/completion.js.map +1 -1
- package/dist/core/tasks/pause.d.ts +4 -1
- package/dist/core/tasks/pause.d.ts.map +1 -1
- package/dist/core/tasks/pause.js +8 -5
- package/dist/core/tasks/pause.js.map +1 -1
- package/dist/index-extensions.d.ts +1 -1
- package/dist/index-extensions.d.ts.map +1 -1
- package/dist/index-extensions.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +7 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-rendering.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-rendering.js +11 -1
- package/dist/modes/interactive/components/chat-session-host-rendering.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-state.d.ts +2 -0
- package/dist/modes/interactive/components/chat-session-host-state.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-state.js +2 -0
- package/dist/modes/interactive/components/chat-session-host-state.js.map +1 -1
- package/dist/modes/interactive/interactive-deferred-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-deferred-startup.js +4 -0
- package/dist/modes/interactive/interactive-deferred-startup.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts +2 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
- package/dist/modes/interactive/interactive-render-chat.js +3 -0
- package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
- package/dist/modes/interactive/interactive-session-routing.js +2 -0
- package/dist/modes/interactive/interactive-session-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-session-runtime.d.ts +2 -1
- package/dist/modes/interactive/interactive-session-runtime.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-session-runtime.js +29 -0
- package/dist/modes/interactive/interactive-session-runtime.js.map +1 -1
- package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-startup.js +4 -1
- package/dist/modes/interactive/interactive-startup.js.map +1 -1
- package/dist/modes/interactive-engine/engine-args.d.ts.map +1 -1
- package/dist/modes/interactive-engine/engine-args.js +4 -0
- package/dist/modes/interactive-engine/engine-args.js.map +1 -1
- package/dist/modes/interactive-engine/engine-render-service.d.ts.map +1 -1
- package/dist/modes/interactive-engine/engine-render-service.js +4 -4
- package/dist/modes/interactive-engine/engine-render-service.js.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.js +4 -0
- package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
- package/dist/modes/interactive-engine/remote-renderer.js +1 -1
- package/dist/modes/interactive-engine/remote-renderer.js.map +1 -1
- package/dist/modes/rpc/rpc-client-api.d.ts +4 -1
- package/dist/modes/rpc/rpc-client-api.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client-api.js +4 -0
- package/dist/modes/rpc/rpc-client-api.js.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.js +3 -0
- package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
- package/dist/modes/rpc/rpc-command-timeouts.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-command-timeouts.js +1 -0
- package/dist/modes/rpc/rpc-command-timeouts.js.map +1 -1
- package/dist/modes/rpc/rpc-input-scheduler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-input-scheduler.js +6 -1
- package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +12 -1
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +44 -11
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/tools-manager.js +1 -1
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/background-tasks.md +5 -3
- package/docs/computer-use.md +2 -2
- package/docs/environment-variables.md +4 -1
- package/docs/extensions/events.md +29 -2
- package/docs/getting-started/first-session.md +1 -1
- package/docs/guides/intercom.md +1 -1
- package/docs/intercom/operations.md +103 -1
- package/docs/intercom/reference.md +10 -3
- package/docs/intercom.md +8 -6
- package/docs/keybindings.md +1 -1
- package/docs/models.md +1 -1
- package/docs/providers.md +4 -2
- package/docs/reference/cli.md +5 -0
- package/docs/rpc/protocol.md +19 -0
- package/docs/rpc.md +3 -0
- package/docs/sdk/web-server.md +1 -1
- package/docs/sdk.md +5 -2
- package/docs/sessions.md +17 -1
- package/docs/settings.md +5 -3
- package/docs/skills.md +1 -1
- package/docs/storage.md +54 -3
- package/docs/subagents.md +49 -9
- package/docs/usage.md +1 -1
- package/docs/windows.md +14 -4
- package/docs/workflows/api-reference.md +1 -1
- package/docs/workflows/authoring.md +1 -1
- package/docs/workflows/operations.md +10 -6
- package/npm-shrinkwrap.json +37 -37
- package/package.json +4 -4
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
loadStorageAdapter,
|
|
5
|
+
openNodeSqliteDatabase,
|
|
6
|
+
resolveAdapterOptions,
|
|
7
|
+
type SqliteDatabase,
|
|
8
|
+
type StorageOpenContext,
|
|
9
|
+
type StorageSettingsSelection,
|
|
10
|
+
StorageUnavailableError,
|
|
11
|
+
selectStorage,
|
|
12
|
+
validateStoragePrefix,
|
|
13
|
+
} from "@bastani/atomic/storage";
|
|
14
|
+
import { type DatabaseWakeUpBusOptions, openDatabaseWakeUpBus } from "./database-wake-up-bus.js";
|
|
15
|
+
import type { IntercomBrokerStateStore } from "./multinode-state-store.js";
|
|
16
|
+
import { openSqlHeldSendStore } from "./sql-held-send-store.js";
|
|
17
|
+
import { openSqlKnownSessionStore } from "./sql-known-session-store.js";
|
|
18
|
+
import { openSqlBrokerLeaseStore } from "./sql-lease-store.js";
|
|
19
|
+
import { openSqlOutboxStore } from "./sql-outbox-store.js";
|
|
20
|
+
import { openSqlPendingAskStore } from "./sql-pending-ask-store.js";
|
|
21
|
+
import { openSqlRestoringReservationStore } from "./sql-restoring-reservation-store.js";
|
|
22
|
+
import { openSqlSentMessageAuthority } from "./sql-sent-message-authority.js";
|
|
23
|
+
import type { WakeUpBus } from "./wake-up-bus.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* S5 (specs/2026-10-19-intercom-multi-node.md §4, §14): opens the shared Intercom broker state on the top-level
|
|
27
|
+
* `storage` selection of the agent directory's settings. The broker loads this module lazily, only when that
|
|
28
|
+
* selection is set, so single-node brokers never load the host storage layer.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** Where the shared Intercom state lives: a database opener and the selection's table prefix. */
|
|
32
|
+
export interface SharedDatabaseSource {
|
|
33
|
+
/** Opens the connection the stores and the wake-up bus share. */
|
|
34
|
+
readonly openDatabase: () => Promise<SqliteDatabase>;
|
|
35
|
+
readonly prefix?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface OpenedBrokerState {
|
|
39
|
+
readonly state: IntercomBrokerStateStore;
|
|
40
|
+
readonly bus: WakeUpBus;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Why multi-node routing is off: there is no `SqliteDatabase` to share, so Intercom behaves exactly as today. */
|
|
44
|
+
export type BrokerStateOffReason = "no-storage" | "memory" | "domain-only-adapter";
|
|
45
|
+
|
|
46
|
+
export type BrokerStateSource =
|
|
47
|
+
| { readonly status: "off"; readonly reason: BrokerStateOffReason }
|
|
48
|
+
| { readonly status: "shared"; readonly source: SharedDatabaseSource };
|
|
49
|
+
|
|
50
|
+
function resolveSettingsPath(path: string, agentDir: string): string {
|
|
51
|
+
if (path === "~") return homedir();
|
|
52
|
+
if (path.startsWith("~/") || (process.platform === "win32" && path.startsWith("~\\"))) {
|
|
53
|
+
return join(homedir(), path.slice(2));
|
|
54
|
+
}
|
|
55
|
+
return isAbsolute(path) ? path : resolve(dirname(agentDir), path);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Decide from a top-level `storage` value where the shared state lives. Invalid configuration throws the storage
|
|
60
|
+
* layer's own `StorageConfigError` or `StorageAdapterLoadError`; it never selects another store.
|
|
61
|
+
*/
|
|
62
|
+
export async function resolveBrokerStateSource(
|
|
63
|
+
value: StorageSettingsSelection | undefined,
|
|
64
|
+
agentDir: string,
|
|
65
|
+
): Promise<BrokerStateSource> {
|
|
66
|
+
if (value === undefined) return { status: "off", reason: "no-storage" };
|
|
67
|
+
const selection = selectStorage(value, "settings", "storage", join(dirname(agentDir), "storage.sqlite"));
|
|
68
|
+
if (selection.kind === "memory") return { status: "off", reason: "memory" };
|
|
69
|
+
if (selection.kind === "sqlite") {
|
|
70
|
+
if (selection.path === "" || selection.path === ":memory:") return { status: "off", reason: "memory" };
|
|
71
|
+
const path = resolveSettingsPath(selection.path, agentDir);
|
|
72
|
+
validateStoragePrefix(selection.prefix);
|
|
73
|
+
return {
|
|
74
|
+
status: "shared",
|
|
75
|
+
source: { openDatabase: () => openNodeSqliteDatabase(path), prefix: selection.prefix },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (selection.kind !== "adapter") return { status: "off", reason: "no-storage" };
|
|
79
|
+
validateStoragePrefix(selection.prefix);
|
|
80
|
+
const context: StorageOpenContext = { agentDir, domain: "intercom" };
|
|
81
|
+
// Destructured: the broker's runtime-graph walker reads a bare `module` identifier as the CJS module object.
|
|
82
|
+
const { module: adapterModule } = selection;
|
|
83
|
+
const adapter = await loadStorageAdapter(adapterModule, context);
|
|
84
|
+
const hook = adapter.openDatabase?.bind(adapter);
|
|
85
|
+
if (hook === undefined) return { status: "off", reason: "domain-only-adapter" };
|
|
86
|
+
const options = await resolveAdapterOptions(selection, context);
|
|
87
|
+
const location = { adapter: adapterModule, location: adapterModule };
|
|
88
|
+
return {
|
|
89
|
+
status: "shared",
|
|
90
|
+
source: {
|
|
91
|
+
prefix: selection.prefix,
|
|
92
|
+
async openDatabase() {
|
|
93
|
+
try {
|
|
94
|
+
return await hook(options, context);
|
|
95
|
+
} catch (cause) {
|
|
96
|
+
if (cause instanceof StorageUnavailableError) throw cause;
|
|
97
|
+
throw new StorageUnavailableError(location, cause as Error);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A view of `database` whose `close` leaves it open. The wake-up bus borrows the stores' connection: a SQLite driver
|
|
106
|
+
* that waits for a lock synchronously (node:sqlite) would otherwise block this process on a second connection's
|
|
107
|
+
* write while one of the stores' transactions is suspended between statements.
|
|
108
|
+
*/
|
|
109
|
+
export function borrowedDatabase(database: SqliteDatabase): SqliteDatabase {
|
|
110
|
+
return {
|
|
111
|
+
descriptor: database.descriptor,
|
|
112
|
+
exec: (sql) => database.exec(sql),
|
|
113
|
+
run: (sql, ...params) => database.run(sql, ...params),
|
|
114
|
+
get: (sql, ...params) => database.get(sql, ...params),
|
|
115
|
+
all: (sql, ...params) => database.all(sql, ...params),
|
|
116
|
+
transaction: (callback) => database.transaction(callback),
|
|
117
|
+
close: async () => {},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Compose S1–S4 and the S5 known-session store, and the wake-up bus, on one connection. */
|
|
122
|
+
export async function openIntercomBrokerState(
|
|
123
|
+
source: SharedDatabaseSource,
|
|
124
|
+
busOptions: Pick<DatabaseWakeUpBusOptions, "pollIntervalMs" | "schedule"> = {},
|
|
125
|
+
): Promise<OpenedBrokerState> {
|
|
126
|
+
const database = await source.openDatabase();
|
|
127
|
+
try {
|
|
128
|
+
const options = { database, prefix: source.prefix ?? "" };
|
|
129
|
+
const leases = await openSqlBrokerLeaseStore(options);
|
|
130
|
+
const outbox = await openSqlOutboxStore(options);
|
|
131
|
+
const heldSends = await openSqlHeldSendStore(options);
|
|
132
|
+
const reservations = await openSqlRestoringReservationStore(options);
|
|
133
|
+
const pendingAsks = await openSqlPendingAskStore(options);
|
|
134
|
+
const sentMessages = await openSqlSentMessageAuthority(options);
|
|
135
|
+
const knownSessions = await openSqlKnownSessionStore(options);
|
|
136
|
+
const bus = await openDatabaseWakeUpBus({
|
|
137
|
+
openDatabase: async () => borrowedDatabase(database),
|
|
138
|
+
prefix: source.prefix,
|
|
139
|
+
...busOptions,
|
|
140
|
+
});
|
|
141
|
+
let closing: Promise<void> | undefined;
|
|
142
|
+
const state: IntercomBrokerStateStore = {
|
|
143
|
+
leases,
|
|
144
|
+
outbox,
|
|
145
|
+
heldSends,
|
|
146
|
+
reservations,
|
|
147
|
+
pendingAsks,
|
|
148
|
+
sentMessages,
|
|
149
|
+
knownSessions,
|
|
150
|
+
close() {
|
|
151
|
+
closing ??= database.close();
|
|
152
|
+
return closing;
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
return { state, bus };
|
|
156
|
+
} catch (cause) {
|
|
157
|
+
await database.close().catch(() => {});
|
|
158
|
+
throw cause;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { homedir } from "os";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The shared storage selection (`ATOMIC_STORAGE` JSON) the launching Atomic process hands its broker: its
|
|
6
|
+
* `--storage`/`--storage-file` or its own `ATOMIC_STORAGE`. It outranks the agent directory's settings.
|
|
7
|
+
*/
|
|
8
|
+
export const BROKER_STORAGE_ENV = "ATOMIC_STORAGE";
|
|
9
|
+
|
|
4
10
|
function getHomeDir(): string {
|
|
5
11
|
if (process.platform === "win32") {
|
|
6
12
|
if (process.env.USERPROFILE) return process.env.USERPROFILE;
|
|
@@ -122,6 +122,20 @@ export class PendingSendRegistry {
|
|
|
122
122
|
return true;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Wait up to `timeoutMs` more for this attempt's outcome: the broker is holding it for a recipient that is
|
|
127
|
+
* coming back (#3760). A pre-attemptId acknowledgement may extend only an ID's first active generation.
|
|
128
|
+
*/
|
|
129
|
+
extend(messageId: string, attemptId: string | undefined, timeoutMs: number): boolean {
|
|
130
|
+
const attempt = this.attempts.get(messageId);
|
|
131
|
+
if (!attempt || (attemptId === undefined ? !attempt.legacyEligible : attempt.attemptId !== attemptId)) return false;
|
|
132
|
+
if (attempt.timer) clearTimeout(attempt.timer);
|
|
133
|
+
attempt.timer = setTimeout(() => {
|
|
134
|
+
this.reject(attempt, new Error("Send timeout"));
|
|
135
|
+
}, timeoutMs);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
125
139
|
/** Resolve a pre-attemptId response only for an ID's first active generation. */
|
|
126
140
|
resolveLegacy(messageId: string, result: SendResultLike): boolean {
|
|
127
141
|
const attempt = this.attempts.get(messageId);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type net from "node:net";
|
|
2
|
+
import { DELIVERED_MESSAGE_TTL_MS, RECIPIENT_RECEIPT_TIMEOUT_MS } from "../retry-policy.js";
|
|
3
|
+
import type { BrokerMessage } from "../types.js";
|
|
4
|
+
|
|
5
|
+
/** #3761: the sender is told this when the recipient did not confirm a save within the sender's wait. */
|
|
6
|
+
export const UNCERTAIN_RECEIPT_REASON =
|
|
7
|
+
"The recipient did not confirm that it saved the message in time, so whether it arrived is uncertain: it may still save and show it. Check with the recipient before sending it again, because a new send is a new message.";
|
|
8
|
+
|
|
9
|
+
export interface ReceiptSender {
|
|
10
|
+
readonly socket: net.Socket;
|
|
11
|
+
readonly attemptId?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AwaitedReceipt<Recipient> {
|
|
15
|
+
readonly messageId: string;
|
|
16
|
+
/** Stable recipient authority, so a reconnected recipient can confirm or receive the message again. */
|
|
17
|
+
readonly targetIdentity: string;
|
|
18
|
+
/** The frame written to the recipient, written again when it registers again before confirming. */
|
|
19
|
+
readonly frame: BrokerMessage;
|
|
20
|
+
/** Runs once, on the recipient's confirmation. `sender` is absent once the sender's wait has ended. */
|
|
21
|
+
readonly complete: (recipient: Recipient, sender: ReceiptSender | undefined) => void;
|
|
22
|
+
/** Runs when the sender's wait ends before the recipient confirms. */
|
|
23
|
+
readonly expire: (sender: ReceiptSender) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface PendingReceipt<Recipient> extends AwaitedReceipt<Recipient> {
|
|
27
|
+
sender: ReceiptSender | undefined;
|
|
28
|
+
senderTimer: ReturnType<typeof setTimeout> | undefined;
|
|
29
|
+
retentionTimer: ReturnType<typeof setTimeout>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* #3761: messages written to a recipient that confirms saves, held until that recipient says it saved them.
|
|
34
|
+
* A message stays held after the sender's wait ends, so a late confirmation still settles it and a
|
|
35
|
+
* re-registered recipient is sent it again; it is forgotten after the delivered-message retention.
|
|
36
|
+
*/
|
|
37
|
+
export class RecipientReceipts<Recipient> {
|
|
38
|
+
private readonly pending = new Map<string, PendingReceipt<Recipient>>();
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
private readonly senderTimeoutMs = RECIPIENT_RECEIPT_TIMEOUT_MS,
|
|
42
|
+
private readonly retentionMs = DELIVERED_MESSAGE_TTL_MS,
|
|
43
|
+
) {}
|
|
44
|
+
|
|
45
|
+
await(receipt: AwaitedReceipt<Recipient>, sender: ReceiptSender): void {
|
|
46
|
+
this.drop(receipt.messageId);
|
|
47
|
+
const entry: PendingReceipt<Recipient> = {
|
|
48
|
+
...receipt,
|
|
49
|
+
sender,
|
|
50
|
+
senderTimer: undefined,
|
|
51
|
+
retentionTimer: setTimeout(() => {
|
|
52
|
+
if (this.pending.get(receipt.messageId) === entry) this.drop(receipt.messageId);
|
|
53
|
+
}, this.retentionMs),
|
|
54
|
+
};
|
|
55
|
+
entry.retentionTimer.unref?.();
|
|
56
|
+
entry.senderTimer = setTimeout(() => {
|
|
57
|
+
entry.senderTimer = undefined;
|
|
58
|
+
const waiting = entry.sender;
|
|
59
|
+
entry.sender = undefined;
|
|
60
|
+
if (waiting !== undefined && this.pending.get(receipt.messageId) === entry) entry.expire(waiting);
|
|
61
|
+
}, this.senderTimeoutMs);
|
|
62
|
+
this.pending.set(receipt.messageId, entry);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Settle a held message when the confirming recipient is the one it was written to. */
|
|
66
|
+
settle(messageId: string, targetIdentity: string, recipient: Recipient): boolean {
|
|
67
|
+
const entry = this.pending.get(messageId);
|
|
68
|
+
if (entry === undefined || entry.targetIdentity !== targetIdentity) return false;
|
|
69
|
+
const sender = entry.sender;
|
|
70
|
+
this.drop(messageId);
|
|
71
|
+
entry.complete(recipient, sender);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Frames still waiting for this recipient, in the order they were first written. */
|
|
76
|
+
framesFor(targetIdentity: string): BrokerMessage[] {
|
|
77
|
+
return [...this.pending.values()]
|
|
78
|
+
.filter((entry) => entry.targetIdentity === targetIdentity)
|
|
79
|
+
.map((entry) => entry.frame);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
drop(messageId: string): void {
|
|
83
|
+
const entry = this.pending.get(messageId);
|
|
84
|
+
if (entry === undefined) return;
|
|
85
|
+
this.pending.delete(messageId);
|
|
86
|
+
if (entry.senderTimer !== undefined) clearTimeout(entry.senderTimer);
|
|
87
|
+
clearTimeout(entry.retentionTimer);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
clear(): void {
|
|
91
|
+
for (const messageId of [...this.pending.keys()]) this.drop(messageId);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import type net from "node:net";
|
|
2
|
+
import { hasGroup } from "../group.js";
|
|
3
|
+
import type { SessionInfo } from "../types.js";
|
|
4
|
+
import { sessionGroups } from "./group-membership.js";
|
|
5
|
+
|
|
6
|
+
/** How long the broker holds a send for a subagent that is being restored; matches its owner's own wait (#3760). */
|
|
7
|
+
export const RESTORING_RECIPIENT_HOLD_MS = 20_000;
|
|
8
|
+
/** A reservation covers the child's name plus its Atomic session id and its pre-restart Intercom id. */
|
|
9
|
+
const MAX_RESERVED_TARGETS = 8;
|
|
10
|
+
const MAX_HELD_SENDS = 256;
|
|
11
|
+
|
|
12
|
+
/** A send the broker is holding for a subagent that is being restored. */
|
|
13
|
+
export interface HeldSend {
|
|
14
|
+
readonly socket: net.Socket;
|
|
15
|
+
readonly senderId: string;
|
|
16
|
+
readonly messageId: string;
|
|
17
|
+
readonly attemptId?: string;
|
|
18
|
+
/** The original send frame, dispatched again once the hold ends. */
|
|
19
|
+
readonly clientMessage: { readonly type: string; readonly to: string } & Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type RestoringRecipientAdmission =
|
|
23
|
+
| { readonly kind: "none" }
|
|
24
|
+
| { readonly kind: "held"; readonly timeoutMs: number }
|
|
25
|
+
| { readonly kind: "refused"; readonly reason: string };
|
|
26
|
+
|
|
27
|
+
/** One reservation as `RestoringRecipients.active` reports it; `gone` is the formatted gone refusal. */
|
|
28
|
+
export interface RestoringReservationSnapshot {
|
|
29
|
+
readonly capability: string;
|
|
30
|
+
readonly ownerSessionId: string;
|
|
31
|
+
readonly childName: string;
|
|
32
|
+
readonly group: string;
|
|
33
|
+
readonly aliases: readonly string[];
|
|
34
|
+
readonly dormant: boolean;
|
|
35
|
+
readonly gone?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface Reservation {
|
|
39
|
+
readonly ownerSessionId: string;
|
|
40
|
+
readonly childName: string;
|
|
41
|
+
group: string;
|
|
42
|
+
/**
|
|
43
|
+
* Made by a session that is only keeping the child addressable until the session that will really restore it
|
|
44
|
+
* is back, such as the launching session of an interrupted workflow run. The restoring supervisor of the
|
|
45
|
+
* same child and group takes it over with its held sends.
|
|
46
|
+
*/
|
|
47
|
+
dormant: boolean;
|
|
48
|
+
readonly targets: Set<string>;
|
|
49
|
+
/** Set once the owner reports the child gone; held and later senders get this refusal. */
|
|
50
|
+
gone?: string;
|
|
51
|
+
readonly held: Map<HeldSend, ReturnType<typeof setTimeout>>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const normalizeTarget = (target: string): string => target.trim().toLowerCase();
|
|
55
|
+
|
|
56
|
+
export function restoringRecipientRetryReason(childName: string): string {
|
|
57
|
+
return `Subagent \`${childName}\` is still being restored after the restart. The message was not sent; try again shortly.`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function goneRecipientReason(childName: string, reason: string): string {
|
|
61
|
+
return `Subagent \`${childName}\` did not survive the restart and is gone${reason.trim() ? `: ${reason.trim()}` : ""}. The message was not sent; launch a fresh subagent if its work is still needed.`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Broker-side holds for background subagents that their owner is restoring after a restart (#3760).
|
|
66
|
+
*
|
|
67
|
+
* The owner reserves the child's targets (its name, Atomic session id and pre-restart Intercom id) under the
|
|
68
|
+
* supervisor capability it was issued for that child; only that capability names the child, and the extra
|
|
69
|
+
* targets are ids, never names. While a send by any sender in the child's group finds no
|
|
70
|
+
* live session, it is held instead of failing with `Session not found`. A hold ends in exactly one way: the
|
|
71
|
+
* child registers with that capability and every held send is dispatched again, once and in order; the owner
|
|
72
|
+
* reports the child gone and held and later senders get the gone refusal; or the hold times out and the sender
|
|
73
|
+
* is told to try again. Reservations end when their owner disconnects.
|
|
74
|
+
*
|
|
75
|
+
* A reservation can start before the restore does: a resumed session reserves its recorded children at session
|
|
76
|
+
* start, and the launching session of an interrupted workflow run reserves the children of the run's stages while
|
|
77
|
+
* the run waits for `/workflow resume`. The restoring supervisor's own reservation of the same child then takes
|
|
78
|
+
* the earlier one over, so a send is held from the first moment until the child is back, without a gap.
|
|
79
|
+
*/
|
|
80
|
+
export class RestoringRecipients {
|
|
81
|
+
private readonly reservations = new Map<string, Reservation>();
|
|
82
|
+
|
|
83
|
+
constructor(private readonly onExpired: (held: HeldSend, reason: string) => void) {}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Reserve, or add targets to, the reservation for the child this capability authorizes. `group` is normalized.
|
|
87
|
+
* A `dormant` reservation may be taken over by the supervisor that restores the child.
|
|
88
|
+
*/
|
|
89
|
+
reserve(
|
|
90
|
+
capability: string,
|
|
91
|
+
ownerSessionId: string,
|
|
92
|
+
childName: string,
|
|
93
|
+
group: string,
|
|
94
|
+
aliases: readonly string[],
|
|
95
|
+
options: { readonly dormant?: boolean } = {},
|
|
96
|
+
): void {
|
|
97
|
+
const dormant = options.dormant === true;
|
|
98
|
+
let reservation = this.reservations.get(capability);
|
|
99
|
+
if (reservation === undefined) {
|
|
100
|
+
reservation = {
|
|
101
|
+
ownerSessionId,
|
|
102
|
+
childName,
|
|
103
|
+
group,
|
|
104
|
+
dormant,
|
|
105
|
+
targets: new Set([normalizeTarget(childName)]),
|
|
106
|
+
held: new Map(),
|
|
107
|
+
};
|
|
108
|
+
// A newer restoration of the same child replaces its owner's earlier reservation, gone marker included,
|
|
109
|
+
// and a dormant owner's reservation of that child and group, whoever restores it.
|
|
110
|
+
for (const [other, previous] of this.reservations) {
|
|
111
|
+
if (normalizeTarget(previous.childName) !== normalizeTarget(childName)) continue;
|
|
112
|
+
if (previous.ownerSessionId !== ownerSessionId && !(previous.dormant && previous.group === group)) continue;
|
|
113
|
+
this.reservations.delete(other);
|
|
114
|
+
for (const target of previous.targets) reservation.targets.add(target);
|
|
115
|
+
for (const [held, timer] of previous.held) reservation.held.set(held, timer);
|
|
116
|
+
}
|
|
117
|
+
this.reservations.set(capability, reservation);
|
|
118
|
+
}
|
|
119
|
+
reservation.group = group;
|
|
120
|
+
reservation.dormant = dormant;
|
|
121
|
+
for (const alias of aliases) {
|
|
122
|
+
const target = normalizeTarget(alias);
|
|
123
|
+
if (target && reservation.targets.size < MAX_RESERVED_TARGETS) reservation.targets.add(target);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Decide a send that found no live recipient. `canHold` is false for clients that cannot wait past their
|
|
129
|
+
* ordinary acknowledgement timeout; they are told to try again instead of being held.
|
|
130
|
+
*/
|
|
131
|
+
admit(
|
|
132
|
+
target: string,
|
|
133
|
+
sender: { readonly id: string; readonly info: SessionInfo },
|
|
134
|
+
held: HeldSend,
|
|
135
|
+
canHold: boolean,
|
|
136
|
+
): RestoringRecipientAdmission {
|
|
137
|
+
const normalized = normalizeTarget(target);
|
|
138
|
+
const reservation = [...this.reservations.values()].find(
|
|
139
|
+
(candidate) =>
|
|
140
|
+
candidate.targets.has(normalized) &&
|
|
141
|
+
(candidate.ownerSessionId === sender.id || hasGroup(sessionGroups(sender.info), candidate.group)),
|
|
142
|
+
);
|
|
143
|
+
if (reservation === undefined) return { kind: "none" };
|
|
144
|
+
if (reservation.gone !== undefined) return { kind: "refused", reason: reservation.gone };
|
|
145
|
+
if (!canHold || reservation.held.size >= MAX_HELD_SENDS)
|
|
146
|
+
return { kind: "refused", reason: restoringRecipientRetryReason(reservation.childName) };
|
|
147
|
+
const timer = setTimeout(() => {
|
|
148
|
+
for (const candidate of this.reservations.values()) {
|
|
149
|
+
if (!candidate.held.delete(held)) continue;
|
|
150
|
+
this.onExpired(held, restoringRecipientRetryReason(candidate.childName));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}, RESTORING_RECIPIENT_HOLD_MS);
|
|
154
|
+
timer.unref?.();
|
|
155
|
+
reservation.held.set(held, timer);
|
|
156
|
+
return { kind: "held", timeoutMs: RESTORING_RECIPIENT_HOLD_MS };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The child registered under this capability: end its reservation and return the sends held for it, in the
|
|
161
|
+
* order they arrived. Each is routed again as it was addressed, so it reaches the child only through a name or
|
|
162
|
+
* id the child really registered with, never merely because the owner listed it as an alias.
|
|
163
|
+
*
|
|
164
|
+
* `registered` names the child that registered. A dormant reservation of that child and group that no
|
|
165
|
+
* restoring supervisor took over (it restored the child without reserving first) ends with it too.
|
|
166
|
+
*/
|
|
167
|
+
consume(
|
|
168
|
+
capability: string,
|
|
169
|
+
registered?: { readonly childName: string; readonly groups: ReadonlySet<string> },
|
|
170
|
+
): HeldSend[] {
|
|
171
|
+
const held: HeldSend[] = [];
|
|
172
|
+
const reservation = this.reservations.get(capability);
|
|
173
|
+
if (reservation !== undefined && reservation.gone === undefined) {
|
|
174
|
+
this.reservations.delete(capability);
|
|
175
|
+
held.push(...this.take(reservation));
|
|
176
|
+
}
|
|
177
|
+
if (registered === undefined) return held;
|
|
178
|
+
const name = normalizeTarget(registered.childName);
|
|
179
|
+
for (const [other, orphan] of this.reservations) {
|
|
180
|
+
if (!orphan.dormant || orphan.gone !== undefined || normalizeTarget(orphan.childName) !== name) continue;
|
|
181
|
+
if (!hasGroup(registered.groups, orphan.group)) continue;
|
|
182
|
+
this.reservations.delete(other);
|
|
183
|
+
held.push(...this.take(orphan));
|
|
184
|
+
}
|
|
185
|
+
return held;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The owner ended its reservation. With `goneReason` the reservation stays as a gone marker and the held
|
|
190
|
+
* sends are returned with the gone refusal. Without it the reservation ends: the held sends are returned with
|
|
191
|
+
* the owner's `refusal` when it gave one (the child is coming back elsewhere), otherwise for ordinary routing.
|
|
192
|
+
* Undefined when this owner holds no such reservation.
|
|
193
|
+
*/
|
|
194
|
+
release(
|
|
195
|
+
capability: string,
|
|
196
|
+
ownerSessionId: string,
|
|
197
|
+
goneReason: string | undefined,
|
|
198
|
+
refusal?: string,
|
|
199
|
+
): { readonly held: HeldSend[]; readonly gone?: string; readonly refusal?: string } | undefined {
|
|
200
|
+
const reservation = this.reservations.get(capability);
|
|
201
|
+
if (reservation === undefined || reservation.ownerSessionId !== ownerSessionId) return undefined;
|
|
202
|
+
const held = this.take(reservation);
|
|
203
|
+
if (goneReason === undefined) {
|
|
204
|
+
this.reservations.delete(capability);
|
|
205
|
+
return refusal === undefined ? { held } : { held, refusal };
|
|
206
|
+
}
|
|
207
|
+
reservation.gone = goneRecipientReason(reservation.childName, goneReason);
|
|
208
|
+
return { held, gone: reservation.gone };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The owner disconnected: end its reservations and return their held sends for ordinary routing. */
|
|
212
|
+
dropOwner(ownerSessionId: string): HeldSend[] {
|
|
213
|
+
const released: HeldSend[] = [];
|
|
214
|
+
for (const [capability, reservation] of this.reservations) {
|
|
215
|
+
if (reservation.ownerSessionId !== ownerSessionId) continue;
|
|
216
|
+
this.reservations.delete(capability);
|
|
217
|
+
released.push(...this.take(reservation));
|
|
218
|
+
}
|
|
219
|
+
return released;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** A sender disconnected: nobody is waiting for its held sends any more. */
|
|
223
|
+
dropSender(senderId: string): void {
|
|
224
|
+
for (const reservation of this.reservations.values()) {
|
|
225
|
+
for (const [held, timer] of reservation.held) {
|
|
226
|
+
if (held.senderId !== senderId) continue;
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
reservation.held.delete(held);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The reservations in force now, as their owners made them: the child's name, its other targets, and the gone
|
|
235
|
+
* marker its owner left. Multi-node routing that starts after they were acknowledged mirrors them from here.
|
|
236
|
+
*/
|
|
237
|
+
active(): RestoringReservationSnapshot[] {
|
|
238
|
+
return [...this.reservations].map(([capability, reservation]) => {
|
|
239
|
+
const childTarget = normalizeTarget(reservation.childName);
|
|
240
|
+
return {
|
|
241
|
+
capability,
|
|
242
|
+
ownerSessionId: reservation.ownerSessionId,
|
|
243
|
+
childName: reservation.childName,
|
|
244
|
+
group: reservation.group,
|
|
245
|
+
aliases: [...reservation.targets].filter((target) => target !== childTarget),
|
|
246
|
+
dormant: reservation.dormant,
|
|
247
|
+
...(reservation.gone === undefined ? {} : { gone: reservation.gone }),
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
clear(): void {
|
|
253
|
+
for (const reservation of this.reservations.values()) this.take(reservation);
|
|
254
|
+
this.reservations.clear();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private take(reservation: Reservation): HeldSend[] {
|
|
258
|
+
const held = [...reservation.held.keys()];
|
|
259
|
+
for (const timer of reservation.held.values()) clearTimeout(timer);
|
|
260
|
+
reservation.held.clear();
|
|
261
|
+
return held;
|
|
262
|
+
}
|
|
263
|
+
}
|