@securecoms/agent-bridge 0.8.0 → 0.10.0

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/dist/bin.js CHANGED
@@ -27,6 +27,8 @@ function loadConfig(opts) {
27
27
  let memoryProvider;
28
28
  let memoryBaseDir;
29
29
  let memoryAgent;
30
+ let memoryWorkspaceDir;
31
+ let memoryFactsCharBudget;
30
32
  if (memProvRaw === "none") {
31
33
  memoryProvider = "none";
32
34
  } else if (memProvRaw === "hermes") {
@@ -40,6 +42,10 @@ function loadConfig(opts) {
40
42
  memoryBaseDir = typeof bd === "string" && bd ? bd : "~/.openclaw";
41
43
  const ag = oc?.agent;
42
44
  memoryAgent = typeof ag === "string" && ag ? ag : void 0;
45
+ const wd = oc?.workspace_dir;
46
+ memoryWorkspaceDir = typeof wd === "string" && wd ? wd : void 0;
47
+ const fb = oc?.facts_char_budget;
48
+ memoryFactsCharBudget = typeof fb === "number" && Number.isInteger(fb) && fb >= 0 ? fb : void 0;
43
49
  } else {
44
50
  memoryProvider = "securecoms";
45
51
  }
@@ -56,7 +62,9 @@ function loadConfig(opts) {
56
62
  rateLimitRefillPerMinute,
57
63
  memoryProvider,
58
64
  memoryBaseDir,
59
- memoryAgent
65
+ memoryAgent,
66
+ memoryWorkspaceDir,
67
+ memoryFactsCharBudget
60
68
  };
61
69
  }
62
70
  function arg(argv, flag) {
@@ -1379,6 +1387,18 @@ var LeafRosterMismatchError = class extends Error {
1379
1387
  this.name = "LeafRosterMismatchError";
1380
1388
  }
1381
1389
  };
1390
+ var AmbiguousOwnLeafError = class extends Error {
1391
+ deviceId;
1392
+ totalLeaves;
1393
+ candidateLeaves;
1394
+ constructor(deviceId, totalLeaves, candidateLeaves) {
1395
+ super(`removeDeviceFromLiveGroup: cannot identify device ${deviceId}'s leaf \u2014 the identity owns ${totalLeaves} leaves and ${candidateLeaves} remain after excluding this committer's own. Leaf credentials carry the identity, not the device, so the target is genuinely ambiguous. Refusing rather than evicting a device that was never revoked.`);
1396
+ this.deviceId = deviceId;
1397
+ this.totalLeaves = totalLeaves;
1398
+ this.candidateLeaves = candidateLeaves;
1399
+ this.name = "AmbiguousOwnLeafError";
1400
+ }
1401
+ };
1382
1402
  var HealAddAfterRemoveError = class extends Error {
1383
1403
  constructor(cause) {
1384
1404
  super("healDivergedMember: Add failed after Remove already landed \u2014 member detached, re-run heal to recover");
@@ -1398,7 +1418,7 @@ var ForgedCredentialError = class extends Error {
1398
1418
  this.name = "ForgedCredentialError";
1399
1419
  }
1400
1420
  };
1401
- var MlsGroupKeyProvider = class {
1421
+ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
1402
1422
  o;
1403
1423
  suite;
1404
1424
  managers = /* @__PURE__ */ new Map();
@@ -1548,9 +1568,25 @@ var MlsGroupKeyProvider = class {
1548
1568
  * Snapshot of per-channel decrypt-error / stuck state (ADR-0002 observability).
1549
1569
  * Surfaces a wedged channel that would otherwise "look idle": the daemon serves
1550
1570
  * it on GET /v1/metrics and the web shell reads it via a selector. A channel is
1551
- * `stuck` once it has had `stuckThreshold` consecutive decrypt failures with no
1552
- * intervening successful decrypt. Returns a plain object keyed by channelId;
1553
- * absence of a key means zero errors observed for that channel.
1571
+ * `stuck` once it has had `stuckThreshold` GENUINELY-WEDGED decrypt failures
1572
+ * with no intervening successful decrypt. Returns a plain object keyed by
1573
+ * channelId; absence of a key means zero errors observed for that channel.
1574
+ *
1575
+ * `stuck` gates on `wedgedRun`, NOT `consecutiveErrors` — the same correction
1576
+ * the quarantine gate already got (P0a). `consecutiveErrors` is bumped at the
1577
+ * top of the catch for EVERY failure, INCLUDING the benign past-epoch
1578
+ * duplicate-commit churn that the `permanentlyDead` path immediately and
1579
+ * safely auto-drains. Deriving the user-visible health signal from it meant
1580
+ * ordinary at-least-once / committer-race traffic painted a perfectly healthy
1581
+ * channel red.
1582
+ *
1583
+ * That is the prod symptom of 2026-07-31: a freshly-welcomed federated partner
1584
+ * was queued the very Add commit its Welcome already contained. It joined the
1585
+ * correct group at the correct epoch, correctly identified the replayed commit
1586
+ * as dead and drained it — and still showed "This channel is stuck", because
1587
+ * the drain happened AFTER the error had been counted. `wedgedRun` counts only
1588
+ * rows left UN-acked (actually stuck), and a clean decrypt clears it, so a
1589
+ * genuine wedge still reports exactly as before.
1554
1590
  */
1555
1591
  errorStats() {
1556
1592
  const out = {};
@@ -1559,7 +1595,7 @@ var MlsGroupKeyProvider = class {
1559
1595
  decryptErrors: e.decryptErrors,
1560
1596
  consecutiveErrors: e.consecutiveErrors,
1561
1597
  lastErrorAt: e.lastErrorAt,
1562
- stuck: e.consecutiveErrors >= this.stuckThreshold,
1598
+ stuck: e.wedgedRun >= this.stuckThreshold,
1563
1599
  quarantined: e.quarantined
1564
1600
  };
1565
1601
  }
@@ -2224,6 +2260,25 @@ var MlsGroupKeyProvider = class {
2224
2260
  * `removed_device_id` (instead of removed_identity_id); the server resolves the
2225
2261
  * device's identity, asserts it matches, and purges that ONE device.
2226
2262
  */
2263
+ /**
2264
+ * This committer's OWN leaf index in the given group's ratchet tree, or null
2265
+ * when it cannot be read.
2266
+ *
2267
+ * `MLSGroupManager.state` is marked `@internal` but is a declared (non-private)
2268
+ * field, and `state.privatePath.leafIndex` is the same value ts-mls itself uses
2269
+ * as the sender leaf when protecting application data. Reading it here avoids a
2270
+ * change to `@agentvault/crypto`; if a public accessor is added later this
2271
+ * should delegate to it.
2272
+ *
2273
+ * Returns null rather than throwing so every caller must decide explicitly what
2274
+ * an unknown own-leaf means. The one caller today FAILS CLOSED on null by
2275
+ * falling back to the positional path only for FOREIGN identities — it never
2276
+ * uses null to justify guessing among an own identity's leaves.
2277
+ */
2278
+ ownLeafIndex(mgr) {
2279
+ const idx = mgr.state?.privatePath?.leafIndex;
2280
+ return typeof idx === "number" ? idx : null;
2281
+ }
2227
2282
  async removeDeviceFromLiveGroup(channelId, deviceId, opts) {
2228
2283
  return this.locks.run(channelId, () => this.evictOnThrow(channelId, () => this.removeDeviceFromLiveGroupUnlocked(channelId, deviceId, opts, 0)));
2229
2284
  }
@@ -2236,7 +2291,17 @@ var MlsGroupKeyProvider = class {
2236
2291
  if (leaves.length !== opts.orderedDeviceIds.length) {
2237
2292
  throw new LeafRosterMismatchError();
2238
2293
  }
2239
- const leafIndex = leaves[pos];
2294
+ let leafIndex;
2295
+ const ownLeaf = this.ownLeafIndex(mgr);
2296
+ if (ownLeaf !== null && leaves.includes(ownLeaf)) {
2297
+ const others = leaves.filter((i) => i !== ownLeaf);
2298
+ if (others.length !== 1) {
2299
+ throw new AmbiguousOwnLeafError(deviceId, leaves.length, others.length);
2300
+ }
2301
+ leafIndex = others[0];
2302
+ } else {
2303
+ leafIndex = leaves[pos];
2304
+ }
2240
2305
  const init = await this.o.backend.initConnectChannelGroup(channelId, [this.o.deviceId]);
2241
2306
  const priorEpoch = Number(mgr.epoch);
2242
2307
  const { commit } = await mgr.removeMembers([leafIndex]);
@@ -2489,7 +2554,7 @@ var MlsGroupKeyProvider = class {
2489
2554
  });
2490
2555
  await this.o.store.save(channelId, mgr.exportState());
2491
2556
  this.managers.set(channelId, mgr);
2492
- await this.o.store.deletePendingBundle(this.o.deviceId);
2557
+ return { staleMemberDeviceIds: reinit.stale_member_device_ids ?? [] };
2493
2558
  });
2494
2559
  }
2495
2560
  /** Returns true when an in-memory MLS manager is live for the channel.
@@ -2565,7 +2630,60 @@ var MlsGroupKeyProvider = class {
2565
2630
  * ⇒ refuse, never call verify), then structural signer check, then attestation
2566
2631
  * verify against the NON-EMPTY pinned key, then post-join group-id assertion.
2567
2632
  */
2633
+ /** C4 (split-brain Task 4): back-off for REFUSED supersedes, keyed
2634
+ * `${channelId}:${groupId}`. A refused supersede leaves its row un-acked by
2635
+ * design (it is not a stray), but pre-back-off that meant a full re-verify —
2636
+ * including a fetchResetAttestation HTTP round-trip — on EVERY receive poll,
2637
+ * forever, for a request that can never succeed (the nine production split
2638
+ * channels all carry NULL-attestation legacy groups).
2639
+ *
2640
+ * ⚠️ ONLY POST-FETCH refusals enter the back-off. The step-0 pin guard and
2641
+ * the retired-group check run BEFORE fetchResetAttestation, so they
2642
+ * contribute zero HTTP to the storm — and the pin guard's precondition is
2643
+ * TRANSIENT (cold owner cache / momentarily anomalous roster; the bridge
2644
+ * warms the cache before receiveOnce for exactly this reason). Backing a
2645
+ * pre-fetch refusal off buys nothing and converts a one-cycle stall into a
2646
+ * ten-minute outage on a valid recovery.
2647
+ *
2648
+ * Of the backed-off refusals: no-attestation, unparseable content,
2649
+ * channel/group mismatch and post-join mismatch are DETERMINISTIC per
2650
+ * (channel, group) — the attestation columns are written at group creation
2651
+ * and never change. Signer-mismatch, signature-verify and no-bundle are
2652
+ * ASSUMED stable for the window; they are pin- or bundle-dependent, but by
2653
+ * the time they run the fetch has already been paid, so suppressing the
2654
+ * window bounds the storm at the cost of a ≤10-minute delay if the pin or
2655
+ * bundle changes. Note the suppression means the reason is not RE-OBSERVED
2656
+ * during the window — the underlying precondition can change freely; the
2657
+ * window is a cost bound, not a claim of immutability.
2658
+ *
2659
+ * The reason lives in the VALUE for observability; the clearing rule lives
2660
+ * in the KEY: the owner minting a FIXED replacement group arrives with a
2661
+ * NEW group id — a different key, tried immediately. The un-openable path
2662
+ * (verified Welcome, KP mismatch) is EXEMPT: onSupersedeUnopenable's own
2663
+ * cooldown governs it. In-memory only: a reload/restart costs one extra
2664
+ * fetch per stranded row — reload-bounded, not poll-bounded. */
2665
+ supersedeRefusals = /* @__PURE__ */ new Map();
2666
+ static SUPERSEDE_REFUSAL_BACKOFF_MS = 10 * 6e4;
2667
+ /** True while (channelId, groupId) is inside its refusal back-off window.
2668
+ * @internal exposed for diagnostics only — not part of the stable surface. */
2669
+ supersedeBackoffActive(channelId, groupId) {
2670
+ const rec = this.supersedeRefusals.get(`${channelId}:${groupId}`);
2671
+ return rec !== void 0 && Date.now() < rec.until;
2672
+ }
2568
2673
  async trySupersede(channelId, row, heldGroupId) {
2674
+ const refuse = (msg) => {
2675
+ this.recordDecryptError(row, new Error(msg));
2676
+ const now = Date.now();
2677
+ for (const [k, v] of this.supersedeRefusals) {
2678
+ if (now >= v.until)
2679
+ this.supersedeRefusals.delete(k);
2680
+ }
2681
+ this.supersedeRefusals.set(`${channelId}:${row.group_id}`, {
2682
+ reason: msg,
2683
+ until: now + _MlsGroupKeyProvider.SUPERSEDE_REFUSAL_BACKOFF_MS
2684
+ });
2685
+ return false;
2686
+ };
2569
2687
  const owner = this.o.onResolvePinnedOwner ? await this.o.onResolvePinnedOwner(channelId) : null;
2570
2688
  if (!owner || !owner.publicKeyHex) {
2571
2689
  this.recordDecryptError(row, new Error("supersede refused: no pinned owner key (step-0 guard)"));
@@ -2578,33 +2696,27 @@ var MlsGroupKeyProvider = class {
2578
2696
  }
2579
2697
  const att = await this.o.backend.fetchResetAttestation(row.group_id);
2580
2698
  if (!att) {
2581
- this.recordDecryptError(row, new Error(`supersede refused: no attestation for group ${row.group_id}`));
2582
- return false;
2699
+ return refuse(`supersede refused: no attestation for group ${row.group_id}`);
2583
2700
  }
2584
2701
  if (att.signerIdentityId !== owner.identityId) {
2585
- this.recordDecryptError(row, new Error("supersede refused: signer is not the pinned owner"));
2586
- return false;
2702
+ return refuse("supersede refused: signer is not the pinned owner");
2587
2703
  }
2588
2704
  let content;
2589
2705
  try {
2590
2706
  content = JSON.parse(att.signedContent);
2591
2707
  } catch {
2592
- this.recordDecryptError(row, new Error("supersede refused: unparseable signed_content"));
2593
- return false;
2708
+ return refuse("supersede refused: unparseable signed_content");
2594
2709
  }
2595
2710
  if (content.channel_id !== row.connect_channel_id || content.new_group_id !== row.group_id) {
2596
- this.recordDecryptError(row, new Error("supersede refused: attestation channel/group mismatch"));
2597
- return false;
2711
+ return refuse("supersede refused: attestation channel/group mismatch");
2598
2712
  }
2599
2713
  const ok = await verifyApprovalArtifact({ content, signature: att.signatureHex, signerPublicKey: owner.publicKeyHex, createdAt: "", version: 1 }, owner.publicKeyHex);
2600
2714
  if (!ok) {
2601
- this.recordDecryptError(row, new Error("supersede refused: signature does not verify against pinned owner key"));
2602
- return false;
2715
+ return refuse("supersede refused: signature does not verify against pinned owner key");
2603
2716
  }
2604
2717
  const bundle = await this.o.store.loadPendingBundle(this.o.deviceId);
2605
2718
  if (!bundle) {
2606
- this.recordDecryptError(row, new Error("supersede refused: no pending KeyPackage bundle to open the new Welcome"));
2607
- return false;
2719
+ return refuse("supersede refused: no pending KeyPackage bundle to open the new Welcome");
2608
2720
  }
2609
2721
  return await this.locks.run(channelId, async () => {
2610
2722
  const mgr = new MLSGroupManager();
@@ -2629,14 +2741,14 @@ var MlsGroupKeyProvider = class {
2629
2741
  return false;
2630
2742
  }
2631
2743
  if (new TextDecoder().decode(mgr.groupId) !== row.group_id) {
2632
- this.recordDecryptError(row, new Error("supersede refused: post-join group id != attested new_group_id"));
2633
- return false;
2744
+ return refuse("supersede refused: post-join group id != attested new_group_id");
2634
2745
  }
2635
2746
  this.evictChannelState(channelId);
2636
2747
  await this.o.store.delete(channelId);
2637
2748
  await this.o.store.discardStaging(channelId);
2638
2749
  await this.o.store.addRetiredGroupId(channelId, heldGroupId);
2639
2750
  this.errors.delete(channelId);
2751
+ this.supersedeRefusals.delete(`${channelId}:${row.group_id}`);
2640
2752
  this.managers.set(channelId, mgr);
2641
2753
  await this.o.store.save(channelId, mgr.exportState());
2642
2754
  await this.o.backend.ackDelivery(this.o.deviceId, [row.queue_id]);
@@ -2673,9 +2785,15 @@ var MlsGroupKeyProvider = class {
2673
2785
  if (heldGroupId !== null && heldGroupId === row.group_id) {
2674
2786
  handshakeAcked.push(row.queue_id);
2675
2787
  } else if (heldGroupId !== null && heldGroupId !== row.group_id) {
2676
- const ok = await this.trySupersede(wcid, row, heldGroupId);
2677
- if (ok)
2678
- supersededChannelIds.push(wcid);
2788
+ if (this.supersedeBackoffActive(wcid, row.group_id)) {
2789
+ if (typeof process !== "undefined" && process.env?.SECURECOMS_TRACE_DELIVERY) {
2790
+ console.warn(`[trace] welcome ch=${wcid.slice(0, 8)} g=${row.group_id.slice(0, 8)} -> SKIP(refusal back-off)`);
2791
+ }
2792
+ } else {
2793
+ const ok = await this.trySupersede(wcid, row, heldGroupId);
2794
+ if (ok)
2795
+ supersededChannelIds.push(wcid);
2796
+ }
2679
2797
  }
2680
2798
  } else if (typeof process !== "undefined" && process.env?.SECURECOMS_TRACE_DELIVERY) {
2681
2799
  console.warn(`[trace] welcome ch=${wcid.slice(0, 8)} rowGroup=${(row.group_id || "-").slice(0, 8)} -> NO-LOCAL-STATE (leave un-acked for join path)`);
@@ -3378,19 +3496,8 @@ var RestMlsBackend = class {
3378
3496
  created: j.created === void 0 ? true : Boolean(j.created)
3379
3497
  };
3380
3498
  }
3381
- async reinitConnectChannelGroup(channelId, memberDeviceIds) {
3382
- const committer = memberDeviceIds[0];
3383
- if (!committer)
3384
- throw new Error("reinitConnectChannelGroup: empty member set");
3385
- const j = await this.post(`/api/v1/mls/connect-channels/${channelId}/reinit`, {
3386
- member_device_ids: memberDeviceIds,
3387
- committer_device_id: committer
3388
- });
3389
- return {
3390
- group_id: String(j.group_id),
3391
- current_epoch: Number(j.current_epoch),
3392
- member_device_ids: Array.isArray(j.member_device_ids) ? j.member_device_ids.map(String) : []
3393
- };
3499
+ async reinitConnectChannelGroup() {
3500
+ throw new Error("reinitConnectChannelGroup was removed (it minted unattested MLS groups that orphaned channel members permanently). Use resetReinitConnectChannelGroup with an owner attestation.");
3394
3501
  }
3395
3502
  async resetReinitConnectChannelGroup(channelId, memberDeviceIds, att) {
3396
3503
  const committer = memberDeviceIds[0];
@@ -3408,7 +3515,11 @@ var RestMlsBackend = class {
3408
3515
  return {
3409
3516
  group_id: String(j.group_id),
3410
3517
  current_epoch: Number(j.current_epoch),
3411
- member_device_ids: Array.isArray(j.member_device_ids) ? j.member_device_ids.map(String) : []
3518
+ member_device_ids: Array.isArray(j.member_device_ids) ? j.member_device_ids.map(String) : [],
3519
+ // Absent on a backend that predates the dropped-member report; an empty
3520
+ // list is the correct read of "the server told us nothing", since the old
3521
+ // server also never dropped anyone for an expired KeyPackage.
3522
+ stale_member_device_ids: Array.isArray(j.stale_member_device_ids) ? j.stale_member_device_ids.map(String) : []
3412
3523
  };
3413
3524
  }
3414
3525
  async fetchKeyPackages(deviceIds) {
@@ -3947,7 +4058,7 @@ function loadPackageMeta(importMetaUrl) {
3947
4058
 
3948
4059
  // src/bridge-agent.ts
3949
4060
  import { promises as fsp } from "node:fs";
3950
- import { join as join7, dirname as dirname3 } from "node:path";
4061
+ import { join as join8, dirname as dirname3 } from "node:path";
3951
4062
 
3952
4063
  // src/latency.ts
3953
4064
  var DEFAULT_CAPACITY = 1024;
@@ -4460,29 +4571,153 @@ function readRecentTurns(baseDir, agent, sessionId, k) {
4460
4571
  return k > 0 ? turns.slice(-k) : [];
4461
4572
  }
4462
4573
 
4574
+ // src/memory/openclaw-workspace-facts.ts
4575
+ import { readdirSync, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
4576
+ import { join as join7 } from "node:path";
4577
+ var DEFAULT_FACTS_BUDGET = 8e3;
4578
+ var MEMORY_MD_CAP = 6e3;
4579
+ var MIN_FRAGMENT = 200;
4580
+ var MAX_SOURCE_BYTES = 1024 * 1024;
4581
+ var DAILY_RE = /^\d{4}-\d{2}-\d{2}\.md$/;
4582
+ function resolveWorkspaceDir(baseDir, agent, override) {
4583
+ if (override) return expandHome(override);
4584
+ const base = expandHome(baseDir);
4585
+ const isMain2 = agent === "" || agent === "main";
4586
+ try {
4587
+ const cfg = JSON.parse(readFileSync5(join7(base, "openclaw.json"), "utf8"));
4588
+ const agents = cfg?.agents;
4589
+ if (agents && typeof agents === "object") {
4590
+ const list = Array.isArray(agents.list) ? agents.list : [];
4591
+ const wanted = isMain2 ? "main" : agent;
4592
+ const entry = list.find((e) => e && typeof e === "object" && e.id === wanted);
4593
+ const ws = entry?.workspace;
4594
+ if (typeof ws === "string" && ws) return expandHome(ws);
4595
+ const def = agents.defaults?.workspace;
4596
+ if (typeof def === "string" && def) return expandHome(def);
4597
+ }
4598
+ } catch {
4599
+ }
4600
+ return isMain2 ? join7(base, "workspace") : join7(base, `workspace-${agent}`);
4601
+ }
4602
+ var TRUNC = "\u2026[truncated]";
4603
+ function headSlice(s, n) {
4604
+ if (s.length <= n) return { text: s, clipped: false };
4605
+ let out = s.slice(0, n);
4606
+ const last = out.charCodeAt(out.length - 1);
4607
+ if (last >= 55296 && last <= 56319) out = out.slice(0, -1);
4608
+ return { text: out, clipped: true };
4609
+ }
4610
+ function tailSlice(s, n) {
4611
+ if (s.length <= n) return { text: s, clipped: false };
4612
+ let out = s.slice(s.length - n);
4613
+ const first = out.charCodeAt(0);
4614
+ if (first >= 56320 && first <= 57343) out = out.slice(1);
4615
+ return { text: out, clipped: true };
4616
+ }
4617
+ function readBounded(path) {
4618
+ if (statSync2(path).size > MAX_SOURCE_BYTES) return { text: null, oversize: true };
4619
+ return { text: readFileSync5(path, "utf8"), oversize: false };
4620
+ }
4621
+ function readWorkspaceFacts(baseDir, agent, opts) {
4622
+ const raw = opts?.charBudget;
4623
+ const budget = typeof raw === "number" && Number.isInteger(raw) && raw >= 0 ? raw : DEFAULT_FACTS_BUDGET;
4624
+ if (budget === 0) return { facts: [], truncated: false, missing: false };
4625
+ const dir = resolveWorkspaceDir(baseDir, agent, opts?.workspaceDir);
4626
+ const facts = [];
4627
+ let truncated = false;
4628
+ let remaining = budget;
4629
+ try {
4630
+ const r = readBounded(join7(dir, "MEMORY.md"));
4631
+ if (r.oversize) truncated = true;
4632
+ else if (r.text && r.text.trim()) {
4633
+ const { text, clipped } = headSlice(r.text, Math.min(MEMORY_MD_CAP, budget));
4634
+ truncated ||= clipped;
4635
+ facts.push(`workspace MEMORY.md:
4636
+ ${text}${clipped ? `
4637
+ ${TRUNC}` : ""}`);
4638
+ remaining -= text.length;
4639
+ }
4640
+ } catch {
4641
+ }
4642
+ try {
4643
+ const dailies = readdirSync(join7(dir, "memory")).filter((f) => DAILY_RE.test(f)).sort().reverse().slice(0, 2);
4644
+ for (const f of dailies) {
4645
+ if (remaining < MIN_FRAGMENT) {
4646
+ truncated = true;
4647
+ break;
4648
+ }
4649
+ let r;
4650
+ try {
4651
+ r = readBounded(join7(dir, "memory", f));
4652
+ } catch {
4653
+ continue;
4654
+ }
4655
+ if (r.oversize) {
4656
+ truncated = true;
4657
+ continue;
4658
+ }
4659
+ if (!r.text || !r.text.trim()) continue;
4660
+ const { text, clipped } = tailSlice(r.text, remaining);
4661
+ truncated ||= clipped;
4662
+ facts.push(`workspace memory/${f} (daily):
4663
+ ${clipped ? `${TRUNC}
4664
+ ` : ""}${text}`);
4665
+ remaining -= text.length;
4666
+ }
4667
+ } catch {
4668
+ }
4669
+ return { facts, truncated, missing: facts.length === 0 };
4670
+ }
4671
+
4463
4672
  // src/memory/openclaw-provider.ts
4464
4673
  var DEFAULT_K2 = 200;
4465
4674
  var OpenClawMemoryProvider = class {
4466
- constructor(store, baseDir, agent) {
4675
+ // gauge: last successful returned total, including prefixes/markers
4676
+ constructor(store, baseDir, agent, factsOpts) {
4467
4677
  this.store = store;
4468
4678
  this.baseDir = baseDir;
4469
4679
  this.agent = agent;
4680
+ this.factsOpts = factsOpts;
4470
4681
  }
4471
4682
  noSession = 0;
4472
4683
  readFailed = 0;
4684
+ factsMissing = 0;
4685
+ // absence-shaped: resolved path missing or zero sources (per-call)
4686
+ factsReadFailed = 0;
4687
+ // anything THROWN out of the facts computation
4688
+ factsTruncated = 0;
4689
+ // budget clipped/dropped a source (incl. the 1MB skip)
4690
+ factsChars = 0;
4473
4691
  async record(input) {
4474
4692
  if (input.modality !== "voice") return { seq: 0, deduped: true };
4475
4693
  return this.store.recordMemory({ ...input, text: `[voice] ${input.text}` });
4476
4694
  }
4477
4695
  async context(conversationId, limit) {
4478
4696
  const k = limit ?? DEFAULT_K2;
4697
+ let wsFacts = [];
4698
+ try {
4699
+ const r = readWorkspaceFacts(this.baseDir, this.agent, {
4700
+ workspaceDir: this.factsOpts?.workspaceDir,
4701
+ charBudget: this.factsOpts?.factsCharBudget
4702
+ });
4703
+ wsFacts = r.facts;
4704
+ if (r.missing) this.factsMissing++;
4705
+ if (r.truncated) this.factsTruncated++;
4706
+ this.factsChars = r.facts.reduce((n, f) => n + f.length, 0);
4707
+ } catch {
4708
+ this.factsReadFailed++;
4709
+ }
4710
+ const withFacts = (res) => ({
4711
+ recent: res.recent,
4712
+ facts: [...wsFacts, ...res.facts]
4713
+ });
4479
4714
  const voice = await this.store.memoryContext(conversationId, k);
4480
4715
  let textTurns = [];
4481
4716
  try {
4482
4717
  const sid = resolveOwnerDmSession(this.baseDir, this.agent);
4483
4718
  if (!sid) {
4484
4719
  this.noSession++;
4485
- return voice;
4720
+ return withFacts(voice);
4486
4721
  }
4487
4722
  textTurns = readRecentTurns(this.baseDir, this.agent, sid, k).map((t) => ({
4488
4723
  seq: 0,
@@ -4492,7 +4727,7 @@ var OpenClawMemoryProvider = class {
4492
4727
  }));
4493
4728
  } catch {
4494
4729
  this.readFailed++;
4495
- return voice;
4730
+ return withFacts(voice);
4496
4731
  }
4497
4732
  const tagged = [
4498
4733
  ...textTurns.map((t, i) => ({ ...t, _src: 0, _i: i })),
@@ -4511,10 +4746,17 @@ var OpenClawMemoryProvider = class {
4511
4746
  text: t.text,
4512
4747
  ts: t.ts
4513
4748
  }));
4514
- return { recent, facts: voice.facts };
4749
+ return withFacts({ recent, facts: voice.facts });
4515
4750
  }
4516
4751
  stats() {
4517
- return { openclaw_no_session: this.noSession, openclaw_read_failed: this.readFailed };
4752
+ return {
4753
+ openclaw_no_session: this.noSession,
4754
+ openclaw_read_failed: this.readFailed,
4755
+ openclaw_facts_missing: this.factsMissing,
4756
+ openclaw_facts_read_failed: this.factsReadFailed,
4757
+ openclaw_facts_truncated: this.factsTruncated,
4758
+ openclaw_facts_chars: this.factsChars
4759
+ };
4518
4760
  }
4519
4761
  };
4520
4762
 
@@ -4538,10 +4780,10 @@ var NullMemory = class {
4538
4780
  return { recent: [], facts: [] };
4539
4781
  }
4540
4782
  };
4541
- function createMemoryProvider(provider, store, baseDir, agent) {
4783
+ function createMemoryProvider(provider, store, baseDir, agent, openclawFacts) {
4542
4784
  if (provider === "none") return new NullMemory();
4543
4785
  if (provider === "hermes") return new HermesMemoryProvider(baseDir ?? "~/.hermes");
4544
- if (provider === "openclaw") return new OpenClawMemoryProvider(store, baseDir ?? "~/.openclaw", agent ?? "");
4786
+ if (provider === "openclaw") return new OpenClawMemoryProvider(store, baseDir ?? "~/.openclaw", agent ?? "", openclawFacts);
4545
4787
  return new SecureComsLocalMemory(store);
4546
4788
  }
4547
4789
 
@@ -4649,7 +4891,10 @@ var BridgeAgent = class _BridgeAgent {
4649
4891
  identityId: cfg.identityId,
4650
4892
  cipherFor: () => new KeyedInboxCipher(deriveMlsStoreKey(seed))
4651
4893
  });
4652
- this.memory = createMemoryProvider(this.cfg.memoryProvider, this.store, this.cfg.memoryBaseDir, this.cfg.memoryAgent);
4894
+ this.memory = createMemoryProvider(this.cfg.memoryProvider, this.store, this.cfg.memoryBaseDir, this.cfg.memoryAgent, {
4895
+ workspaceDir: this.cfg.memoryWorkspaceDir,
4896
+ factsCharBudget: this.cfg.memoryFactsCharBudget
4897
+ });
4653
4898
  this.provider = new MlsGroupKeyProvider({
4654
4899
  store: this.store,
4655
4900
  backend: this.backend,
@@ -4729,7 +4974,7 @@ var BridgeAgent = class _BridgeAgent {
4729
4974
  const j = await res.json();
4730
4975
  return String(j.attachment_id);
4731
4976
  };
4732
- this.attachmentsDirImpl = deps.attachmentsDir ?? join7(dirname3(cfg.storePath), "attachments");
4977
+ this.attachmentsDirImpl = deps.attachmentsDir ?? join8(dirname3(cfg.storePath), "attachments");
4733
4978
  this.action = {
4734
4979
  begin: (channelId) => this.actionBegin(channelId),
4735
4980
  start: (opts) => this.actionStart(opts),
@@ -5213,11 +5458,33 @@ var BridgeAgent = class _BridgeAgent {
5213
5458
  return;
5214
5459
  }
5215
5460
  this.providerCapabilityMissing = false;
5216
- let chans;
5217
- try {
5218
- chans = await this.listChannels();
5219
- } catch (e) {
5220
- console.warn("[bridge] selfHealSweep aborted: listChannels failed:", e);
5461
+ const enclaves = await this.enclavesToCover();
5462
+ this.enclaveListEverSucceeded.add(this.cfg.enclaveId);
5463
+ const seen = /* @__PURE__ */ new Set();
5464
+ const chans = [];
5465
+ const failed = [];
5466
+ let listedAny = false;
5467
+ for (const encId of enclaves) {
5468
+ try {
5469
+ for (const c of await this.listChannels(encId)) {
5470
+ if (seen.has(c.channel_id)) continue;
5471
+ seen.add(c.channel_id);
5472
+ chans.push(c);
5473
+ }
5474
+ listedAny = true;
5475
+ this.enclaveListEverSucceeded.add(encId);
5476
+ this.enclaveListWarned.delete(encId);
5477
+ } catch (e) {
5478
+ failed.push(encId);
5479
+ if (!this.enclaveListWarned.has(encId)) {
5480
+ this.enclaveListWarned.add(encId);
5481
+ console.warn(`[bridge] selfHealSweep: listChannels failed for enclave ${encId}; skipping (warn-once per streak):`, e);
5482
+ }
5483
+ }
5484
+ }
5485
+ this.sweepEnclavesFailed = failed;
5486
+ if (!listedAny) {
5487
+ console.warn("[bridge] selfHealSweep aborted: every enclave listing failed");
5221
5488
  return;
5222
5489
  }
5223
5490
  for (const c of chans) {
@@ -5563,7 +5830,7 @@ var BridgeAgent = class _BridgeAgent {
5563
5830
  * the other enclaves in the set.
5564
5831
  */
5565
5832
  async refreshChannelMeta() {
5566
- const enclaves = /* @__PURE__ */ new Set([this.cfg.enclaveId, ...this.store.distinctEnclaveIds()]);
5833
+ const enclaves = await this.enclavesToCover();
5567
5834
  const now = Date.now();
5568
5835
  for (const encId of enclaves) {
5569
5836
  try {
@@ -5576,6 +5843,89 @@ var BridgeAgent = class _BridgeAgent {
5576
5843
  }
5577
5844
  }
5578
5845
  }
5846
+ /** Cache for the server-side enclave enumeration (see enclavesToCover). */
5847
+ enclaveEnumCache = null;
5848
+ /** Enclaves whose channel listing failed on the NEWEST sweep (audit HIGH:
5849
+ * skip-one deleted the sweep's partial-coverage signal — observations_stale
5850
+ * is a global max, so one healthy enclave masked a failed one). Surfaced on
5851
+ * health() so partial coverage is never silent. */
5852
+ sweepEnclavesFailed = [];
5853
+ /** Enclave ids whose channel listing has EVER succeeded this process, seeded
5854
+ * with HOME (home is always expected to list; without the seed, a home
5855
+ * failure right after a restart would read as "never succeeded" and slip
5856
+ * through the expected-gap branch — weaker than the old abort-on-failure).
5857
+ * A failure of a member of this set is a REGRESSION and degrades health; a
5858
+ * failure of a never-succeeded enclave (partner: tenant-scoped 404, permanent
5859
+ * pending backend authz) is an EXPECTED gap — reported, not degrading, or
5860
+ * every federated agent would peg degraded forever. */
5861
+ enclaveListEverSucceeded = /* @__PURE__ */ new Set();
5862
+ /** Warn-once-per-failure-streak, keyed by enclave id (cleared on success):
5863
+ * a permanently-404ing partner enclave must not log 1440 lines a day. */
5864
+ enclaveListWarned = /* @__PURE__ */ new Set();
5865
+ static ENCLAVE_ENUM_TTL_MS = 5 * 6e4;
5866
+ enclaveEnumWarned = false;
5867
+ /**
5868
+ * C1 (split-brain Task 3): every enclave this agent must COVER — for the
5869
+ * self-heal sweep and for channel-meta refresh alike. ONE helper on purpose:
5870
+ * these two call sites had drifted (refreshChannelMeta looped home+learned,
5871
+ * selfHealSweep looped nothing), and the drift is exactly what left both
5872
+ * production agents with zero recovery coverage in their second enclave.
5873
+ *
5874
+ * The set is SERVER enumeration ∪ home ∪ store-learned:
5875
+ *
5876
+ * - SERVER (`GET /api/v1/enclaves`, membership-scoped, federated included)
5877
+ * is the authoritative source. The store CANNOT be: channel_meta's only
5878
+ * writers are listChannels itself (circular — you learn enclave B by
5879
+ * having already listed B) and the receive pump's successfully-decrypted
5880
+ * loop, and a FORKED agent decrypts nothing, so the agent that most needs
5881
+ * sweeping in enclave B is precisely the one whose store never learned B.
5882
+ * - home + learned stay in the union as the degraded-mode floor: if the
5883
+ * enumeration endpoint is down, we sweep what we can rather than nothing,
5884
+ * and we warn (once) so the gap is diagnosable.
5885
+ *
5886
+ * The server result is cached for ENCLAVE_ENUM_TTL_MS so per-ingest-cycle
5887
+ * callers (refreshChannelMeta) do not add an HTTP round-trip per cycle;
5888
+ * membership changes are picked up within one TTL, which matches the sweep
5889
+ * cadence this exists to serve.
5890
+ *
5891
+ * ⚠️ Residual gap, stated: the enumeration RETURNS cross-tenant partner
5892
+ * enclaves, but `list_channels_in_enclave` is tenant-scoped and 404s them
5893
+ * under this agent's bearer — so a partner enclave in this set still yields
5894
+ * no channels. Callers must treat a per-enclave listing failure as skip-one,
5895
+ * never abort-all. Closing that needs backend authz work, not daemon work.
5896
+ */
5897
+ async enclavesToCover() {
5898
+ const now = Date.now();
5899
+ if (!this.enclaveEnumCache || now - this.enclaveEnumCache.at > _BridgeAgent.ENCLAVE_ENUM_TTL_MS) {
5900
+ try {
5901
+ const url = `${this.cfg.apiBase}/api/v1/enclaves?caller_identity_id=${encodeURIComponent(this.cfg.identityId)}`;
5902
+ const res = await this.fetchImpl(url, {
5903
+ headers: { authorization: `Bearer ${this.cfg.apiKey}` }
5904
+ });
5905
+ if (!res.ok) throw new Error(`list_enclaves failed: ${res.status}`);
5906
+ const rows = await res.json();
5907
+ this.enclaveEnumCache = { ids: rows.map((r) => r.enclave_id), at: Date.now() };
5908
+ this.enclaveEnumWarned = false;
5909
+ } catch (e) {
5910
+ if (!this.enclaveEnumWarned) {
5911
+ this.enclaveEnumWarned = true;
5912
+ console.warn(
5913
+ "[bridge] enclave enumeration failed; sweep coverage degraded to home+learned enclaves:",
5914
+ e
5915
+ );
5916
+ }
5917
+ }
5918
+ }
5919
+ const fresh = this.enclaveEnumCache && Date.now() - this.enclaveEnumCache.at <= _BridgeAgent.ENCLAVE_ENUM_TTL_MS;
5920
+ if (fresh) {
5921
+ return /* @__PURE__ */ new Set([this.cfg.enclaveId, ...this.enclaveEnumCache.ids]);
5922
+ }
5923
+ return /* @__PURE__ */ new Set([
5924
+ this.cfg.enclaveId,
5925
+ ...this.store.distinctEnclaveIds(),
5926
+ ...this.enclaveEnumCache?.ids ?? []
5927
+ ]);
5928
+ }
5579
5929
  /** A live roster for a channel, or undefined if missing OR expired past the TTL. */
5580
5930
  freshRoster(channelId) {
5581
5931
  const hit = this.channelRoster.get(channelId);
@@ -6172,6 +6522,9 @@ var BridgeAgent = class _BridgeAgent {
6172
6522
  const stalePeriodMs = _BridgeAgent.SELF_HEAL_SWEEP_INTERVAL_MS + _BridgeAgent.SELF_HEAL_SWEEP_JITTER_MS;
6173
6523
  const observationsStale = this.providerCapabilityMissing || channels.length === 0 || Date.now() - newest > stalePeriodMs;
6174
6524
  const divergedChannels = channels.filter((o) => o.diverged === true).length;
6525
+ const sweepCoverageLost = this.sweepEnclavesFailed.some(
6526
+ (id) => this.enclaveListEverSucceeded.has(id)
6527
+ );
6175
6528
  const liveChannels = channels.map((o) => ({
6176
6529
  ...o,
6177
6530
  last_decrypt_at: this.provider.lastDecryptAtFor?.(o.channel_id) ?? o.last_decrypt_at
@@ -6186,8 +6539,9 @@ var BridgeAgent = class _BridgeAgent {
6186
6539
  observations_stale: observationsStale,
6187
6540
  diverged_channels: divergedChannels,
6188
6541
  // A2: the truthful roll-up gates read instead of `status` (D-3).
6189
- degraded: divergedChannels > 0 || observationsStale,
6542
+ degraded: divergedChannels > 0 || observationsStale || sweepCoverageLost,
6190
6543
  channels: liveChannels,
6544
+ sweep_enclaves_failed: [...this.sweepEnclavesFailed],
6191
6545
  identity_id: this.cfg.identityId,
6192
6546
  joined_channels: joined,
6193
6547
  buffered: this.buffer.length,
@@ -6257,11 +6611,11 @@ var BridgeAgent = class _BridgeAgent {
6257
6611
 
6258
6612
  // src/auth.ts
6259
6613
  import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
6260
- import { existsSync, readFileSync as readFileSync5, writeFileSync } from "node:fs";
6614
+ import { existsSync, readFileSync as readFileSync6, writeFileSync } from "node:fs";
6261
6615
  function resolveToken(opts) {
6262
6616
  if (opts.token) return opts.token;
6263
6617
  const f = opts.tokenFile;
6264
- if (f && existsSync(f)) return readFileSync5(f, "utf8").trim();
6618
+ if (f && existsSync(f)) return readFileSync6(f, "utf8").trim();
6265
6619
  const tok = randomBytes3(32).toString("base64url");
6266
6620
  if (f) writeFileSync(f, tok, { mode: 384 });
6267
6621
  return tok;
@@ -6832,8 +7186,8 @@ async function handleInbound(sock, agent, data, inflight) {
6832
7186
  }
6833
7187
 
6834
7188
  // src/bin.ts
6835
- import { readFileSync as readFileSync7, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, rmSync, existsSync as existsSync2, realpathSync, readdirSync, statSync as statSync2 } from "node:fs";
6836
- import { join as join11, dirname as dirname7, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
7189
+ import { readFileSync as readFileSync8, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, rmSync, existsSync as existsSync2, realpathSync, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
7190
+ import { join as join12, dirname as dirname7, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
6837
7191
  import { fileURLToPath as fileURLToPath2 } from "node:url";
6838
7192
  import { homedir as homedir2, userInfo } from "node:os";
6839
7193
  import { spawnSync } from "node:child_process";
@@ -7042,10 +7396,10 @@ function uninstallService(input, deps) {
7042
7396
  }
7043
7397
 
7044
7398
  // src/service/install-prep.ts
7045
- import { join as join9 } from "node:path";
7399
+ import { join as join10 } from "node:path";
7046
7400
 
7047
7401
  // src/service/wire.ts
7048
- import { join as join8, dirname as dirname5 } from "node:path";
7402
+ import { join as join9, dirname as dirname5 } from "node:path";
7049
7403
  import { fileURLToPath } from "node:url";
7050
7404
 
7051
7405
  // src/service/hermes-config.ts
@@ -7211,7 +7565,7 @@ function connectorTargetDir(home, harness) {
7211
7565
  }
7212
7566
  function connectorsSrcDir(importMetaUrl, harness) {
7213
7567
  const pkgRoot = dirname5(dirname5(fileURLToPath(importMetaUrl)));
7214
- return join8(pkgRoot, "connectors", harness);
7568
+ return join9(pkgRoot, "connectors", harness);
7215
7569
  }
7216
7570
  function hermesConfigPath(home) {
7217
7571
  return `${home}/.hermes/config.yaml`;
@@ -7430,10 +7784,10 @@ function assertReadableCredential(absPath, deps) {
7430
7784
  return { deviceId: json2.device_id, identityId: json2.identity_id, issuedAt };
7431
7785
  }
7432
7786
  function durableDir(home, slug) {
7433
- return join9(home, `securecoms-agent-${slug}`);
7787
+ return join10(home, `securecoms-agent-${slug}`);
7434
7788
  }
7435
7789
  function durableCredentialPath(home, slug) {
7436
- return join9(durableDir(home, slug), `${slug}.json`);
7790
+ return join10(durableDir(home, slug), `${slug}.json`);
7437
7791
  }
7438
7792
  function takeCustody(args2, deps) {
7439
7793
  const dir = durableDir(args2.home, args2.slug);
@@ -7480,7 +7834,7 @@ function readSiblingDefs(dir, prefix, deps) {
7480
7834
  for (const n of names) {
7481
7835
  if (!n.startsWith(prefix)) continue;
7482
7836
  try {
7483
- out.push(deps.readFile(join9(dir, n)));
7837
+ out.push(deps.readFile(join10(dir, n)));
7484
7838
  } catch {
7485
7839
  }
7486
7840
  }
@@ -7676,29 +8030,29 @@ function renderStatusTable(rows) {
7676
8030
  import {
7677
8031
  chmodSync,
7678
8032
  mkdirSync,
7679
- readFileSync as readFileSync6,
8033
+ readFileSync as readFileSync7,
7680
8034
  renameSync,
7681
8035
  unlinkSync,
7682
8036
  writeFileSync as writeFileSync2
7683
8037
  } from "node:fs";
7684
8038
  import { randomUUID } from "node:crypto";
7685
- import { join as join10 } from "node:path";
8039
+ import { join as join11 } from "node:path";
7686
8040
  var REAL_FS = {
7687
8041
  mkdir: (p, mode) => mkdirSync(p, { recursive: true, mode }),
7688
8042
  chmod: (p, mode) => chmodSync(p, mode),
7689
8043
  writeFile: (p, data, mode) => writeFileSync2(p, data, { mode }),
7690
8044
  rename: (a, b) => renameSync(a, b),
7691
- readFile: (p) => readFileSync6(p, "utf8"),
8045
+ readFile: (p) => readFileSync7(p, "utf8"),
7692
8046
  unlink: (p) => unlinkSync(p)
7693
8047
  };
7694
8048
  function securecomsDir(home) {
7695
- return join10(home, ".securecoms");
8049
+ return join11(home, ".securecoms");
7696
8050
  }
7697
8051
  function connectionsDir(home) {
7698
- return join10(securecomsDir(home), "connections");
8052
+ return join11(securecomsDir(home), "connections");
7699
8053
  }
7700
8054
  function connectionFilePath(home, identityId) {
7701
- return join10(connectionsDir(home), `${identityId}.json`);
8055
+ return join11(connectionsDir(home), `${identityId}.json`);
7702
8056
  }
7703
8057
  function newInstanceId() {
7704
8058
  return randomUUID();
@@ -7813,7 +8167,7 @@ function realResolveDeps() {
7813
8167
  }
7814
8168
  function realPrepDeps() {
7815
8169
  return {
7816
- readFile: (p) => readFileSync7(p, "utf8"),
8170
+ readFile: (p) => readFileSync8(p, "utf8"),
7817
8171
  writeFileMode: (p, data, mode) => writeFileSync3(p, data, { mode }),
7818
8172
  mkdirp: (p, mode) => {
7819
8173
  mkdirSync2(p, { recursive: true, mode: mode ?? 493 });
@@ -7822,7 +8176,7 @@ function realPrepDeps() {
7822
8176
  realpath: (p) => realpathSync(p),
7823
8177
  listDir: (p) => {
7824
8178
  try {
7825
- return readdirSync(p);
8179
+ return readdirSync2(p);
7826
8180
  } catch {
7827
8181
  return [];
7828
8182
  }
@@ -7864,12 +8218,12 @@ function realReadFleet() {
7864
8218
  home: homedir2(),
7865
8219
  listDir: (p) => {
7866
8220
  try {
7867
- return readdirSync(p);
8221
+ return readdirSync2(p);
7868
8222
  } catch {
7869
8223
  return [];
7870
8224
  }
7871
8225
  },
7872
- readFile: (p) => readFileSync7(p, "utf8")
8226
+ readFile: (p) => readFileSync8(p, "utf8")
7873
8227
  });
7874
8228
  }
7875
8229
  function runStatus() {
@@ -7892,43 +8246,43 @@ function runStatus() {
7892
8246
  function realCopyConnector(srcDir, dstDir, harness) {
7893
8247
  const allowedExt = CONNECTOR_FILE_EXTENSIONS[harness];
7894
8248
  mkdirSync2(dstDir, { recursive: true });
7895
- for (const name of readdirSync(srcDir)) {
7896
- const srcPath = join11(srcDir, name);
7897
- if (statSync2(srcPath).isDirectory()) {
8249
+ for (const name of readdirSync2(srcDir)) {
8250
+ const srcPath = join12(srcDir, name);
8251
+ if (statSync3(srcPath).isDirectory()) {
7898
8252
  if (CONNECTOR_SKIP_DIRS.has(name)) continue;
7899
- realCopyConnector(srcPath, join11(dstDir, name), harness);
8253
+ realCopyConnector(srcPath, join12(dstDir, name), harness);
7900
8254
  continue;
7901
8255
  }
7902
8256
  if (name.includes(".test.")) continue;
7903
8257
  if (!allowedExt.some((ext) => name.endsWith(ext))) continue;
7904
- writeFileSync3(join11(dstDir, name), readFileSync7(srcPath));
8258
+ writeFileSync3(join12(dstDir, name), readFileSync8(srcPath));
7905
8259
  }
7906
8260
  }
7907
8261
  function realWriteBinding(dstDir, identityId) {
7908
8262
  mkdirSync2(dstDir, { recursive: true });
7909
- writeFileSync3(join11(dstDir, "securecoms.binding.json"), JSON.stringify({ identity_id: identityId }) + "\n", { mode: 384 });
8263
+ writeFileSync3(join12(dstDir, "securecoms.binding.json"), JSON.stringify({ identity_id: identityId }) + "\n", { mode: 384 });
7910
8264
  }
7911
8265
  function realReadBinding(dstDir) {
7912
8266
  try {
7913
- const parsed = JSON.parse(readFileSync7(join11(dstDir, "securecoms.binding.json"), "utf8"));
8267
+ const parsed = JSON.parse(readFileSync8(join12(dstDir, "securecoms.binding.json"), "utf8"));
7914
8268
  return parsed.identity_id ?? null;
7915
8269
  } catch {
7916
8270
  return null;
7917
8271
  }
7918
8272
  }
7919
8273
  function realRemoveBinding(dstDir) {
7920
- rmSync(join11(dstDir, "securecoms.binding.json"), { force: true });
8274
+ rmSync(join12(dstDir, "securecoms.binding.json"), { force: true });
7921
8275
  }
7922
8276
  function realStatDir(p) {
7923
8277
  try {
7924
- return statSync2(p).isDirectory();
8278
+ return statSync3(p).isDirectory();
7925
8279
  } catch {
7926
8280
  return false;
7927
8281
  }
7928
8282
  }
7929
8283
  function realReadHermesConfig(p) {
7930
8284
  try {
7931
- return readFileSync7(p, "utf8");
8285
+ return readFileSync8(p, "utf8");
7932
8286
  } catch {
7933
8287
  return null;
7934
8288
  }
@@ -8087,11 +8441,11 @@ async function main() {
8087
8441
  `);
8088
8442
  process.exit(1);
8089
8443
  }
8090
- const tokenFile = daemon.tokenFile ?? join11(dirname7(credentialPath), "bridge-token");
8444
+ const tokenFile = daemon.tokenFile ?? join12(dirname7(credentialPath), "bridge-token");
8091
8445
  const token = resolveToken({ tokenFile, token: daemon.token });
8092
8446
  const agent = new BridgeAgent(cfg);
8093
8447
  await agent.start();
8094
- const tls = daemon.tls ? { cert: readFileSync7(daemon.tlsCert, "utf8"), key: readFileSync7(daemon.tlsKey, "utf8") } : void 0;
8448
+ const tls = daemon.tls ? { cert: readFileSync8(daemon.tlsCert, "utf8"), key: readFileSync8(daemon.tlsKey, "utf8") } : void 0;
8095
8449
  const { server, closeAllSseStreams } = createHttpServer(agent, { token, allowOrigins: daemon.allowOrigins, tls, websocket: true });
8096
8450
  const agentWs = attachAgentWebSocket(server, agent, { token, allowOrigins: daemon.allowOrigins });
8097
8451
  let down = false;