@masons/runtime-broker 0.2.6 → 0.2.8

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.
@@ -4,6 +4,7 @@ import type { ClosedEndpointLookup } from "./closed-endpoint-lookup.js";
4
4
  import type { ConnectorWS } from "./connector-ws.js";
5
5
  import { type ControlEventDispatcher } from "./control-event-dispatcher.js";
6
6
  import type { ControlAck } from "./control-event-types.js";
7
+ import { type DiscoveryRecord } from "./discovery-file.js";
7
8
  import { type EndpointState } from "./endpoint-state-machine.js";
8
9
  import type { BrokerLogger } from "./logger.js";
9
10
  import { type NetworkPresence } from "./network-presence.js";
@@ -74,6 +75,10 @@ export interface RunningBroker {
74
75
  networkPresenceChangedQueueSize(): number;
75
76
  runtimeInboundRoutedQueueSize(): number;
76
77
  }
78
+ export declare class BrokerAlreadyRunningError extends Error {
79
+ readonly discovery: DiscoveryRecord;
80
+ constructor(discovery: DiscoveryRecord);
81
+ }
77
82
  export declare function startBrokerDaemon(opts: BrokerDaemonOptions): Promise<RunningBroker>;
78
83
  export declare function readPluginPidHeader(req: IncomingMessage): number | null;
79
84
  export type { BufferedMessage };
@@ -1 +1 @@
1
- {"version":3,"file":"broker-daemon.d.ts","sourceRoot":"","sources":["../../src/broker/broker-daemon.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"broker-daemon.d.ts","sourceRoot":"","sources":["../../src/broker/broker-daemon.ts"],"names":[],"mappings":"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"}
@@ -4,8 +4,8 @@ import { basename } from "node:path";
4
4
  import { ConnectorClientUnavailableError, ConnectorSendAckTimeoutError, } from "../connector-client.js";
5
5
  import { createControlEventDispatcher, } from "./control-event-dispatcher.js";
6
6
  import { readDeliveryCursorFile, writeDeliveryCursorFile, } from "./delivery-cursor-file.js";
7
- import { deleteDiscoveryFile, mintBearerToken, writeDiscoveryFile, } from "./discovery-file.js";
8
- import { EndpointRegistry } from "./endpoint-registry.js";
7
+ import { deleteDiscoveryFileIfOwned, mintBearerToken, readDiscoveryFile, writeDiscoveryFile, } from "./discovery-file.js";
8
+ import { EndpointRegistry, } from "./endpoint-registry.js";
9
9
  import { DEFAULT_GRACE_MS, transition, } from "./endpoint-state-machine.js";
10
10
  import { createGraceTimerManager } from "./grace-timer.js";
11
11
  import { BrokerHttpError, pushToPlugin, startIPCServer, } from "./ipc-server.js";
@@ -59,6 +59,41 @@ function derivePublicDisplayLabel(body) {
59
59
  ? parts.join(" · ")
60
60
  : `${body.kind}-${String(body.plugin_pid).slice(-4)}`;
61
61
  }
62
+ export class BrokerAlreadyRunningError extends Error {
63
+ discovery;
64
+ constructor(discovery) {
65
+ super(`another healthy runtime broker already owns discovery at ${discovery.ipcUrl}`);
66
+ this.name = "BrokerAlreadyRunningError";
67
+ this.discovery = discovery;
68
+ }
69
+ }
70
+ async function findHealthyDiscoveryOwner(discoveryFile) {
71
+ const existing = readDiscoveryFile(discoveryFile);
72
+ if (!existing)
73
+ return null;
74
+ if (existing.pid === process.pid)
75
+ return null;
76
+ if (!(await probeDiscoveryHealth(existing)))
77
+ return null;
78
+ return existing;
79
+ }
80
+ async function probeDiscoveryHealth(record) {
81
+ const ctrl = new AbortController();
82
+ const timer = setTimeout(() => ctrl.abort(), 2_000);
83
+ try {
84
+ const res = await fetch(`${record.ipcUrl}/health`, {
85
+ headers: { Authorization: `Bearer ${record.bearerToken}` },
86
+ signal: ctrl.signal,
87
+ });
88
+ return res.ok;
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ finally {
94
+ clearTimeout(timer);
95
+ }
96
+ }
62
97
  export async function startBrokerDaemon(opts) {
63
98
  const { paths, asNodeId: _asNodeIdReservedForA3, connector, apiPort, logger, closedEndpointLookup, livenessProbe = defaultIsPluginAlive, spawnDriverRegistry, spawnFn = spawnChild, servicesEventClientFactory, } = opts;
64
99
  const graceMs = opts.graceMs ?? DEFAULT_GRACE_MS;
@@ -69,6 +104,14 @@ export async function startBrokerDaemon(opts) {
69
104
  (async () => {
70
105
  });
71
106
  void _asNodeIdReservedForA3;
107
+ const existingOwner = await findHealthyDiscoveryOwner(paths.discoveryFile);
108
+ if (existingOwner) {
109
+ logger.info("broker_already_running", {
110
+ pid: existingOwner.pid,
111
+ ipcUrl: existingOwner.ipcUrl,
112
+ });
113
+ throw new BrokerAlreadyRunningError(existingOwner);
114
+ }
72
115
  const persistedDeliveryCursor = readDeliveryCursorFile(paths.deliveryCursorFile) ?? 0;
73
116
  connector.setDeliveryCursor(persistedDeliveryCursor);
74
117
  logger.info("delivery_cursor_loaded", {
@@ -170,6 +213,9 @@ export async function startBrokerDaemon(opts) {
170
213
  const endpointCorrelationKey = (endpointId, correlationId) => `${endpointId}\u0000${correlationId}`;
171
214
  const isBackgroundExchangeTarget = (entry) => Boolean(entry.runtime_capabilities?.includes("background_exchange_v1") &&
172
215
  isBackgroundExchangeMode(entry.background_exchange_mode));
216
+ const requiresForegroundChannel = (entry) => entry.execution_surface !== "headless";
217
+ const canAcceptRuntimeMailboxDispatch = (entry) => !requiresForegroundChannel(entry) ||
218
+ Boolean(entry.runtime_capabilities?.includes("foreground_channel_v1"));
173
219
  const rememberRuntimeAssignmentCorrelation = (endpointId, correlationId, assignmentKey) => {
174
220
  if (typeof correlationId !== "string" || correlationId.length === 0) {
175
221
  return;
@@ -772,6 +818,13 @@ export async function startBrokerDaemon(opts) {
772
818
  detail: `target endpoint not found: ${targetEndpointId}`,
773
819
  };
774
820
  }
821
+ if (!canAcceptRuntimeMailboxDispatch(target)) {
822
+ undispatchedInbox.tryAdd(taken);
823
+ return {
824
+ ok: false,
825
+ detail: `target endpoint is not foreground-channel capable: ${targetEndpointId}`,
826
+ };
827
+ }
775
828
  let targetRef = event.target_ref;
776
829
  if (!targetRef) {
777
830
  if (target.background_exchange_mode === "resident_endpoint") {
@@ -1559,6 +1612,24 @@ export async function startBrokerDaemon(opts) {
1559
1612
  }
1560
1613
  },
1561
1614
  });
1615
+ const ownerAfterBind = await findHealthyDiscoveryOwner(paths.discoveryFile);
1616
+ if (ownerAfterBind) {
1617
+ logger.info("broker_already_running_after_ipc_bind", {
1618
+ pid: ownerAfterBind.pid,
1619
+ ipcUrl: ownerAfterBind.ipcUrl,
1620
+ });
1621
+ try {
1622
+ await ipc.close();
1623
+ }
1624
+ catch {
1625
+ }
1626
+ try {
1627
+ connector.disconnect();
1628
+ }
1629
+ catch {
1630
+ }
1631
+ throw new BrokerAlreadyRunningError(ownerAfterBind);
1632
+ }
1562
1633
  const inboundProcessing = new Set();
1563
1634
  const blockedInboundSeqs = new Set();
1564
1635
  const asyncAcceptedInboundSeqs = new Set();
@@ -1847,13 +1918,14 @@ export async function startBrokerDaemon(opts) {
1847
1918
  deferredDeliveryPendingUpTo = undefined;
1848
1919
  connector.ackDelivery(payload.upTo);
1849
1920
  });
1850
- writeDiscoveryFile(paths.discoveryFile, {
1921
+ const ownDiscovery = {
1851
1922
  version: 1,
1852
1923
  pid: process.pid,
1853
1924
  startedAt: new Date().toISOString(),
1854
1925
  ipcUrl: ipc.ipcUrl,
1855
1926
  bearerToken,
1856
- });
1927
+ };
1928
+ writeDiscoveryFile(paths.discoveryFile, ownDiscovery);
1857
1929
  logger.info("discovery_written", { path: paths.discoveryFile });
1858
1930
  let servicesEventClient;
1859
1931
  if (servicesEventClientFactory) {
@@ -1927,7 +1999,13 @@ export async function startBrokerDaemon(opts) {
1927
1999
  catch {
1928
2000
  }
1929
2001
  }));
1930
- deleteDiscoveryFile(paths.discoveryFile);
2002
+ const discoveryDeleted = deleteDiscoveryFileIfOwned(paths.discoveryFile, ownDiscovery);
2003
+ if (!discoveryDeleted) {
2004
+ logger.warn("discovery_delete_skipped_not_owner", {
2005
+ path: paths.discoveryFile,
2006
+ pid: process.pid,
2007
+ });
2008
+ }
1931
2009
  try {
1932
2010
  await ipc.close();
1933
2011
  }
@@ -5,8 +5,11 @@ export interface DiscoveryRecord {
5
5
  ipcUrl: string;
6
6
  bearerToken: string;
7
7
  }
8
+ export type DiscoveryOwner = Pick<DiscoveryRecord, "pid" | "ipcUrl" | "bearerToken">;
8
9
  export declare function mintBearerToken(): string;
9
10
  export declare function writeDiscoveryFile(path: string, record: DiscoveryRecord): void;
10
11
  export declare function readDiscoveryFile(path: string): DiscoveryRecord | null;
11
12
  export declare function deleteDiscoveryFile(path: string): void;
13
+ export declare function deleteDiscoveryFileIfOwned(path: string, owner: DiscoveryOwner): boolean;
14
+ export declare function isDiscoveryOwner(record: DiscoveryRecord, owner: DiscoveryOwner): boolean;
12
15
  //# sourceMappingURL=discovery-file.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"discovery-file.d.ts","sourceRoot":"","sources":["../../src/broker/discovery-file.ts"],"names":[],"mappings":"AA6BA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,CAAC,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AAGD,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAMD,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,eAAe,GACtB,IAAI,CA4BN;AAMD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAetE;AAGD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAOtD"}
1
+ {"version":3,"file":"discovery-file.d.ts","sourceRoot":"","sources":["../../src/broker/discovery-file.ts"],"names":[],"mappings":"AA6BA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,CAAC,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,cAAc,GAAG,IAAI,CAC/B,eAAe,EACf,KAAK,GAAG,QAAQ,GAAG,aAAa,CACjC,CAAC;AAGF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAMD,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,eAAe,GACtB,IAAI,CA4BN;AAMD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAetE;AAGD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAOtD;AAOD,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,cAAc,GACpB,OAAO,CAMT;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,cAAc,GACpB,OAAO,CAMT"}
@@ -59,6 +59,20 @@ export function deleteDiscoveryFile(path) {
59
59
  throw err;
60
60
  }
61
61
  }
62
+ export function deleteDiscoveryFileIfOwned(path, owner) {
63
+ const current = readDiscoveryFile(path);
64
+ if (!current)
65
+ return false;
66
+ if (!isDiscoveryOwner(current, owner))
67
+ return false;
68
+ deleteDiscoveryFile(path);
69
+ return true;
70
+ }
71
+ export function isDiscoveryOwner(record, owner) {
72
+ return (record.pid === owner.pid &&
73
+ record.ipcUrl === owner.ipcUrl &&
74
+ record.bearerToken === owner.bearerToken);
75
+ }
62
76
  function isDiscoveryRecord(value) {
63
77
  if (typeof value !== "object" || value === null)
64
78
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../../src/broker/entry.ts"],"names":[],"mappings":"AAmFA,OAAO,EAAE,KAAK,YAAY,EAAsB,MAAM,aAAa,CAAC;AAEpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAMtE,UAAU,mBAAmB;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,gBAAgB;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAUD,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAAC,mBAAmB,CAAC,CA8F9B;AA2DD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,GAAE,OAAO,UAAU,CAAC,KAAwB,GACpD,OAAO,CAAC,gBAAgB,CAAC,CA4B3B;AAaD,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,YAAY,GACnB,mBAAmB,CAqIrB;AAYD,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAgG1C"}
1
+ {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../../src/broker/entry.ts"],"names":[],"mappings":"AAsFA,OAAO,EAAE,KAAK,YAAY,EAAsB,MAAM,aAAa,CAAC;AAEpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAMtE,UAAU,mBAAmB;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,gBAAgB;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAUD,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAAC,mBAAmB,CAAC,CA8F9B;AA2DD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,GAAE,OAAO,UAAU,CAAC,KAAwB,GACpD,OAAO,CAAC,gBAAgB,CAAC,CA4B3B;AAaD,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,YAAY,GACnB,mBAAmB,CAqIrB;AAYD,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAgG1C"}
@@ -3,7 +3,7 @@ import { pathToFileURL } from "node:url";
3
3
  import { readConfig } from "../config-fs.js";
4
4
  import { DEFAULT_API_HOST } from "../platform-client.js";
5
5
  import { claimRuntimeInboundRoute, emitRuntimeInboundRouted, emitRuntimeNetworkPresenceChanged, emitRuntimeProcessingState, emitRuntimeUndispatchedChanged, heartbeatRuntimeEndpoint, registerRuntimeEndpoint, transitionRuntimeEndpointState, unregisterRuntimeEndpoint, updateRuntimeEndpointDisplayMetadata, } from "../runtime-endpoint-client.js";
6
- import { startBrokerDaemon } from "./broker-daemon.js";
6
+ import { BrokerAlreadyRunningError, startBrokerDaemon, } from "./broker-daemon.js";
7
7
  import { ClaudeCodeSpawnDriver } from "./claude-code-spawn-driver.js";
8
8
  import { CodexSpawnDriverStub } from "./codex-spawn-driver-stub.js";
9
9
  import { ConnectorWS } from "./connector-ws.js";
@@ -273,6 +273,9 @@ export async function main() {
273
273
  const argvUrl = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
274
274
  if (import.meta.url === argvUrl) {
275
275
  main().catch((err) => {
276
+ if (err instanceof BrokerAlreadyRunningError) {
277
+ process.exit(0);
278
+ }
276
279
  console.error("broker_entry_fatal", err);
277
280
  process.exit(1);
278
281
  });
@@ -1 +1 @@
1
- {"version":3,"file":"lazy-spawn.d.ts","sourceRoot":"","sources":["../../src/broker-client/lazy-spawn.ts"],"names":[],"mappings":"AA0CA,OAAO,EACL,KAAK,eAAe,EAErB,MAAM,6BAA6B,CAAC;AAErC,MAAM,WAAW,gBAAgB;IAE/B,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE7B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAMD,MAAM,WAAW,eAAgB,SAAQ,gBAAgB;IAEvD,aAAa,EAAE,MAAM,CAAC;IAEtB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,eAAe,CAAC;IAExB,OAAO,EAAE,OAAO,CAAC;CAClB;AAMD,wBAAsB,cAAc,CAClC,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,eAAe,CAAC,CAgC1B"}
1
+ {"version":3,"file":"lazy-spawn.d.ts","sourceRoot":"","sources":["../../src/broker-client/lazy-spawn.ts"],"names":[],"mappings":"AA6BA,OAAO,EACL,KAAK,eAAe,EAErB,MAAM,6BAA6B,CAAC;AAErC,MAAM,WAAW,gBAAgB;IAE/B,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE7B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAOD,MAAM,WAAW,eAAgB,SAAQ,gBAAgB;IAEvD,aAAa,EAAE,MAAM,CAAC;IAEtB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,eAAe,CAAC;IAExB,OAAO,EAAE,OAAO,CAAC;CAClB;AAsBD,wBAAsB,cAAc,CAClC,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,eAAe,CAAC,CAsD1B"}
@@ -1,9 +1,12 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { chmodSync, closeSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { dirname } from "node:path";
2
4
  import { fileURLToPath } from "node:url";
3
5
  import { readDiscoveryFile, } from "../broker/discovery-file.js";
4
6
  const DEFAULT_POLL_INTERVAL_MS = 100;
5
7
  const DEFAULT_DISCOVER_TIMEOUT_MS = 5_000;
6
8
  const DEFAULT_HEALTH_TIMEOUT_MS = 2_000;
9
+ const SPAWN_LOCK_VERSION = 1;
7
10
  export async function discoverBroker(opts) {
8
11
  const deadline = Date.now() + (opts.timeoutMs ?? DEFAULT_DISCOVER_TIMEOUT_MS);
9
12
  const poll = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
@@ -14,17 +17,138 @@ export async function discoverBroker(opts) {
14
17
  return { record: existing, spawned: false };
15
18
  }
16
19
  }
17
- spawnDaemon(opts);
18
- while (Date.now() < deadline) {
19
- const rec = readDiscoveryFile(opts.discoveryFile);
20
- if (rec && (await probeHealth(rec.ipcUrl, rec.bearerToken))) {
21
- return { record: rec, spawned: true };
20
+ const acquired = await acquireSpawnLock({
21
+ discoveryFile: opts.discoveryFile,
22
+ deadline,
23
+ pollIntervalMs: poll,
24
+ });
25
+ if (acquired.kind === "existing") {
26
+ return { record: acquired.record, spawned: false };
27
+ }
28
+ const { lock } = acquired;
29
+ try {
30
+ const recovered = readDiscoveryFile(opts.discoveryFile);
31
+ if (recovered &&
32
+ (await probeHealth(recovered.ipcUrl, recovered.bearerToken))) {
33
+ return { record: recovered, spawned: false };
34
+ }
35
+ spawnDaemon(opts);
36
+ while (Date.now() < deadline) {
37
+ const rec = readDiscoveryFile(opts.discoveryFile);
38
+ if (rec && (await probeHealth(rec.ipcUrl, rec.bearerToken))) {
39
+ return { record: rec, spawned: true };
40
+ }
41
+ await sleep(poll);
22
42
  }
23
- await sleep(poll);
43
+ }
44
+ finally {
45
+ lock.release();
24
46
  }
25
47
  throw new Error(`broker did not become healthy within ${opts.timeoutMs ?? DEFAULT_DISCOVER_TIMEOUT_MS}ms; ` +
26
48
  `check ${opts.discoveryFile} and the daemon log`);
27
49
  }
50
+ async function acquireSpawnLock(opts) {
51
+ const lockPath = `${opts.discoveryFile}.lock`;
52
+ mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 });
53
+ while (Date.now() < opts.deadline) {
54
+ try {
55
+ const fd = openSync(lockPath, "wx", 0o600);
56
+ const record = {
57
+ version: SPAWN_LOCK_VERSION,
58
+ pid: process.pid,
59
+ startedAt: new Date().toISOString(),
60
+ };
61
+ writeFileSync(fd, JSON.stringify(record, null, 2), {
62
+ encoding: "utf8",
63
+ });
64
+ try {
65
+ chmodSync(lockPath, 0o600);
66
+ }
67
+ catch {
68
+ }
69
+ return {
70
+ kind: "lock",
71
+ lock: {
72
+ path: lockPath,
73
+ fd,
74
+ release() {
75
+ releaseSpawnLock(lockPath, fd);
76
+ },
77
+ },
78
+ };
79
+ }
80
+ catch (err) {
81
+ if (!isNodeErr(err, "EEXIST"))
82
+ throw err;
83
+ const existing = readDiscoveryFile(opts.discoveryFile);
84
+ if (existing &&
85
+ (await probeHealth(existing.ipcUrl, existing.bearerToken))) {
86
+ return { kind: "existing", record: existing };
87
+ }
88
+ if (isStaleSpawnLock(lockPath)) {
89
+ try {
90
+ unlinkSync(lockPath);
91
+ continue;
92
+ }
93
+ catch (unlinkErr) {
94
+ if (!isNodeErr(unlinkErr, "ENOENT")) {
95
+ throw unlinkErr;
96
+ }
97
+ }
98
+ }
99
+ await sleep(opts.pollIntervalMs);
100
+ }
101
+ }
102
+ throw new Error(`broker spawn lock was not acquired before discover timeout; ` +
103
+ `check ${lockPath}`);
104
+ }
105
+ function releaseSpawnLock(path, fd) {
106
+ try {
107
+ closeSync(fd);
108
+ }
109
+ catch {
110
+ }
111
+ try {
112
+ unlinkSync(path);
113
+ }
114
+ catch {
115
+ }
116
+ }
117
+ function isStaleSpawnLock(path) {
118
+ let parsed;
119
+ try {
120
+ const raw = readFileSync(path, "utf8");
121
+ const value = JSON.parse(raw);
122
+ if (!isSpawnLockRecord(value))
123
+ return false;
124
+ parsed = value;
125
+ }
126
+ catch (err) {
127
+ if (isNodeErr(err, "ENOENT"))
128
+ return false;
129
+ return false;
130
+ }
131
+ return !isProcessAlive(parsed.pid);
132
+ }
133
+ function isSpawnLockRecord(value) {
134
+ if (typeof value !== "object" || value === null)
135
+ return false;
136
+ const v = value;
137
+ return (v.version === SPAWN_LOCK_VERSION &&
138
+ typeof v.pid === "number" &&
139
+ Number.isInteger(v.pid) &&
140
+ v.pid > 0 &&
141
+ typeof v.startedAt === "string");
142
+ }
143
+ function isProcessAlive(pid) {
144
+ try {
145
+ process.kill(pid, 0);
146
+ return true;
147
+ }
148
+ catch (err) {
149
+ return isNodeErr(err, "EPERM");
150
+ }
151
+ }
28
152
  function spawnDaemon(opts) {
29
153
  const entry = opts.brokerEntry ?? resolveDefaultBrokerEntry();
30
154
  const child = spawn(opts.nodeBinary ?? process.execPath, [entry], {
@@ -59,3 +183,9 @@ async function probeHealth(ipcUrl, bearerToken) {
59
183
  function sleep(ms) {
60
184
  return new Promise((resolve) => setTimeout(resolve, ms));
61
185
  }
186
+ function isNodeErr(err, code) {
187
+ return (typeof err === "object" &&
188
+ err !== null &&
189
+ "code" in err &&
190
+ err.code === code);
191
+ }
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const PLUGIN_VERSION = "0.2.6";
1
+ export declare const PLUGIN_VERSION = "0.2.8";
2
2
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const PLUGIN_VERSION = "0.2.6";
1
+ export const PLUGIN_VERSION = "0.2.8";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masons/runtime-broker",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "MASONS Runtime Broker — local daemon and BrokerClient SDK for multi-session agent runtime coordination.",
5
5
  "license": "MIT",
6
6
  "author": "MASONS.ai <hello@masons.ai> (https://masons.ai)",
@@ -1,27 +0,0 @@
1
- import type { UndispatchedMessage } from "./undispatched-inbox.js";
2
- export type DurableBackgroundAssignmentState = "assigned" | "committing" | "committed";
3
- export type DurableBackgroundAssignmentKind = "mailbox_dispatch" | "broker_route";
4
- export interface DurableBackgroundAssignment {
5
- kind: DurableBackgroundAssignmentKind;
6
- state: DurableBackgroundAssignmentState;
7
- assignmentId: string;
8
- sourceMessageId: string;
9
- endpointId: string;
10
- message: UndispatchedMessage;
11
- assignedAt: number;
12
- acceptedAt?: number;
13
- committedAt?: number;
14
- deliverySeq?: number;
15
- routeKind?: "active_endpoint" | "reconnecting_endpoint";
16
- }
17
- export interface BackgroundAssignmentLedger {
18
- get(sourceMessageId: string): DurableBackgroundAssignment | undefined;
19
- upsert(record: DurableBackgroundAssignment): boolean;
20
- delete(sourceMessageId: string): boolean;
21
- list(): DurableBackgroundAssignment[];
22
- }
23
- export interface BackgroundAssignmentLedgerOptions {
24
- persistFile?: string;
25
- }
26
- export declare function createBackgroundAssignmentLedger(opts?: BackgroundAssignmentLedgerOptions): BackgroundAssignmentLedger;
27
- //# sourceMappingURL=background-assignment-ledger.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"background-assignment-ledger.d.ts","sourceRoot":"","sources":["../../src/broker/background-assignment-ledger.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAEnE,MAAM,MAAM,gCAAgC,GACxC,UAAU,GACV,YAAY,GACZ,WAAW,CAAC;AAEhB,MAAM,MAAM,+BAA+B,GACvC,kBAAkB,GAClB,cAAc,CAAC;AAEnB,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,+BAA+B,CAAC;IACtC,KAAK,EAAE,gCAAgC,CAAC;IACxC,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,mBAAmB,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,iBAAiB,GAAG,uBAAuB,CAAC;CACzD;AAED,MAAM,WAAW,0BAA0B;IACzC,GAAG,CAAC,eAAe,EAAE,MAAM,GAAG,2BAA2B,GAAG,SAAS,CAAC;IACtE,MAAM,CAAC,MAAM,EAAE,2BAA2B,GAAG,OAAO,CAAC;IACrD,MAAM,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC;IACzC,IAAI,IAAI,2BAA2B,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,iCAAiC;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,gCAAgC,CAC9C,IAAI,GAAE,iCAAsC,GAC3C,0BAA0B,CAkF5B"}
@@ -1,125 +0,0 @@
1
- import { closeSync, fdatasyncSync, mkdirSync, openSync, readFileSync, renameSync, writeSync, } from "node:fs";
2
- import { dirname } from "node:path";
3
- export function createBackgroundAssignmentLedger(opts = {}) {
4
- const persistFile = opts.persistFile;
5
- const bySourceMessageId = new Map();
6
- const snapshot = () => [
7
- ...bySourceMessageId.values(),
8
- ];
9
- const persist = () => {
10
- if (!persistFile)
11
- return;
12
- mkdirSync(dirname(persistFile), { recursive: true, mode: 0o700 });
13
- const tmp = `${persistFile}.tmp`;
14
- const fd = openSync(tmp, "w", 0o600);
15
- try {
16
- writeSync(fd, `${JSON.stringify({ version: 1, assignments: snapshot() })}\n`);
17
- fdatasyncSync(fd);
18
- }
19
- finally {
20
- closeSync(fd);
21
- }
22
- renameSync(tmp, persistFile);
23
- };
24
- if (persistFile) {
25
- try {
26
- const raw = readFileSync(persistFile, "utf8").trim();
27
- if (raw.length > 0) {
28
- const parsed = JSON.parse(raw);
29
- if (parsed.version !== 1 || !Array.isArray(parsed.assignments)) {
30
- throw new Error("invalid persisted background assignment ledger");
31
- }
32
- for (const assignment of parsed.assignments) {
33
- if (!isDurableBackgroundAssignment(assignment)) {
34
- throw new Error("invalid persisted background assignment ledger record");
35
- }
36
- bySourceMessageId.set(assignment.sourceMessageId, assignment);
37
- }
38
- }
39
- }
40
- catch (err) {
41
- if (!isNodeErr(err, "ENOENT"))
42
- throw err;
43
- }
44
- }
45
- return {
46
- get(sourceMessageId) {
47
- return bySourceMessageId.get(sourceMessageId);
48
- },
49
- upsert(record) {
50
- const prior = bySourceMessageId.get(record.sourceMessageId);
51
- bySourceMessageId.set(record.sourceMessageId, record);
52
- try {
53
- persist();
54
- return true;
55
- }
56
- catch {
57
- if (prior)
58
- bySourceMessageId.set(record.sourceMessageId, prior);
59
- else
60
- bySourceMessageId.delete(record.sourceMessageId);
61
- return false;
62
- }
63
- },
64
- delete(sourceMessageId) {
65
- const prior = bySourceMessageId.get(sourceMessageId);
66
- if (!prior)
67
- return true;
68
- bySourceMessageId.delete(sourceMessageId);
69
- try {
70
- persist();
71
- return true;
72
- }
73
- catch {
74
- bySourceMessageId.set(sourceMessageId, prior);
75
- return false;
76
- }
77
- },
78
- list() {
79
- return snapshot();
80
- },
81
- };
82
- }
83
- function isDurableBackgroundAssignment(value) {
84
- if (!value || typeof value !== "object")
85
- return false;
86
- const record = value;
87
- return ((record.kind === "mailbox_dispatch" || record.kind === "broker_route") &&
88
- (record.state === "assigned" ||
89
- record.state === "committing" ||
90
- record.state === "committed") &&
91
- typeof record.assignmentId === "string" &&
92
- typeof record.sourceMessageId === "string" &&
93
- typeof record.endpointId === "string" &&
94
- typeof record.assignedAt === "number" &&
95
- isUndispatchedMessageShape(record.message) &&
96
- (record.acceptedAt === undefined ||
97
- typeof record.acceptedAt === "number") &&
98
- (record.committedAt === undefined ||
99
- typeof record.committedAt === "number") &&
100
- (record.deliverySeq === undefined ||
101
- typeof record.deliverySeq === "number") &&
102
- (record.routeKind === undefined ||
103
- record.routeKind === "active_endpoint" ||
104
- record.routeKind === "reconnecting_endpoint"));
105
- }
106
- function isUndispatchedMessageShape(value) {
107
- if (!value || typeof value !== "object")
108
- return false;
109
- const record = value;
110
- return (typeof record.id === "string" &&
111
- typeof record.arrived_at === "string" &&
112
- typeof record.sender_address === "string" &&
113
- typeof record.content === "string" &&
114
- typeof record.content_type === "string" &&
115
- !!record.metadata &&
116
- typeof record.metadata === "object" &&
117
- (record.reason === "unaddressed" ||
118
- record.reason === "closed_session_continuation"));
119
- }
120
- function isNodeErr(err, code) {
121
- return (typeof err === "object" &&
122
- err !== null &&
123
- "code" in err &&
124
- err.code === code);
125
- }