@securecoms/agent-bridge 0.3.1 → 0.4.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.
Files changed (45) hide show
  1. package/README.md +11 -1
  2. package/dist/bin.js +892 -118
  3. package/dist/bridge-agent.d.ts +69 -8
  4. package/dist/config.d.ts +6 -0
  5. package/dist/http-server.d.ts +42 -0
  6. package/dist/index.js +844 -62
  7. package/dist/memory/hermes-provider.d.ts +21 -0
  8. package/dist/memory/hermes-session-map.d.ts +8 -0
  9. package/dist/memory/hermes-store.d.ts +32 -0
  10. package/dist/memory/hermes-test-fixture.d.ts +5 -0
  11. package/dist/memory/openclaw-provider.d.ts +24 -0
  12. package/dist/memory/openclaw-session-log.d.ts +15 -0
  13. package/dist/memory/openclaw-session-map.d.ts +7 -0
  14. package/dist/memory/provider.d.ts +29 -0
  15. package/dist/memory-taps.d.ts +13 -0
  16. package/dist/preflight-modern.cjs +35 -0
  17. package/dist/preflight-modern.d.cts +1 -0
  18. package/dist/preflight-upgrade.d.cts +30 -0
  19. package/dist/preflight.cjs +277 -0
  20. package/dist/preflight.d.cts +14 -0
  21. package/package.json +3 -5
  22. package/dist/attachments.js +0 -49
  23. package/dist/auth.js +0 -56
  24. package/dist/bin.js.pre-c1-f803007 +0 -5795
  25. package/dist/bin.js.pre-c3-096a9a3 +0 -5832
  26. package/dist/bin.js.pre-ffrebuild-b162d4a +0 -5639
  27. package/dist/bin.js.pre-kpheal-20260708-150924 +0 -5531
  28. package/dist/bin.js.pre-kppersist-20260708-152341 +0 -5543
  29. package/dist/bin.js.pre-p0-20260708-102415 +0 -5196
  30. package/dist/bin.js.pre-p1b-02f47e1 +0 -5639
  31. package/dist/bin.js.pre-warmfix-20260708-153705 +0 -5613
  32. package/dist/bin.js.pretrace-113205 +0 -5395
  33. package/dist/bridge-agent.js +0 -831
  34. package/dist/config.js +0 -74
  35. package/dist/coordination.js +0 -63
  36. package/dist/errors.js +0 -25
  37. package/dist/http-server.js +0 -220
  38. package/dist/index.js.pre-p0-20260708-102415 +0 -4879
  39. package/dist/latency.js +0 -87
  40. package/dist/rate-limit.js +0 -56
  41. package/dist/service/config.js +0 -26
  42. package/dist/service/install.js +0 -60
  43. package/dist/service/launchd.js +0 -30
  44. package/dist/service/systemd.js +0 -25
  45. package/dist/ws-server.js +0 -254
package/dist/index.js CHANGED
@@ -568,6 +568,152 @@ var payloadsSchema = {
568
568
  }
569
569
  }
570
570
  ]
571
+ },
572
+ "call_control": {
573
+ "description": 'The call_control signaling body for 1:1 voice calls (docs/superpowers/specs/2026-07-12-voice-human-to-human-calling-design.md). NOT a message_type \u2014 it rides envelope message_type "system_event"; the "kind":"call_control" field is a body-level discriminator and "event" selects the variant. Compiled standalone via validate.ts validateCallControlBody. SERVER-BLIND KEY CONTRACT: NO variant ever carries media key material (the media key is derived independently from the MLS exporter, never transmitted). Every variant sets additionalProperties:false so a stray media_key/key/secret is rejected \u2014 the only key-adjacent field is `key_confirmation` on accept, which is HMAC(media_key, call_id), not the key itself.',
574
+ "oneOf": [
575
+ {
576
+ "type": "object",
577
+ "additionalProperties": false,
578
+ "required": [
579
+ "kind",
580
+ "event",
581
+ "call_id",
582
+ "room",
583
+ "caller_identity_id",
584
+ "epoch",
585
+ "ts"
586
+ ],
587
+ "properties": {
588
+ "kind": {
589
+ "const": "call_control"
590
+ },
591
+ "event": {
592
+ "const": "invite"
593
+ },
594
+ "call_id": {
595
+ "type": "string",
596
+ "minLength": 1
597
+ },
598
+ "room": {
599
+ "type": "string",
600
+ "minLength": 1
601
+ },
602
+ "caller_identity_id": {
603
+ "type": "string",
604
+ "format": "uuid"
605
+ },
606
+ "epoch": {
607
+ "type": "integer",
608
+ "minimum": 0
609
+ },
610
+ "ts": {
611
+ "type": "number"
612
+ }
613
+ }
614
+ },
615
+ {
616
+ "type": "object",
617
+ "additionalProperties": false,
618
+ "required": [
619
+ "kind",
620
+ "event",
621
+ "call_id",
622
+ "accepting_device_id",
623
+ "key_confirmation"
624
+ ],
625
+ "properties": {
626
+ "kind": {
627
+ "const": "call_control"
628
+ },
629
+ "event": {
630
+ "const": "accept"
631
+ },
632
+ "call_id": {
633
+ "type": "string",
634
+ "minLength": 1
635
+ },
636
+ "accepting_device_id": {
637
+ "type": "string",
638
+ "minLength": 1
639
+ },
640
+ "key_confirmation": {
641
+ "type": "string",
642
+ "minLength": 1
643
+ }
644
+ }
645
+ },
646
+ {
647
+ "type": "object",
648
+ "additionalProperties": false,
649
+ "required": [
650
+ "kind",
651
+ "event",
652
+ "call_id"
653
+ ],
654
+ "properties": {
655
+ "kind": {
656
+ "const": "call_control"
657
+ },
658
+ "event": {
659
+ "const": "decline"
660
+ },
661
+ "call_id": {
662
+ "type": "string",
663
+ "minLength": 1
664
+ },
665
+ "reason": {
666
+ "type": "string",
667
+ "enum": [
668
+ "busy",
669
+ "declined"
670
+ ]
671
+ }
672
+ }
673
+ },
674
+ {
675
+ "type": "object",
676
+ "additionalProperties": false,
677
+ "required": [
678
+ "kind",
679
+ "event",
680
+ "call_id"
681
+ ],
682
+ "properties": {
683
+ "kind": {
684
+ "const": "call_control"
685
+ },
686
+ "event": {
687
+ "const": "cancel"
688
+ },
689
+ "call_id": {
690
+ "type": "string",
691
+ "minLength": 1
692
+ }
693
+ }
694
+ },
695
+ {
696
+ "type": "object",
697
+ "additionalProperties": false,
698
+ "required": [
699
+ "kind",
700
+ "event",
701
+ "call_id"
702
+ ],
703
+ "properties": {
704
+ "kind": {
705
+ "const": "call_control"
706
+ },
707
+ "event": {
708
+ "const": "end"
709
+ },
710
+ "call_id": {
711
+ "type": "string",
712
+ "minLength": 1
713
+ }
714
+ }
715
+ }
716
+ ]
571
717
  }
572
718
  }
573
719
  };
@@ -592,6 +738,11 @@ if (!agentActionDef) {
592
738
  throw new Error(`payloads.schema.json is missing the "agent_action" $defs schema`);
593
739
  }
594
740
  var agentActionValidator = ajv.compile(agentActionDef);
741
+ var callControlDef = payloadDefs["call_control"];
742
+ if (!callControlDef) {
743
+ throw new Error(`payloads.schema.json is missing the "call_control" $defs schema`);
744
+ }
745
+ var callControlValidator = ajv.compile(callControlDef);
595
746
 
596
747
  // ../connect-core/dist/envelope/sign.js
597
748
  import { canonicalize } from "@agentvault/crypto/canonical";
@@ -653,6 +804,7 @@ import { gcm } from "@noble/ciphers/aes";
653
804
  import { xchacha20poly1305 } from "@noble/ciphers/chacha";
654
805
  import { randomBytes } from "@noble/hashes/utils";
655
806
  import { canonicalize as canonicalize2 } from "@agentvault/crypto/canonical";
807
+ var VOICE_MEDIA_EXPORTER_LABEL = "securecoms-voice-media-v1";
656
808
  var APPLICATION_KEY_BYTES = 32;
657
809
  var REPLAY_NONCE_BYTES = 24;
658
810
  var NONCE_BYTES = {
@@ -674,10 +826,20 @@ function epochToBytes(epoch) {
674
826
  new DataView(b.buffer).setBigUint64(0, BigInt(epoch), false);
675
827
  return b;
676
828
  }
829
+ function u32ToBytes(n) {
830
+ const b = new Uint8Array(4);
831
+ new DataView(b.buffer).setUint32(0, n >>> 0, false);
832
+ return b;
833
+ }
677
834
  function buildExporterContext(enclaveId, channelId, epoch) {
678
835
  const enc = new TextEncoder();
679
836
  return concatBytes(enc.encode(enclaveId), enc.encode(channelId), epochToBytes(epoch));
680
837
  }
838
+ function buildVoiceExporterContext(enclaveId, channelId, epoch, callId) {
839
+ const enc = new TextEncoder();
840
+ const callIdBytes = enc.encode(callId);
841
+ return concatBytes(buildExporterContext(enclaveId, channelId, epoch), u32ToBytes(callIdBytes.length), callIdBytes);
842
+ }
681
843
  function cipherFor(suite, key, nonce, aad) {
682
844
  return suite === "aes256gcm" ? gcm(key, nonce, aad) : xchacha20poly1305(key, nonce, aad);
683
845
  }
@@ -1045,8 +1207,13 @@ var ALWAYS_SAMPLE = /* @__PURE__ */ new Set([
1045
1207
  // ../connect-sdk-ts/dist/spans/emitter.js
1046
1208
  var KNOWN = new Set(Object.values(SPAN));
1047
1209
 
1210
+ // ../connect-sdk-ts/dist/voice-key.js
1211
+ import { hmac } from "@noble/hashes/hmac";
1212
+ import { sha256 as sha2562 } from "@noble/hashes/sha256";
1213
+ import { bytesToHex } from "@agentvault/crypto";
1214
+
1048
1215
  // ../connect-sdk-ts/dist/groupkey/mls.js
1049
- import { MLSGroupManager, bytesToHex, hexToBytes, verifyApprovalArtifact } from "@agentvault/crypto";
1216
+ import { MLSGroupManager, bytesToHex as bytesToHex2, hexToBytes, verifyApprovalArtifact } from "@agentvault/crypto";
1050
1217
 
1051
1218
  // ../connect-sdk-ts/dist/groupkey/operation-queue.js
1052
1219
  var MlsOperationQueue = class {
@@ -1426,6 +1593,28 @@ var MlsGroupKeyProvider = class {
1426
1593
  const mgr = await this.manager(channelId);
1427
1594
  return mgr.exportSecret(LABEL, buildExporterContext(enclaveId, channelId, epoch), 32);
1428
1595
  }
1596
+ /**
1597
+ * The 1:1 voice media key: taken STRAIGHT from the MLS exporter under a
1598
+ * DEDICATED label ({@link VOICE_MEDIA_EXPORTER_LABEL}) and a context that
1599
+ * binds `callId` (voice-h2h spec, audit H-P1) — domain-separated from the
1600
+ * message key ({@link exporterKey}) and per-call, with NO HKDF layered on
1601
+ * top. Taken under the per-channel lock (as the receive-side message-key
1602
+ * read is, mls.ts) so the exporter read is epoch-stable against a concurrent
1603
+ * commit. Never transmitted or persisted; both call participants derive it
1604
+ * independently at the invite epoch.
1605
+ *
1606
+ * CONTRACT: unlike {@link exporterKey} (lock-free — its callers wrap it), this
1607
+ * takes the channel lock INTERNALLY. The lock queue is non-reentrant, so this
1608
+ * MUST NOT be called while already holding this channel's lock (doing so
1609
+ * self-deadlocks). Call it stand-alone from the call-setup path, never from
1610
+ * inside another `locks.run(channelId, ...)` for the same channel.
1611
+ */
1612
+ async voiceMediaKey(enclaveId, channelId, epoch, callId) {
1613
+ return this.locks.run(channelId, async () => {
1614
+ const mgr = await this.manager(channelId);
1615
+ return mgr.exportSecret(VOICE_MEDIA_EXPORTER_LABEL, buildVoiceExporterContext(enclaveId, channelId, epoch, callId), 32);
1616
+ });
1617
+ }
1429
1618
  /**
1430
1619
  * Join a channel from the host-committed Welcome, then drain any commits that
1431
1620
  * landed after the Welcome so we converge on the true current epoch. Without
@@ -1554,7 +1743,7 @@ var MlsGroupKeyProvider = class {
1554
1743
  const bundle = await this.o.store.loadPendingBundle(this.o.deviceId);
1555
1744
  if (!bundle)
1556
1745
  return false;
1557
- const heldHex = bytesToHex(MLSGroupManager.serializeKeyPackage(bundle.publicPackage));
1746
+ const heldHex = bytesToHex2(MLSGroupManager.serializeKeyPackage(bundle.publicPackage));
1558
1747
  const published = await this.o.backend.fetchKeyPackages([this.o.deviceId]);
1559
1748
  const pubHex = published[this.o.deviceId];
1560
1749
  return !!pubHex && pubHex === heldHex;
@@ -1570,7 +1759,7 @@ var MlsGroupKeyProvider = class {
1570
1759
  console.warn(`publishKeyPackage: minting a fresh KeyPackage (rotation) for device ${this.o.deviceId.slice(0, 8)}`);
1571
1760
  const mgr = new MLSGroupManager();
1572
1761
  const bundle = await mgr.generateKeyPackage(this.o.identity);
1573
- const hex = bytesToHex(MLSGroupManager.serializeKeyPackage(bundle.publicPackage));
1762
+ const hex = bytesToHex2(MLSGroupManager.serializeKeyPackage(bundle.publicPackage));
1574
1763
  const id = await this.o.backend.publishKeyPackage(this.o.deviceId, hex);
1575
1764
  await this.o.store.saveLastKpAndBundle(this.o.deviceId, id, bundle);
1576
1765
  return id;
@@ -1610,11 +1799,11 @@ var MlsGroupKeyProvider = class {
1610
1799
  const { commit, welcome } = await mgr.addMembers(pkgs);
1611
1800
  await this.o.backend.distribute(reinit.group_id, {
1612
1801
  device_id: this.o.deviceId,
1613
- commit: bytesToHex(commit),
1802
+ commit: bytesToHex2(commit),
1614
1803
  commit_epoch: Number(mgr.epoch),
1615
1804
  expected_epoch: reinit.current_epoch,
1616
1805
  member_device_ids: [this.o.deviceId, ...others],
1617
- welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload: bytesToHex(welcome) })) : []
1806
+ welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload: bytesToHex2(welcome) })) : []
1618
1807
  });
1619
1808
  await this.o.store.save(channelId, mgr.exportState());
1620
1809
  this.managers.set(channelId, mgr);
@@ -1637,11 +1826,11 @@ var MlsGroupKeyProvider = class {
1637
1826
  const { commit, welcome } = await mgr.addMembers(pkgs);
1638
1827
  await this.o.backend.distribute(init.group_id, {
1639
1828
  device_id: this.o.deviceId,
1640
- commit: bytesToHex(commit),
1829
+ commit: bytesToHex2(commit),
1641
1830
  commit_epoch: Number(mgr.epoch),
1642
1831
  expected_epoch: init.current_epoch,
1643
1832
  member_device_ids: [this.o.deviceId, ...others],
1644
- welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload: bytesToHex(welcome) })) : []
1833
+ welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload: bytesToHex2(welcome) })) : []
1645
1834
  });
1646
1835
  this.managers.set(channelId, mgr);
1647
1836
  await this.o.store.save(channelId, mgr.exportState());
@@ -1701,12 +1890,12 @@ var MlsGroupKeyProvider = class {
1701
1890
  try {
1702
1891
  await this.o.backend.distribute(init.group_id, {
1703
1892
  device_id: this.o.deviceId,
1704
- commit: bytesToHex(commit),
1893
+ commit: bytesToHex2(commit),
1705
1894
  commit_epoch: Number(mgr.epoch),
1706
1895
  expected_epoch: priorEpoch,
1707
1896
  member_device_ids: members,
1708
1897
  op: "add",
1709
- welcomes: welcome ? [{ target_device_id: newDeviceId, payload: bytesToHex(welcome) }] : []
1898
+ welcomes: welcome ? [{ target_device_id: newDeviceId, payload: bytesToHex2(welcome) }] : []
1710
1899
  });
1711
1900
  } catch (e) {
1712
1901
  this.managers.delete(channelId);
@@ -1764,7 +1953,7 @@ var MlsGroupKeyProvider = class {
1764
1953
  try {
1765
1954
  await this.o.backend.distribute(init.group_id, {
1766
1955
  device_id: this.o.deviceId,
1767
- commit: bytesToHex(commit),
1956
+ commit: bytesToHex2(commit),
1768
1957
  commit_epoch: Number(mgr.epoch),
1769
1958
  expected_epoch: priorEpoch,
1770
1959
  member_device_ids: members,
@@ -1832,11 +2021,11 @@ var MlsGroupKeyProvider = class {
1832
2021
  }
1833
2022
  await this.o.store.saveStaging(channelId, mgr.exportState());
1834
2023
  const members = init.member_device_ids ?? [this.o.deviceId];
1835
- const removedIdentity = typeof opts.identity === "string" ? opts.identity : bytesToHex(opts.identity);
2024
+ const removedIdentity = typeof opts.identity === "string" ? opts.identity : bytesToHex2(opts.identity);
1836
2025
  try {
1837
2026
  await this.o.backend.distribute(init.group_id, {
1838
2027
  device_id: this.o.deviceId,
1839
- commit: bytesToHex(commit),
2028
+ commit: bytesToHex2(commit),
1840
2029
  commit_epoch: Number(mgr.epoch),
1841
2030
  expected_epoch: priorEpoch,
1842
2031
  member_device_ids: members,
@@ -2067,11 +2256,11 @@ var MlsGroupKeyProvider = class {
2067
2256
  const { commit, welcome } = await mgr.addMembers(pkgs);
2068
2257
  await this.o.backend.distribute(reinit.group_id, {
2069
2258
  device_id: this.o.deviceId,
2070
- commit: bytesToHex(commit),
2259
+ commit: bytesToHex2(commit),
2071
2260
  commit_epoch: Number(mgr.epoch),
2072
2261
  expected_epoch: reinit.current_epoch,
2073
2262
  member_device_ids: [this.o.deviceId, ...others],
2074
- welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload: bytesToHex(welcome) })) : []
2263
+ welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload: bytesToHex2(welcome) })) : []
2075
2264
  });
2076
2265
  await this.o.store.save(channelId, mgr.exportState());
2077
2266
  this.managers.set(channelId, mgr);
@@ -3023,11 +3212,11 @@ var RestMlsBackend = class {
3023
3212
  };
3024
3213
 
3025
3214
  // ../connect-sdk-ts/dist/mls/fake-backend.js
3026
- import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes2 } from "@agentvault/crypto";
3215
+ import { bytesToHex as bytesToHex3, hexToBytes as hexToBytes2 } from "@agentvault/crypto";
3027
3216
 
3028
3217
  // ../connect-sdk-ts/dist/receive/inbox-cipher.js
3029
3218
  import { hkdf } from "@noble/hashes/hkdf";
3030
- import { sha256 as sha2562 } from "@noble/hashes/sha256";
3219
+ import { sha256 as sha2563 } from "@noble/hashes/sha256";
3031
3220
  var SUITE = "xchacha20poly1305";
3032
3221
  var INFO = new TextEncoder().encode("agentvault-connect-inbox-v1");
3033
3222
  var EMPTY_AAD = new Uint8Array(0);
@@ -3055,12 +3244,12 @@ var KeyedInboxCipher = class {
3055
3244
  };
3056
3245
  var MLS_STORE_INFO = new TextEncoder().encode("agentvault-connect-mls-store-v1");
3057
3246
  function deriveMlsStoreKey(identityKeyMaterial) {
3058
- return hkdf(sha2562, identityKeyMaterial, void 0, MLS_STORE_INFO, 32);
3247
+ return hkdf(sha2563, identityKeyMaterial, void 0, MLS_STORE_INFO, 32);
3059
3248
  }
3060
3249
 
3061
3250
  // ../connect-sdk-ts/dist/recovery/backup-crypto.js
3062
3251
  import { hkdf as hkdf2 } from "@noble/hashes/hkdf";
3063
- import { sha256 as sha2563 } from "@noble/hashes/sha256";
3252
+ import { sha256 as sha2564 } from "@noble/hashes/sha256";
3064
3253
  import { randomBytes as randomBytes2 } from "@noble/hashes/utils";
3065
3254
  var BACKUP_INFO = new TextEncoder().encode("connect-backup-v1");
3066
3255
  var RECOVERY_INFO = new TextEncoder().encode("connect-recovery-v1");
@@ -3068,15 +3257,15 @@ var RECOVERY_INFO = new TextEncoder().encode("connect-recovery-v1");
3068
3257
  // ../connect-sdk-ts/dist/recovery/guest-recovery-crypto.js
3069
3258
  import { argon2idAsync } from "@noble/hashes/argon2";
3070
3259
  import { hkdf as hkdf3 } from "@noble/hashes/hkdf";
3071
- import { sha256 as sha2564 } from "@noble/hashes/sha256";
3260
+ import { sha256 as sha2565 } from "@noble/hashes/sha256";
3072
3261
  var HKDF_INFO = new TextEncoder().encode("connect-guest-recovery-wrap-v1");
3073
3262
 
3074
3263
  // ../connect-mls-store/dist/sqlite-store.js
3075
3264
  import { DatabaseSync } from "node:sqlite";
3076
- import { MLSGroupManager as MLSGroupManager2, bytesToHex as bytesToHex3, hexToBytes as hexToBytes3 } from "@agentvault/crypto";
3265
+ import { MLSGroupManager as MLSGroupManager2, bytesToHex as bytesToHex4, hexToBytes as hexToBytes3 } from "@agentvault/crypto";
3077
3266
 
3078
3267
  // ../connect-mls-store/dist/schema.js
3079
- var SCHEMA_VERSION = 4;
3268
+ var SCHEMA_VERSION = 6;
3080
3269
  function openSchema(db) {
3081
3270
  db.exec("PRAGMA journal_mode=WAL");
3082
3271
  const row = db.prepare("PRAGMA user_version").get();
@@ -3123,6 +3312,51 @@ function openSchema(db) {
3123
3312
  );
3124
3313
  `);
3125
3314
  }
3315
+ if (row.user_version < 5) {
3316
+ db.exec(`
3317
+ CREATE TABLE IF NOT EXISTS channel_meta (
3318
+ identity_id TEXT NOT NULL,
3319
+ channel_id TEXT NOT NULL,
3320
+ enclave_id TEXT,
3321
+ is_dm INTEGER,
3322
+ updated_at INTEGER NOT NULL,
3323
+ PRIMARY KEY (identity_id, channel_id)
3324
+ );
3325
+ `);
3326
+ }
3327
+ if (row.user_version < 6) {
3328
+ db.exec(`
3329
+ CREATE TABLE IF NOT EXISTS agent_memory (
3330
+ identity_id TEXT NOT NULL,
3331
+ conversation_id TEXT NOT NULL,
3332
+ seq INTEGER NOT NULL,
3333
+ role TEXT NOT NULL,
3334
+ peer_id TEXT,
3335
+ sealed_text BLOB NOT NULL,
3336
+ modality TEXT NOT NULL,
3337
+ message_id TEXT,
3338
+ idempotency_key TEXT,
3339
+ dedup_key TEXT,
3340
+ recorded_at INTEGER NOT NULL,
3341
+ PRIMARY KEY (identity_id, conversation_id, seq)
3342
+ );
3343
+ `);
3344
+ db.exec(`
3345
+ CREATE UNIQUE INDEX IF NOT EXISTS agent_memory_dedup
3346
+ ON agent_memory (identity_id, conversation_id, dedup_key)
3347
+ WHERE dedup_key IS NOT NULL;
3348
+ `);
3349
+ db.exec(`
3350
+ CREATE TABLE IF NOT EXISTS agent_memory_fact (
3351
+ identity_id TEXT NOT NULL,
3352
+ conversation_id TEXT NOT NULL,
3353
+ fact_id INTEGER NOT NULL,
3354
+ sealed_fact BLOB NOT NULL,
3355
+ added_at INTEGER NOT NULL,
3356
+ PRIMARY KEY (identity_id, conversation_id, fact_id)
3357
+ );
3358
+ `);
3359
+ }
3126
3360
  db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
3127
3361
  }
3128
3362
 
@@ -3144,10 +3378,10 @@ var MlsStoreSealError = class extends Error {
3144
3378
  function encodeBundle(bundle) {
3145
3379
  const pp = bundle.privatePackage;
3146
3380
  const obj = {
3147
- pub: bytesToHex3(MLSGroupManager2.serializeKeyPackage(bundle.publicPackage)),
3148
- initPriv: bytesToHex3(pp.initPrivateKey),
3149
- hpkePriv: bytesToHex3(pp.hpkePrivateKey),
3150
- sigPriv: bytesToHex3(pp.signaturePrivateKey)
3381
+ pub: bytesToHex4(MLSGroupManager2.serializeKeyPackage(bundle.publicPackage)),
3382
+ initPriv: bytesToHex4(pp.initPrivateKey),
3383
+ hpkePriv: bytesToHex4(pp.hpkePrivateKey),
3384
+ sigPriv: bytesToHex4(pp.signaturePrivateKey)
3151
3385
  };
3152
3386
  return new TextEncoder().encode(JSON.stringify(obj));
3153
3387
  }
@@ -3174,9 +3408,11 @@ var SqliteMlsStateStore = class {
3174
3408
  // the source of truth across process restarts.
3175
3409
  bundles = /* @__PURE__ */ new Map();
3176
3410
  lastKpIds = /* @__PURE__ */ new Map();
3411
+ memoryRetentionK;
3177
3412
  constructor(opts) {
3178
3413
  this.id = opts.identityId;
3179
3414
  this.cipher = opts.cipherFor(opts.identityId);
3415
+ this.memoryRetentionK = opts.memoryRetentionK ?? 500;
3180
3416
  try {
3181
3417
  this.db = new DatabaseSync(opts.dbPath);
3182
3418
  } catch (e) {
@@ -3311,6 +3547,74 @@ var SqliteMlsStateStore = class {
3311
3547
  const rows = this.db.prepare("SELECT group_id FROM mls_retired_group WHERE channel_id=?").all(channelId);
3312
3548
  return rows.map((r) => r.group_id);
3313
3549
  }
3550
+ recordChannelEnclave(channelId, enclaveId) {
3551
+ this.db.prepare(`INSERT INTO channel_meta (identity_id, channel_id, enclave_id, updated_at)
3552
+ VALUES (?,?,?,?)
3553
+ ON CONFLICT(identity_id, channel_id) DO UPDATE SET enclave_id=excluded.enclave_id, updated_at=excluded.updated_at`).run(this.id, channelId, enclaveId, Date.now());
3554
+ }
3555
+ recordChannelIsDm(channelId, isDm) {
3556
+ this.db.prepare(`INSERT INTO channel_meta (identity_id, channel_id, is_dm, updated_at)
3557
+ VALUES (?,?,?,?)
3558
+ ON CONFLICT(identity_id, channel_id) DO UPDATE SET is_dm=excluded.is_dm, updated_at=excluded.updated_at`).run(this.id, channelId, isDm ? 1 : 0, Date.now());
3559
+ }
3560
+ getChannelMeta(channelId) {
3561
+ const row = this.db.prepare("SELECT enclave_id, is_dm FROM channel_meta WHERE identity_id=? AND channel_id=?").get(this.id, channelId);
3562
+ if (!row)
3563
+ return void 0;
3564
+ return {
3565
+ enclaveId: row.enclave_id ?? void 0,
3566
+ isDm: row.is_dm == null ? void 0 : row.is_dm === 1
3567
+ };
3568
+ }
3569
+ /**
3570
+ * The set of enclave ids this identity has learned about (from inbound
3571
+ * envelopes or a prior listChannels discovery), beyond its own home
3572
+ * enclave. Task 3: refreshChannelMeta unions this with cfg.enclaveId so a
3573
+ * cross-enclave room's is_dm gets resolved instead of staying UNKNOWN.
3574
+ */
3575
+ distinctEnclaveIds() {
3576
+ const rows = this.db.prepare("SELECT DISTINCT enclave_id FROM channel_meta WHERE identity_id=? AND enclave_id IS NOT NULL").all(this.id);
3577
+ return rows.map((r) => r.enclave_id);
3578
+ }
3579
+ async recordMemory(input) {
3580
+ const dedupKey = input.messageId ?? input.idempotencyKey ?? null;
3581
+ if (!dedupKey)
3582
+ throw new Error("recordMemory requires message_id or idempotency_key");
3583
+ const sealed = await this.cipher.seal(new TextEncoder().encode(input.text));
3584
+ const info = this.db.prepare(`INSERT OR IGNORE INTO agent_memory
3585
+ (identity_id, conversation_id, seq, role, peer_id, sealed_text, modality, message_id, idempotency_key, dedup_key, recorded_at)
3586
+ VALUES (?, ?,
3587
+ (SELECT COALESCE(MAX(seq),0)+1 FROM agent_memory WHERE identity_id=? AND conversation_id=?),
3588
+ ?, ?, ?, ?, ?, ?, ?, ?)`).run(this.id, input.conversationId, this.id, input.conversationId, input.role, input.peerId ?? null, sealed, input.modality, input.messageId ?? null, input.idempotencyKey ?? null, dedupKey, Date.now());
3589
+ if (info.changes === 0) {
3590
+ const row2 = this.db.prepare("SELECT seq FROM agent_memory WHERE identity_id=? AND conversation_id=? AND dedup_key=?").get(this.id, input.conversationId, dedupKey);
3591
+ return { seq: row2.seq, deduped: true };
3592
+ }
3593
+ const row = this.db.prepare("SELECT MAX(seq) AS seq FROM agent_memory WHERE identity_id=? AND conversation_id=?").get(this.id, input.conversationId);
3594
+ if (this.memoryRetentionK > 0) {
3595
+ this.db.prepare("DELETE FROM agent_memory WHERE identity_id=? AND conversation_id=? AND seq <= ?").run(this.id, input.conversationId, row.seq - this.memoryRetentionK);
3596
+ }
3597
+ return { seq: row.seq, deduped: false };
3598
+ }
3599
+ async memoryContext(conversationId, limit = 50) {
3600
+ const rows = this.db.prepare(`SELECT seq, role, peer_id, sealed_text, recorded_at FROM agent_memory
3601
+ WHERE identity_id=? AND conversation_id=? ORDER BY seq DESC LIMIT ?`).all(this.id, conversationId, limit);
3602
+ const recent = [];
3603
+ for (const r of rows.reverse()) {
3604
+ const text = new TextDecoder().decode(await this.cipher.open(r.sealed_text));
3605
+ recent.push({ seq: r.seq, role: r.role, peerId: r.peer_id ?? void 0, text, ts: r.recorded_at });
3606
+ }
3607
+ const factRows = this.db.prepare("SELECT sealed_fact FROM agent_memory_fact WHERE identity_id=? AND conversation_id=? ORDER BY fact_id ASC").all(this.id, conversationId);
3608
+ const facts = [];
3609
+ for (const f of factRows)
3610
+ facts.push(new TextDecoder().decode(await this.cipher.open(f.sealed_fact)));
3611
+ return { recent, facts };
3612
+ }
3613
+ async recordMemoryFact(conversationId, fact) {
3614
+ const sealed = await this.cipher.seal(new TextEncoder().encode(fact));
3615
+ this.db.prepare(`INSERT INTO agent_memory_fact (identity_id, conversation_id, fact_id, sealed_fact, added_at)
3616
+ VALUES (?, ?, (SELECT COALESCE(MAX(fact_id),0)+1 FROM agent_memory_fact WHERE identity_id=? AND conversation_id=?), ?, ?)`).run(this.id, conversationId, this.id, conversationId, sealed, Date.now());
3617
+ }
3314
3618
  /** @internal test-only */
3315
3619
  _rawMainForTest(identityId, channelId) {
3316
3620
  const row = this.db.prepare("SELECT sealed_main FROM mls_group_state WHERE identity_id=? AND channel_id=?").get(identityId, channelId);
@@ -3323,8 +3627,18 @@ var SqliteMlsStateStore = class {
3323
3627
 
3324
3628
  // src/bridge-agent.ts
3325
3629
  import { decryptFile, encryptFile, computeFileDigest, base64ToBytes as base64ToBytes2, bytesToBase64 as bytesToBase642 } from "@agentvault/crypto";
3630
+
3631
+ // src/service/resolve-bin.ts
3632
+ import { createRequire } from "node:module";
3633
+ function loadPackageMeta(importMetaUrl) {
3634
+ const require2 = createRequire(importMetaUrl);
3635
+ const pkg = require2("../package.json");
3636
+ return { name: pkg.name, version: pkg.version };
3637
+ }
3638
+
3639
+ // src/bridge-agent.ts
3326
3640
  import { promises as fsp } from "node:fs";
3327
- import { join, dirname } from "node:path";
3641
+ import { join as join5, dirname } from "node:path";
3328
3642
 
3329
3643
  // src/latency.ts
3330
3644
  var DEFAULT_CAPACITY = 1024;
@@ -3598,8 +3912,8 @@ async function prepareAttachment(filePath, caption, channelId, deps) {
3598
3912
  }
3599
3913
  async function materializeAttachment(body, ctx, deps) {
3600
3914
  const ciphertext = await deps.downloadBlob(body.blobId);
3601
- const sha2565 = deps.computeFileDigest(ciphertext);
3602
- if (sha2565 !== body.digest) {
3915
+ const sha2566 = deps.computeFileDigest(ciphertext);
3916
+ if (sha2566 !== body.digest) {
3603
3917
  throw new Error(`attachment digest mismatch for blob ${body.blobId}`);
3604
3918
  }
3605
3919
  const plain = deps.decryptFile(
@@ -3612,7 +3926,328 @@ async function materializeAttachment(body, ctx, deps) {
3612
3926
  const path = `${dir}/${ctx.messageId}-${safeName}`;
3613
3927
  await deps.mkdirp(dir);
3614
3928
  await deps.writeFile(path, plain);
3615
- return { filename: safeName, mime: body.mime, size: plain.length, path, sha256: sha2565 };
3929
+ return { filename: safeName, mime: body.mime, size: plain.length, path, sha256: sha2566 };
3930
+ }
3931
+
3932
+ // src/memory/hermes-session-map.ts
3933
+ import { readFileSync } from "node:fs";
3934
+ import { homedir } from "node:os";
3935
+ import { join } from "node:path";
3936
+ function expandHome(p) {
3937
+ return p === "~" || p.startsWith("~/") ? join(homedir(), p.slice(1)) : p;
3938
+ }
3939
+ function resolveHermesSession(baseDir, conversationId) {
3940
+ try {
3941
+ const path = join(expandHome(baseDir), "sessions", "sessions.json");
3942
+ const map = JSON.parse(readFileSync(path, "utf8"));
3943
+ const entry = map[`agent:main:securecoms:dm:${conversationId}`];
3944
+ const sid = entry?.session_id;
3945
+ return typeof sid === "string" && sid ? sid : null;
3946
+ } catch {
3947
+ return null;
3948
+ }
3949
+ }
3950
+
3951
+ // src/memory/hermes-store.ts
3952
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
3953
+ import { join as join2 } from "node:path";
3954
+ var HermesStore = class {
3955
+ dbPath;
3956
+ constructor(baseDir) {
3957
+ this.dbPath = join2(expandHome(baseDir), "state.db");
3958
+ }
3959
+ /** Recent-K conversational turns for a session, chronological. Fail-safe → []. */
3960
+ readRecent(sessionId, limit) {
3961
+ let db;
3962
+ try {
3963
+ db = new DatabaseSync2(this.dbPath, { readOnly: true });
3964
+ const rows = db.prepare(
3965
+ `SELECT id, role, content, timestamp FROM messages
3966
+ WHERE session_id = ? AND active = 1 AND content IS NOT NULL AND role IN ('user','assistant')
3967
+ ORDER BY timestamp DESC LIMIT ?`
3968
+ ).all(sessionId, limit);
3969
+ return rows.reverse().map((r) => ({ id: r.id, role: r.role, content: r.content, timestamp: r.timestamp }));
3970
+ } catch {
3971
+ return [];
3972
+ } finally {
3973
+ db?.close();
3974
+ }
3975
+ }
3976
+ open(busyMs = 5e3) {
3977
+ const db = new DatabaseSync2(this.dbPath);
3978
+ db.exec(`PRAGMA busy_timeout = ${busyMs}`);
3979
+ return db;
3980
+ }
3981
+ /**
3982
+ * Append a voice turn to a session in one synchronous BEGIN IMMEDIATE block
3983
+ * (no await between statements → atomic against Hermes and our own retries).
3984
+ * Guards: skip when the session is absent/archived/ended; in-transaction dedup
3985
+ * on (session_id, platform_message_id). Writes only `messages`. Fail-safe.
3986
+ */
3987
+ writeVoiceTurn(input) {
3988
+ let db;
3989
+ try {
3990
+ db = this.open();
3991
+ db.exec("BEGIN IMMEDIATE");
3992
+ try {
3993
+ const s = db.prepare("SELECT archived, ended_at FROM sessions WHERE id = ?").get(input.sessionId);
3994
+ if (!s || s.archived === 1 || s.ended_at != null) {
3995
+ db.exec("ROLLBACK");
3996
+ return "skipped";
3997
+ }
3998
+ if (input.dedupKey) {
3999
+ const dup = db.prepare("SELECT 1 FROM messages WHERE session_id = ? AND platform_message_id = ?").get(input.sessionId, input.dedupKey);
4000
+ if (dup) {
4001
+ db.exec("ROLLBACK");
4002
+ return "deduped";
4003
+ }
4004
+ }
4005
+ db.prepare(
4006
+ `INSERT INTO messages (session_id, role, content, timestamp, active, platform_message_id) VALUES (?, ?, ?, ?, 1, ?)`
4007
+ ).run(input.sessionId, input.role, input.content, input.timestamp, input.dedupKey);
4008
+ db.exec("COMMIT");
4009
+ return "written";
4010
+ } catch (e) {
4011
+ try {
4012
+ db.exec("ROLLBACK");
4013
+ } catch {
4014
+ }
4015
+ throw e;
4016
+ }
4017
+ } catch {
4018
+ return "failed";
4019
+ } finally {
4020
+ db?.close();
4021
+ }
4022
+ }
4023
+ /**
4024
+ * Startup write-capability probe: insert a sentinel against any existing
4025
+ * session inside a rolled-back transaction. Throws if the write path is broken
4026
+ * (e.g. a schema regression). No-op when the db has no sessions.
4027
+ */
4028
+ probeWrite() {
4029
+ let db;
4030
+ try {
4031
+ db = this.open(500);
4032
+ const s = db.prepare("SELECT id FROM sessions LIMIT 1").get();
4033
+ if (!s) return;
4034
+ db.exec("BEGIN IMMEDIATE");
4035
+ try {
4036
+ db.prepare(
4037
+ `INSERT INTO messages (session_id, role, content, timestamp, active, platform_message_id) VALUES (?, 'assistant', '[voice] __probe__', ?, 1, ?)`
4038
+ ).run(s.id, Date.now() / 1e3, `__probe__${Date.now()}`);
4039
+ } finally {
4040
+ db.exec("ROLLBACK");
4041
+ }
4042
+ } finally {
4043
+ db?.close();
4044
+ }
4045
+ }
4046
+ };
4047
+
4048
+ // src/memory/hermes-provider.ts
4049
+ var DEFAULT_K = 30;
4050
+ var HermesMemoryProvider = class {
4051
+ constructor(baseDir) {
4052
+ this.baseDir = baseDir;
4053
+ this.store = new HermesStore(baseDir);
4054
+ try {
4055
+ this.store.probeWrite();
4056
+ } catch (e) {
4057
+ process.stderr.write(`[bridge] HERMES MEMORY WRITE-BACK NON-FUNCTIONAL (schema probe failed): ${String(e)}
4058
+ `);
4059
+ }
4060
+ }
4061
+ store;
4062
+ noSession = 0;
4063
+ skipped = 0;
4064
+ writeFailed = 0;
4065
+ async record(input) {
4066
+ if (input.modality !== "voice") return { seq: 0, deduped: true };
4067
+ const sessionId = resolveHermesSession(this.baseDir, input.conversationId);
4068
+ if (!sessionId) {
4069
+ this.noSession++;
4070
+ return { seq: 0, deduped: false };
4071
+ }
4072
+ const role = input.role === "peer" ? "user" : "assistant";
4073
+ const res = this.store.writeVoiceTurn({
4074
+ sessionId,
4075
+ role,
4076
+ content: `[voice] ${input.text}`,
4077
+ dedupKey: input.idempotencyKey ?? input.messageId ?? null,
4078
+ timestamp: Date.now() / 1e3
4079
+ });
4080
+ if (res === "failed") this.writeFailed++;
4081
+ else if (res === "skipped") this.skipped++;
4082
+ return { seq: 0, deduped: res === "deduped" };
4083
+ }
4084
+ async context(conversationId, limit) {
4085
+ const sessionId = resolveHermesSession(this.baseDir, conversationId);
4086
+ if (!sessionId) return { recent: [], facts: [] };
4087
+ const turns = this.store.readRecent(sessionId, limit ?? DEFAULT_K);
4088
+ return {
4089
+ recent: turns.map((t) => ({ seq: t.id, role: t.role === "user" ? "peer" : "agent", text: t.content })),
4090
+ facts: []
4091
+ };
4092
+ }
4093
+ stats() {
4094
+ return { hermes_no_session: this.noSession, hermes_skipped: this.skipped, hermes_write_failed: this.writeFailed };
4095
+ }
4096
+ };
4097
+
4098
+ // src/memory/openclaw-session-map.ts
4099
+ import { readFileSync as readFileSync2 } from "node:fs";
4100
+ import { join as join3 } from "node:path";
4101
+ function resolveOwnerDmSession(baseDir, agent) {
4102
+ try {
4103
+ const path = join3(expandHome(baseDir), "agents", agent, "sessions", "sessions.json");
4104
+ const map = JSON.parse(readFileSync2(path, "utf8"));
4105
+ let best = null;
4106
+ for (const [key, entry] of Object.entries(map)) {
4107
+ if (!key.includes(":direct:") || !key.endsWith(":owner")) continue;
4108
+ const sid = entry?.sessionId;
4109
+ if (typeof sid !== "string" || !sid) continue;
4110
+ const updatedAt = typeof entry?.updatedAt === "number" ? entry.updatedAt : 0;
4111
+ if (!best || updatedAt > best.updatedAt) best = { sid, updatedAt };
4112
+ }
4113
+ return best?.sid ?? null;
4114
+ } catch {
4115
+ return null;
4116
+ }
4117
+ }
4118
+
4119
+ // src/memory/openclaw-session-log.ts
4120
+ import { readFileSync as readFileSync3 } from "node:fs";
4121
+ import { join as join4 } from "node:path";
4122
+ function readRecentTurns(baseDir, agent, sessionId, k) {
4123
+ let raw;
4124
+ try {
4125
+ raw = readFileSync3(join4(expandHome(baseDir), "agents", agent, "sessions", `${sessionId}.jsonl`), "utf8");
4126
+ } catch {
4127
+ return [];
4128
+ }
4129
+ const turns = [];
4130
+ for (const line of raw.split("\n")) {
4131
+ if (!line.trim()) continue;
4132
+ let o;
4133
+ try {
4134
+ o = JSON.parse(line);
4135
+ } catch {
4136
+ continue;
4137
+ }
4138
+ if (o?.type !== "message" || !o.message) continue;
4139
+ const ts = typeof o.timestamp === "string" ? Date.parse(o.timestamp) : NaN;
4140
+ if (Number.isNaN(ts)) continue;
4141
+ const role = o.message.role;
4142
+ if (role === "user") {
4143
+ const text = typeof o.message.content === "string" ? o.message.content.trim() : "";
4144
+ if (text) turns.push({ role: "peer", text, ts });
4145
+ } else if (role === "assistant") {
4146
+ const blocks = Array.isArray(o.message.content) ? o.message.content : [];
4147
+ const text = blocks.filter((b) => !!b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
4148
+ if (text) turns.push({ role: "agent", text, ts });
4149
+ }
4150
+ }
4151
+ return k > 0 ? turns.slice(-k) : [];
4152
+ }
4153
+
4154
+ // src/memory/openclaw-provider.ts
4155
+ var DEFAULT_K2 = 200;
4156
+ var OpenClawMemoryProvider = class {
4157
+ constructor(store, baseDir, agent) {
4158
+ this.store = store;
4159
+ this.baseDir = baseDir;
4160
+ this.agent = agent;
4161
+ }
4162
+ noSession = 0;
4163
+ readFailed = 0;
4164
+ async record(input) {
4165
+ if (input.modality !== "voice") return { seq: 0, deduped: true };
4166
+ return this.store.recordMemory({ ...input, text: `[voice] ${input.text}` });
4167
+ }
4168
+ async context(conversationId, limit) {
4169
+ const k = limit ?? DEFAULT_K2;
4170
+ const voice = await this.store.memoryContext(conversationId, k);
4171
+ let textTurns = [];
4172
+ try {
4173
+ const sid = resolveOwnerDmSession(this.baseDir, this.agent);
4174
+ if (!sid) {
4175
+ this.noSession++;
4176
+ return voice;
4177
+ }
4178
+ textTurns = readRecentTurns(this.baseDir, this.agent, sid, k).map((t) => ({
4179
+ seq: 0,
4180
+ role: t.role,
4181
+ text: t.text,
4182
+ ts: t.ts
4183
+ }));
4184
+ } catch {
4185
+ this.readFailed++;
4186
+ return voice;
4187
+ }
4188
+ const tagged = [
4189
+ ...textTurns.map((t, i) => ({ ...t, _src: 0, _i: i })),
4190
+ ...voice.recent.map((t, i) => ({ ...t, _src: 1, _i: i }))
4191
+ ];
4192
+ tagged.sort((a, b) => {
4193
+ const at = a.ts ?? 0, bt = b.ts ?? 0;
4194
+ if (at !== bt) return at - bt;
4195
+ if (a._src !== b._src) return a._src - b._src;
4196
+ return a._i - b._i;
4197
+ });
4198
+ const recent = (k > 0 ? tagged.slice(-k) : []).map((t, i) => ({
4199
+ seq: i + 1,
4200
+ role: t.role,
4201
+ peerId: t.peerId,
4202
+ text: t.text,
4203
+ ts: t.ts
4204
+ }));
4205
+ return { recent, facts: voice.facts };
4206
+ }
4207
+ stats() {
4208
+ return { openclaw_no_session: this.noSession, openclaw_read_failed: this.readFailed };
4209
+ }
4210
+ };
4211
+
4212
+ // src/memory/provider.ts
4213
+ var SecureComsLocalMemory = class {
4214
+ constructor(store) {
4215
+ this.store = store;
4216
+ }
4217
+ record(input) {
4218
+ return this.store.recordMemory(input);
4219
+ }
4220
+ context(conversationId, limit) {
4221
+ return this.store.memoryContext(conversationId, limit);
4222
+ }
4223
+ };
4224
+ var NullMemory = class {
4225
+ async record() {
4226
+ return { seq: 0, deduped: false };
4227
+ }
4228
+ async context() {
4229
+ return { recent: [], facts: [] };
4230
+ }
4231
+ };
4232
+ function createMemoryProvider(provider, store, baseDir, agent) {
4233
+ if (provider === "none") return new NullMemory();
4234
+ if (provider === "hermes") return new HermesMemoryProvider(baseDir ?? "~/.hermes");
4235
+ if (provider === "openclaw") return new OpenClawMemoryProvider(store, baseDir ?? "~/.openclaw", agent ?? "");
4236
+ return new SecureComsLocalMemory(store);
4237
+ }
4238
+
4239
+ // src/memory-taps.ts
4240
+ async function recordInboundMemory(sink, channelId, senderId, text, messageId) {
4241
+ try {
4242
+ await sink.record({ conversationId: channelId, role: "peer", peerId: senderId, text, modality: "text", messageId });
4243
+ } catch {
4244
+ }
4245
+ }
4246
+ async function recordOutboundMemory(sink, channelId, text, messageId) {
4247
+ try {
4248
+ await sink.record({ conversationId: channelId, role: "agent", text, modality: "text", messageId });
4249
+ } catch {
4250
+ }
3616
4251
  }
3617
4252
 
3618
4253
  // src/bridge-agent.ts
@@ -3657,7 +4292,13 @@ var UnknownActionRunError = class extends Error {
3657
4292
  this.name = "UnknownActionRunError";
3658
4293
  }
3659
4294
  };
3660
- var BRIDGE_VERSION = "0.0.0";
4295
+ var BRIDGE_VERSION = (() => {
4296
+ try {
4297
+ return loadPackageMeta(import.meta.url).version;
4298
+ } catch {
4299
+ return "0.0.0";
4300
+ }
4301
+ })();
3661
4302
  function hexToBytes4(h) {
3662
4303
  return new Uint8Array(Buffer.from(h, "hex"));
3663
4304
  }
@@ -3683,6 +4324,7 @@ var BridgeAgent = class _BridgeAgent {
3683
4324
  identityId: cfg.identityId,
3684
4325
  cipherFor: () => new KeyedInboxCipher(deriveMlsStoreKey(seed))
3685
4326
  });
4327
+ this.memory = createMemoryProvider(this.cfg.memoryProvider, this.store, this.cfg.memoryBaseDir, this.cfg.memoryAgent);
3686
4328
  this.provider = new MlsGroupKeyProvider({
3687
4329
  store: this.store,
3688
4330
  backend: this.backend,
@@ -3762,7 +4404,7 @@ var BridgeAgent = class _BridgeAgent {
3762
4404
  const j = await res.json();
3763
4405
  return String(j.attachment_id);
3764
4406
  };
3765
- this.attachmentsDirImpl = deps.attachmentsDir ?? join(dirname(cfg.storePath), "attachments");
4407
+ this.attachmentsDirImpl = deps.attachmentsDir ?? join5(dirname(cfg.storePath), "attachments");
3766
4408
  this.action = {
3767
4409
  begin: (channelId) => this.actionBegin(channelId),
3768
4410
  start: (opts) => this.actionStart(opts),
@@ -3934,6 +4576,13 @@ var BridgeAgent = class _BridgeAgent {
3934
4576
  listeners = /* @__PURE__ */ new Set();
3935
4577
  startedAt = 0;
3936
4578
  store;
4579
+ memory;
4580
+ /** Task 2: warn-once-per-channel guard for the enclave-map fallback (below). */
4581
+ enclaveFallbackWarned = /* @__PURE__ */ new Set();
4582
+ /** Task 2: dedupe guard so ingestOnce writes a channel's enclave at most once
4583
+ * per process — a channel's enclave is immutable, so no need to UPSERT on
4584
+ * every message on the hot receive path (audit M2). */
4585
+ channelEnclaveRecorded = /* @__PURE__ */ new Set();
3937
4586
  latency;
3938
4587
  /** Track 5: injectable blob download fn (default: fetch-based). */
3939
4588
  downloadBlobImpl;
@@ -3973,6 +4622,26 @@ var BridgeAgent = class _BridgeAgent {
3973
4622
  sweepJitter() {
3974
4623
  return Math.floor(this.rng() * _BridgeAgent.SELF_HEAL_SWEEP_JITTER_MS);
3975
4624
  }
4625
+ /**
4626
+ * Task 2 (multi-enclave core fix): resolve the enclave to stamp on outbound
4627
+ * traffic for `channelId`. The daemon previously stamped `cfg.enclaveId`
4628
+ * (its "home" enclave) on EVERY channel, so an agent admitted to a second
4629
+ * enclave got a backend 400 on every send/reply there. The channel→enclave
4630
+ * map (`channel_meta`, Task 1) is fed by inbound envelopes (`ingestOnce`)
4631
+ * and by `listChannels`; a channel not yet in the map (never seen inbound,
4632
+ * never discovered) falls back to home and logs once so the fallback is
4633
+ * diagnosable (audit H3) rather than silently wrong.
4634
+ */
4635
+ resolveEnclave(channelId) {
4636
+ const mapped = this.store.getChannelMeta(channelId)?.enclaveId;
4637
+ if (mapped) return mapped;
4638
+ if (!this.enclaveFallbackWarned.has(channelId)) {
4639
+ this.enclaveFallbackWarned.add(channelId);
4640
+ process.stderr.write(`[bridge] send.enclave_fallback channel=${channelId} stamped=home
4641
+ `);
4642
+ }
4643
+ return this.cfg.enclaveId;
4644
+ }
3976
4645
  async reconcileJoin(channelId) {
3977
4646
  if (this.joinState.get(channelId) === "JOINED") return true;
3978
4647
  try {
@@ -4186,7 +4855,7 @@ var BridgeAgent = class _BridgeAgent {
4186
4855
  groupKey: this.provider,
4187
4856
  messageId: id,
4188
4857
  channelId,
4189
- enclaveId: this.cfg.enclaveId,
4858
+ enclaveId: this.resolveEnclave(channelId),
4190
4859
  senderId: this.cfg.identityId,
4191
4860
  senderType: "agent",
4192
4861
  type: "text",
@@ -4204,6 +4873,7 @@ var BridgeAgent = class _BridgeAgent {
4204
4873
  (envelope) => this.latency.time("send.publish", () => this.transport.publish(envelope))
4205
4874
  );
4206
4875
  this.latency.record("send.total", performance.now() - sendStart);
4876
+ void recordOutboundMemory(this.memory, channelId, text, id);
4207
4877
  return { message_id: id, status: "sent" };
4208
4878
  }
4209
4879
  async sendA2A(channelId, a2a, messageId) {
@@ -4222,7 +4892,7 @@ var BridgeAgent = class _BridgeAgent {
4222
4892
  groupKey: this.provider,
4223
4893
  messageId: id,
4224
4894
  channelId,
4225
- enclaveId: this.cfg.enclaveId,
4895
+ enclaveId: this.resolveEnclave(channelId),
4226
4896
  senderId: this.cfg.identityId,
4227
4897
  senderType: "agent",
4228
4898
  type: "a2a",
@@ -4275,7 +4945,7 @@ var BridgeAgent = class _BridgeAgent {
4275
4945
  groupKey: this.provider,
4276
4946
  messageId: id,
4277
4947
  channelId,
4278
- enclaveId: this.cfg.enclaveId,
4948
+ enclaveId: this.resolveEnclave(channelId),
4279
4949
  senderId: this.cfg.identityId,
4280
4950
  senderType: "agent",
4281
4951
  type: "artifact_share",
@@ -4295,42 +4965,91 @@ var BridgeAgent = class _BridgeAgent {
4295
4965
  this.latency.record("send.total", performance.now() - sendStart);
4296
4966
  return { message_id: id, status: "sent" };
4297
4967
  }
4298
- async listChannels() {
4299
- const url = `${this.cfg.apiBase}/api/v1/enclaves/${this.cfg.enclaveId}/channels?caller_identity_id=${encodeURIComponent(this.cfg.identityId)}`;
4968
+ /** Task 3: delegates to the configured MemoryProvider (SecureComsLocal | Null). */
4969
+ async recordMemory(input) {
4970
+ return this.memory.record(input);
4971
+ }
4972
+ async memoryContext(conversationId, limit) {
4973
+ return this.memory.context(conversationId, limit);
4974
+ }
4975
+ /**
4976
+ * List channels in one enclave (default: the daemon's own home enclave).
4977
+ * Task 3: called per-*known* enclave by refreshChannelMeta so a
4978
+ * cross-enclave room's is_dm gets resolved — see the scope caveat there
4979
+ * (same-tenant only; a cross-tenant enclave 404s under the agent's bearer).
4980
+ */
4981
+ async listChannels(enclaveId = this.cfg.enclaveId) {
4982
+ const url = `${this.cfg.apiBase}/api/v1/enclaves/${enclaveId}/channels?caller_identity_id=${encodeURIComponent(this.cfg.identityId)}`;
4300
4983
  const res = await this.fetchImpl(url, {
4301
4984
  headers: { authorization: `Bearer ${this.cfg.apiKey}` }
4302
4985
  });
4303
4986
  if (!res.ok) throw new Error(`list_channels failed: ${res.status}`);
4304
4987
  const rows = await res.json();
4988
+ for (const r of rows) this.store.recordChannelEnclave(r.channel_id, r.enclave_id);
4305
4989
  return rows.map((r) => ({
4306
4990
  channel_id: r.channel_id,
4307
4991
  name: r.name ?? null,
4308
4992
  is_dm: r.is_dm,
4309
- joined: true
4993
+ joined: true,
4994
+ enclave_id: r.enclave_id
4310
4995
  }));
4311
4996
  }
4312
- /** A live channelMeta entry, or undefined if missing OR expired past the TTL. */
4997
+ /**
4998
+ * A live channelMeta entry, or undefined if missing OR expired past the
4999
+ * TTL. A never-cached-this-process channel (no in-memory entry at all —
5000
+ * cold start, or every listChannels() call in refreshChannelMeta failed,
5001
+ * e.g. network down) falls back to the store's persisted is_dm (Task 3) so
5002
+ * a cross-enclave room stays classified across a restart instead of
5003
+ * reverting to UNKNOWN/fail-open. The store has no TTL (is_dm is immutable
5004
+ * per channel); `name` is not persisted so it comes back undefined on this
5005
+ * path and re-resolves once a listChannels() call succeeds.
5006
+ *
5007
+ * A TTL-*expired* in-memory entry deliberately does NOT use the store
5008
+ * fallback and is still treated as a miss — that miss is what drives
5009
+ * ingestOnce to retry a refresh each cycle (S-2: keeping `name` fresh after
5010
+ * a rename); serving the persisted is_dm there would silently suppress
5011
+ * that retry forever.
5012
+ */
4313
5013
  freshMeta(channelId) {
4314
5014
  const hit = this.channelMeta.get(channelId);
4315
- if (!hit) return void 0;
4316
- if (Date.now() - hit.at >= CHANNEL_META_TTL_MS) return void 0;
4317
- return { is_dm: hit.is_dm, name: hit.name };
5015
+ if (hit) {
5016
+ if (Date.now() - hit.at < CHANNEL_META_TTL_MS) return { is_dm: hit.is_dm, name: hit.name };
5017
+ return void 0;
5018
+ }
5019
+ const persisted = this.store.getChannelMeta(channelId);
5020
+ if (persisted?.isDm !== void 0) {
5021
+ return { is_dm: persisted.isDm, name: void 0 };
5022
+ }
5023
+ return void 0;
4318
5024
  }
4319
5025
  /**
4320
- * One-shot, whole-listing refresh of channelMeta. Called AT MOST ONCE per
4321
- * ingest cycle (the caller guards with a cycle-local "refreshed" flag), so a
4322
- * burst of K unknown channels fires a single listChannels() instead of K. Any
4323
- * listChannels failure is swallowed (UNKNOWN → fail open) so ingest never
4324
- * blocks on the network.
5026
+ * Whole-listing refresh of channelMeta, one listChannels() call per *known*
5027
+ * enclave — the daemon's own home enclave plus every enclave it has learned
5028
+ * about from inbound envelopes or a prior discovery (store.distinctEnclaveIds()).
5029
+ * Called AT MOST ONCE per ingest cycle (the caller guards with a
5030
+ * cycle-local "refreshed" flag). Task 3 (M1): without this, a cross-enclave
5031
+ * room is never listed under the home-enclave-only call, so its is_dm never
5032
+ * resolves and the @mention filter fails open for that room.
5033
+ *
5034
+ * Each enclave is wrapped in its own try/catch (scope caveat — audit
5035
+ * H2/tenant): list_channels_in_enclave filters on tenant_id, so a
5036
+ * same-tenant enclave the agent has learned about resolves fine, but a
5037
+ * cross-TENANT federated-partner enclave 404s under this agent's bearer;
5038
+ * that enclave's channels are left unresolved (fail open) without aborting
5039
+ * the other enclaves in the set.
4325
5040
  */
4326
5041
  async refreshChannelMeta() {
4327
- try {
4328
- const now = Date.now();
4329
- const channels = await this.listChannels();
4330
- for (const c of channels) {
4331
- this.channelMeta.set(c.channel_id, { is_dm: c.is_dm, name: c.name, at: now });
5042
+ const enclaves = /* @__PURE__ */ new Set([this.cfg.enclaveId, ...this.store.distinctEnclaveIds()]);
5043
+ const now = Date.now();
5044
+ for (const encId of enclaves) {
5045
+ try {
5046
+ const channels = await this.listChannels(encId);
5047
+ for (const c of channels) {
5048
+ this.channelMeta.set(c.channel_id, { is_dm: c.is_dm, name: c.name, at: now });
5049
+ this.store.recordChannelIsDm(c.channel_id, c.is_dm);
5050
+ }
5051
+ } catch {
4332
5052
  }
4333
- } catch {
4334
5053
  }
4335
5054
  }
4336
5055
  /** A live roster for a channel, or undefined if missing OR expired past the TTL. */
@@ -4436,6 +5155,8 @@ var BridgeAgent = class _BridgeAgent {
4436
5155
  async reportStatus(channelId, state, detail) {
4437
5156
  const joined = await this.reconcileJoin(channelId);
4438
5157
  if (!joined) throw new ChannelPendingError(channelId);
5158
+ const enc = this.store.getChannelMeta(channelId)?.enclaveId;
5159
+ if (!enc) return;
4439
5160
  await sendWithSelfHeal(
4440
5161
  this.provider,
4441
5162
  channelId,
@@ -4444,7 +5165,7 @@ var BridgeAgent = class _BridgeAgent {
4444
5165
  groupKey: this.provider,
4445
5166
  messageId: crypto.randomUUID(),
4446
5167
  channelId,
4447
- enclaveId: this.cfg.enclaveId,
5168
+ enclaveId: enc,
4448
5169
  senderId: this.cfg.identityId,
4449
5170
  senderType: "agent",
4450
5171
  type: "system_event",
@@ -4491,7 +5212,7 @@ var BridgeAgent = class _BridgeAgent {
4491
5212
  groupKey: this.provider,
4492
5213
  messageId: crypto.randomUUID(),
4493
5214
  channelId,
4494
- enclaveId: this.cfg.enclaveId,
5215
+ enclaveId: this.resolveEnclave(channelId),
4495
5216
  senderId: this.cfg.identityId,
4496
5217
  senderType: "agent",
4497
5218
  type: "system_event",
@@ -4735,6 +5456,11 @@ var BridgeAgent = class _BridgeAgent {
4735
5456
  const acked = [];
4736
5457
  for (const m of entries) {
4737
5458
  try {
5459
+ const encId = m.envelope.enclave_id;
5460
+ if (encId && !this.channelEnclaveRecorded.has(m.channelId)) {
5461
+ this.channelEnclaveRecorded.add(m.channelId);
5462
+ this.store.recordChannelEnclave(m.channelId, encId);
5463
+ }
4738
5464
  const parsed = JSON.parse(
4739
5465
  new TextDecoder().decode(m.plaintext)
4740
5466
  );
@@ -4806,6 +5532,7 @@ var BridgeAgent = class _BridgeAgent {
4806
5532
  sender_is_agent: senderIsAgent
4807
5533
  });
4808
5534
  this.capBuffer();
5535
+ void recordInboundMemory(this.memory, m.channelId, m.envelope.sender_id, parsed.text, m.messageId);
4809
5536
  }
4810
5537
  }
4811
5538
  acked.push(m.queueId);
@@ -4902,7 +5629,8 @@ var BridgeAgent = class _BridgeAgent {
4902
5629
  failed_attachments: this.failedAttachments,
4903
5630
  delivery_pings: this.pingsReceived,
4904
5631
  uptime_s: this.startedAt ? Math.floor((Date.now() - this.startedAt) / 1e3) : 0,
4905
- version: BRIDGE_VERSION
5632
+ version: BRIDGE_VERSION,
5633
+ ...this.memory.stats?.() ?? {}
4906
5634
  };
4907
5635
  }
4908
5636
  /**
@@ -4957,11 +5685,11 @@ var BridgeAgent = class _BridgeAgent {
4957
5685
  };
4958
5686
 
4959
5687
  // src/config.ts
4960
- import { readFileSync, statSync } from "node:fs";
4961
- import { dirname as dirname2, join as join2 } from "node:path";
5688
+ import { readFileSync as readFileSync4, statSync } from "node:fs";
5689
+ import { dirname as dirname2, join as join6 } from "node:path";
4962
5690
  var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
4963
5691
  function loadConfig(opts) {
4964
- const raw = readFileSync(opts.credentialPath, "utf8");
5692
+ const raw = readFileSync4(opts.credentialPath, "utf8");
4965
5693
  try {
4966
5694
  const mode = statSync(opts.credentialPath).mode & 511;
4967
5695
  if (mode & 63) process.stderr.write(`[bridge] WARNING: credential file ${opts.credentialPath} permission ${mode.toString(8)} is not 0600 \u2014 tighten it (chmod 600).
@@ -4975,10 +5703,30 @@ function loadConfig(opts) {
4975
5703
  throw new Error("credential file is not valid JSON");
4976
5704
  }
4977
5705
  for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new Error(`credential file missing required field: ${k}`);
4978
- const storePath = opts.storePath ?? join2(dirname2(opts.credentialPath), "bridge-state.db");
5706
+ const storePath = opts.storePath ?? join6(dirname2(opts.credentialPath), "bridge-state.db");
4979
5707
  const agentName = typeof json2.agent_name === "string" && json2.agent_name ? json2.agent_name : typeof json2.display_name === "string" && json2.display_name ? json2.display_name : void 0;
4980
5708
  const rateLimitCapacity = typeof json2.rate_limit_capacity === "number" ? json2.rate_limit_capacity : 20;
4981
5709
  const rateLimitRefillPerMinute = typeof json2.rate_limit_refill_per_minute === "number" ? json2.rate_limit_refill_per_minute : 20;
5710
+ const memProvRaw = json2.memory?.provider;
5711
+ let memoryProvider;
5712
+ let memoryBaseDir;
5713
+ let memoryAgent;
5714
+ if (memProvRaw === "none") {
5715
+ memoryProvider = "none";
5716
+ } else if (memProvRaw === "hermes") {
5717
+ memoryProvider = "hermes";
5718
+ const bd = json2.memory?.base_dir;
5719
+ memoryBaseDir = typeof bd === "string" && bd ? bd : "~/.hermes";
5720
+ } else if (memProvRaw === "openclaw") {
5721
+ memoryProvider = "openclaw";
5722
+ const oc = json2.memory?.openclaw;
5723
+ const bd = oc?.base_dir;
5724
+ memoryBaseDir = typeof bd === "string" && bd ? bd : "~/.openclaw";
5725
+ const ag = oc?.agent;
5726
+ memoryAgent = typeof ag === "string" && ag ? ag : void 0;
5727
+ } else {
5728
+ memoryProvider = "securecoms";
5729
+ }
4982
5730
  return {
4983
5731
  apiKey: json2.api_key,
4984
5732
  identityId: json2.identity_id,
@@ -4989,7 +5737,10 @@ function loadConfig(opts) {
4989
5737
  storePath,
4990
5738
  agentName,
4991
5739
  rateLimitCapacity,
4992
- rateLimitRefillPerMinute
5740
+ rateLimitRefillPerMinute,
5741
+ memoryProvider,
5742
+ memoryBaseDir,
5743
+ memoryAgent
4993
5744
  };
4994
5745
  }
4995
5746
  function arg(argv, flag) {
@@ -5033,11 +5784,11 @@ import { z } from "zod";
5033
5784
 
5034
5785
  // src/auth.ts
5035
5786
  import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
5036
- import { existsSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
5787
+ import { existsSync, readFileSync as readFileSync5, writeFileSync } from "node:fs";
5037
5788
  function resolveToken(opts) {
5038
5789
  if (opts.token) return opts.token;
5039
5790
  const f = opts.tokenFile;
5040
- if (f && existsSync(f)) return readFileSync2(f, "utf8").trim();
5791
+ if (f && existsSync(f)) return readFileSync5(f, "utf8").trim();
5041
5792
  const tok = randomBytes3(32).toString("base64url");
5042
5793
  if (f) writeFileSync(f, tok, { mode: 384 });
5043
5794
  return tok;
@@ -5147,6 +5898,17 @@ var statusSchema = z.object({
5147
5898
  state: z.enum(["idle", "working", "stalled", "error"]),
5148
5899
  detail: z.string().optional()
5149
5900
  });
5901
+ var memoryRecordSchema = z.object({
5902
+ conversation_id: z.string().min(1),
5903
+ role: z.enum(["peer", "agent"]),
5904
+ peer_id: z.string().optional(),
5905
+ text: z.string(),
5906
+ modality: z.enum(["text", "voice"]),
5907
+ message_id: z.string().optional(),
5908
+ idempotency_key: z.string().optional()
5909
+ }).refine((d) => Boolean(d.message_id || d.idempotency_key), {
5910
+ message: "memory record requires message_id or idempotency_key"
5911
+ });
5150
5912
  function json(res, status, body) {
5151
5913
  const s = JSON.stringify(body);
5152
5914
  res.writeHead(status, { "content-type": "application/json" });
@@ -5240,6 +6002,26 @@ function createHttpServer(agent, opts) {
5240
6002
  await agent.reportStatus(parsed.data.channel_id, parsed.data.state, parsed.data.detail);
5241
6003
  return json(res, 200, { ok: true });
5242
6004
  }
6005
+ if (method === "POST" && path === "/v1/memory/record") {
6006
+ const parsed = memoryRecordSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
6007
+ if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
6008
+ const r = await agent.recordMemory({
6009
+ conversationId: parsed.data.conversation_id,
6010
+ role: parsed.data.role,
6011
+ peerId: parsed.data.peer_id,
6012
+ text: parsed.data.text,
6013
+ modality: parsed.data.modality,
6014
+ messageId: parsed.data.message_id,
6015
+ idempotencyKey: parsed.data.idempotency_key
6016
+ });
6017
+ return json(res, 200, r);
6018
+ }
6019
+ if (method === "GET" && path === "/v1/memory/context") {
6020
+ const conv = url.searchParams.get("conversation_id");
6021
+ if (!conv) return jsonError(res, "invalid_request", "conversation_id required");
6022
+ const limit = Math.max(1, Math.min(200, Number(url.searchParams.get("limit") ?? 50)));
6023
+ return json(res, 200, await agent.memoryContext(conv, limit));
6024
+ }
5243
6025
  if (method === "GET" && path === "/v1/messages") {
5244
6026
  const channel = url.searchParams.get("channel_id") ?? void 0;
5245
6027
  const wait = Math.max(0, Math.min(50, Number(url.searchParams.get("wait") ?? 0)));