@securecoms/agent-bridge 0.32.0 → 0.33.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
@@ -15433,6 +15433,18 @@ async function sendWithSelfHeal(session, channelId, build, publish) {
15433
15433
  // ../connect-sdk-ts/dist/groupkey/stub.js
15434
15434
  import { sha256 as sha2563 } from "@noble/hashes/sha2";
15435
15435
 
15436
+ // ../connect-sdk-ts/dist/receive/inbox.js
15437
+ function parseOpenedAt(v) {
15438
+ if (typeof v !== "object" || v === null)
15439
+ return void 0;
15440
+ const o = v;
15441
+ if (typeof o.groupId !== "string" || o.groupId.length === 0)
15442
+ return void 0;
15443
+ if (typeof o.epoch !== "number" || !Number.isSafeInteger(o.epoch) || o.epoch < 0)
15444
+ return void 0;
15445
+ return { groupId: o.groupId, epoch: o.epoch };
15446
+ }
15447
+
15436
15448
  // ../connect-sdk-ts/dist/spans/names.js
15437
15449
  var SPAN = {
15438
15450
  MESSAGE_SENT: "agentvault.connect.message.sent",
@@ -18161,7 +18173,9 @@ function decodeCustodyEntry(bytes) {
18161
18173
  if (typeof plaintextHex !== "string" || typeof rest.queueId !== "string" || typeof rest.channelId !== "string" || typeof rest.custodiedAt !== "number") {
18162
18174
  throw new Error("malformed custody entry");
18163
18175
  }
18164
- return { ...rest, plaintext: hexToBytes22(plaintextHex) };
18176
+ const { openedAt, ...others } = rest;
18177
+ const position = parseOpenedAt(openedAt);
18178
+ return { ...others, ...position ? { openedAt: position } : {}, plaintext: hexToBytes22(plaintextHex) };
18165
18179
  }
18166
18180
  function custodyAad(identityId, queueId) {
18167
18181
  return new TextEncoder().encode(`securecoms-custody-v1\0${identityId}\0${queueId}`);
@@ -22814,7 +22828,7 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
22814
22828
  * read #750 made in here is gone (ruling §8.2).
22815
22829
  */
22816
22830
  async trySupersede(channelId, row, callerHeldGroupId, owner, mode, pullRows) {
22817
- const refuse2 = (msg) => {
22831
+ const refuse3 = (msg) => {
22818
22832
  this.recordDecryptError(row, new Error(msg));
22819
22833
  const now = Date.now();
22820
22834
  for (const [k, v] of this.supersedeRefusals) {
@@ -22847,34 +22861,34 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
22847
22861
  att = await this.o.backend.fetchResetAttestation(row.group_id);
22848
22862
  }
22849
22863
  if (!att) {
22850
- return refuse2(`supersede refused: no attestation for group ${row.group_id}`);
22864
+ return refuse3(`supersede refused: no attestation for group ${row.group_id}`);
22851
22865
  }
22852
22866
  if (att.signerIdentityId !== owner.identityId) {
22853
- return refuse2("supersede refused: signer is not the pinned owner");
22867
+ return refuse3("supersede refused: signer is not the pinned owner");
22854
22868
  }
22855
22869
  let content;
22856
22870
  try {
22857
22871
  content = JSON.parse(att.signedContent);
22858
22872
  } catch {
22859
- return refuse2("supersede refused: unparseable signed_content");
22873
+ return refuse3("supersede refused: unparseable signed_content");
22860
22874
  }
22861
22875
  if (content.channel_id !== row.connect_channel_id || content.new_group_id !== row.group_id) {
22862
- return refuse2("supersede refused: attestation channel/group mismatch");
22876
+ return refuse3("supersede refused: attestation channel/group mismatch");
22863
22877
  }
22864
22878
  if (content.purpose !== RESET_SUPERSEDE_PURPOSE) {
22865
- return refuse2("supersede refused: attestation purpose is not connect-reset-supersede");
22879
+ return refuse3("supersede refused: attestation purpose is not connect-reset-supersede");
22866
22880
  }
22867
22881
  const targetCreatedMs = signedCreatedAtMs(content);
22868
22882
  if (targetCreatedMs === null) {
22869
- return refuse2("supersede refused: unparseable attestation created_at");
22883
+ return refuse3("supersede refused: unparseable attestation created_at");
22870
22884
  }
22871
22885
  const targetPrev = signedPrevLink(content, row.group_id);
22872
22886
  if (targetPrev === null) {
22873
- return refuse2("supersede refused: unparseable attestation prev_group_id");
22887
+ return refuse3("supersede refused: unparseable attestation prev_group_id");
22874
22888
  }
22875
22889
  const ok = await verifyApprovalArtifact({ content, signature: att.signatureHex, signerPublicKey: owner.publicKeyHex, createdAt: "", version: 1 }, owner.publicKeyHex);
22876
22890
  if (!ok) {
22877
- return refuse2("supersede refused: signature does not verify against pinned owner key");
22891
+ return refuse3("supersede refused: signature does not verify against pinned owner key");
22878
22892
  }
22879
22893
  const targetLink = targetPrev.chained ? { createdAtMs: targetCreatedMs, chained: true, prevGroupId: targetPrev.prev } : { createdAtMs: targetCreatedMs, chained: false, prevGroupId: null };
22880
22894
  if (callerHeldGroupId !== null && callerHeldGroupId !== row.group_id) {
@@ -22884,7 +22898,7 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
22884
22898
  if (v.kind === "refuse") {
22885
22899
  if (v.notNewer)
22886
22900
  this.supersedeNotNewer.add(`${channelId}:${row.group_id}`);
22887
- return refuse2(v.reason);
22901
+ return refuse3(v.reason);
22888
22902
  }
22889
22903
  }
22890
22904
  const bundle = await this.o.store.loadPendingBundle(this.o.deviceId);
@@ -22892,7 +22906,7 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
22892
22906
  if (!bundle && !supersedeSlot) {
22893
22907
  if (mode === "adopt")
22894
22908
  return { kind: "no_bundle" };
22895
- return refuse2("supersede refused: no pending KeyPackage bundle to open the new Welcome");
22909
+ return refuse3("supersede refused: no pending KeyPackage bundle to open the new Welcome");
22896
22910
  }
22897
22911
  return await this.locks.run(channelId, async () => {
22898
22912
  let inLockHeld;
@@ -22939,7 +22953,7 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
22939
22953
  return { kind: "unopenable", requested };
22940
22954
  }
22941
22955
  if (new TextDecoder().decode(mgr.groupId) !== row.group_id) {
22942
- return refuse2("supersede refused: post-join group id != attested new_group_id");
22956
+ return refuse3("supersede refused: post-join group id != attested new_group_id");
22943
22957
  }
22944
22958
  let dead = null;
22945
22959
  try {
@@ -24064,15 +24078,20 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
24064
24078
  done();
24065
24079
  };
24066
24080
  };
24081
+ const fromManager = async (mgr) => {
24082
+ const openedAt = { groupId: new TextDecoder().decode(mgr.groupId), epoch: Number(mgr.epoch) };
24083
+ const key = await mgr.exportSecret(LABEL, buildExporterContext(envelope.enclave_id, envelope.channel_id, envelope.mls_group_epoch), 32);
24084
+ return { key, openedAt };
24085
+ };
24067
24086
  const derive = keyFrom ? () => {
24068
24087
  takeInFlight();
24069
- return keyFrom.exportSecret(LABEL, buildExporterContext(envelope.enclave_id, envelope.channel_id, envelope.mls_group_epoch), 32);
24070
- } : () => {
24088
+ return fromManager(keyFrom);
24089
+ } : async () => {
24071
24090
  takeInFlight();
24072
- return this.exporterKeyUnfenced(envelope.enclave_id, envelope.channel_id, envelope.mls_group_epoch);
24091
+ return fromManager(await this.manager(envelope.channel_id));
24073
24092
  };
24074
24093
  try {
24075
- const key = opts.lockHeld ? await derive() : await this.locks.run(envelope.channel_id, derive);
24094
+ const { key, openedAt } = opts.lockHeld ? await derive() : await this.locks.run(envelope.channel_id, derive);
24076
24095
  const aad = await buildAssociatedData({
24077
24096
  message_id: envelope.message_id,
24078
24097
  channel_id: envelope.channel_id,
@@ -24096,7 +24115,8 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
24096
24115
  voiceMode: row.voice_mode ?? void 0,
24097
24116
  voiceDefaultResponder: row.voice_default_responder ?? void 0,
24098
24117
  ...typeof row.identity_signature === "string" && row.identity_signature ? { identitySignature: row.identity_signature } : {},
24099
- payloadHex: row.payload
24118
+ payloadHex: row.payload,
24119
+ openedAt
24100
24120
  };
24101
24121
  const custodiedAt = this.nowMs();
24102
24122
  this.custody.set(row.queue_id, entry);
@@ -26830,6 +26850,7 @@ function openSchema(db) {
26830
26850
  ensureToolGrantTables(db);
26831
26851
  ensureToolGrantAppliedTable(db);
26832
26852
  ensureRoomSearchCapTable(db);
26853
+ ensureVoiceChannelParentTable(db);
26833
26854
  db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
26834
26855
  }
26835
26856
  function ensureMlsCustody(db) {
@@ -27015,6 +27036,16 @@ function ensureToolGrantAppliedTable(db) {
27015
27036
  );
27016
27037
  `);
27017
27038
  }
27039
+ function ensureVoiceChannelParentTable(db) {
27040
+ db.exec(`
27041
+ CREATE TABLE IF NOT EXISTS voice_channel_parent (
27042
+ identity_id TEXT NOT NULL,
27043
+ voice_channel_id TEXT NOT NULL,
27044
+ parent_channel_id TEXT NOT NULL,
27045
+ PRIMARY KEY (identity_id, voice_channel_id)
27046
+ );
27047
+ `);
27048
+ }
27018
27049
  function ensureRoomSearchCapTable(db) {
27019
27050
  db.exec(`
27020
27051
  CREATE TABLE IF NOT EXISTS room_search_cap (
@@ -27706,6 +27737,21 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
27706
27737
  VALUES (?,?,?,?)
27707
27738
  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());
27708
27739
  }
27740
+ /** ADR-0024 Task 20 (final review I-1): records V -> P once; a later, different parent is ignored (immutable link). */
27741
+ recordVoiceChannelParent(voiceChannelId, parentChannelId) {
27742
+ this.db.prepare(`INSERT INTO voice_channel_parent (identity_id, voice_channel_id, parent_channel_id) VALUES (?,?,?)
27743
+ ON CONFLICT(identity_id, voice_channel_id) DO NOTHING`).run(this.id, voiceChannelId, parentChannelId);
27744
+ }
27745
+ /** The recorded parent of a voice channel, if any. */
27746
+ voiceChannelParent(voiceChannelId) {
27747
+ const row = this.db.prepare("SELECT parent_channel_id FROM voice_channel_parent WHERE identity_id=? AND voice_channel_id=?").get(this.id, voiceChannelId);
27748
+ return row?.parent_channel_id;
27749
+ }
27750
+ /** Every recorded voice channel of a parent conversation. */
27751
+ voiceChannelsOf(parentChannelId) {
27752
+ const rows = this.db.prepare("SELECT voice_channel_id FROM voice_channel_parent WHERE identity_id=? AND parent_channel_id=?").all(this.id, parentChannelId);
27753
+ return rows.map((r) => r.voice_channel_id);
27754
+ }
27709
27755
  getChannelMeta(channelId) {
27710
27756
  const row = this.db.prepare("SELECT enclave_id, is_dm FROM channel_meta WHERE identity_id=? AND channel_id=?").get(this.id, channelId);
27711
27757
  if (!row)
@@ -32410,7 +32456,7 @@ var EmbedderClient = class {
32410
32456
  resolve4(ok);
32411
32457
  }
32412
32458
  }
32413
- const refuse2 = (reason) => {
32459
+ const refuse3 = (reason) => {
32414
32460
  s.destroy();
32415
32461
  if (!settled) {
32416
32462
  this.fail(reason);
@@ -32429,22 +32475,22 @@ var EmbedderClient = class {
32429
32475
  return;
32430
32476
  }
32431
32477
  const f2 = this.chan.open(line);
32432
- if (!f2) return refuse2("embedder_unavailable");
32478
+ if (!f2) return refuse3("embedder_unavailable");
32433
32479
  this.onFrame(f2);
32434
32480
  return;
32435
32481
  }
32436
32482
  const f = JSON.parse(line);
32437
- if (!f || typeof f !== "object") return refuse2("embedder_unavailable");
32483
+ if (!f || typeof f !== "object") return refuse3("embedder_unavailable");
32438
32484
  if (!ns) {
32439
- if (f.t !== "challenge" || f.protocol !== EMBEDDER_PROTOCOL || typeof f.ns !== "string" || !/^[0-9a-f]{32}$/.test(f.ns)) return refuse2(f.t === "challenge" ? "embedder_mismatch" : "embedder_unavailable");
32485
+ if (f.t !== "challenge" || f.protocol !== EMBEDDER_PROTOCOL || typeof f.ns !== "string" || !/^[0-9a-f]{32}$/.test(f.ns)) return refuse3(f.t === "challenge" ? "embedder_mismatch" : "embedder_unavailable");
32440
32486
  ns = f.ns;
32441
32487
  s.write(JSON.stringify({ t: "hello", nc, mac_c: macClient(token, ns, nc) }) + "\n");
32442
32488
  return;
32443
32489
  }
32444
- if (f.t !== "hello" || typeof f.mac_s !== "string" || !macEqual(f.mac_s, macServer(token, ns, nc))) return refuse2("embedder_unavailable");
32490
+ if (f.t !== "hello" || typeof f.mac_s !== "string" || !macEqual(f.mac_s, macServer(token, ns, nc))) return refuse3("embedder_unavailable");
32445
32491
  const model = this.m.files.find((x) => x.name === "model.onnx");
32446
32492
  const ok = f.protocol === EMBEDDER_PROTOCOL && f.model_id === this.m.modelId && f.model_sha256 === model.sha256 && f.wasm_sha256 === this.m.wasmSha256 && f.dims === this.m.dims;
32447
- if (!ok) return refuse2("embedder_mismatch");
32493
+ if (!ok) return refuse3("embedder_mismatch");
32448
32494
  if (this.closed) {
32449
32495
  s.destroy();
32450
32496
  return done(true);
@@ -32462,9 +32508,9 @@ var EmbedderClient = class {
32462
32508
  this.settleReady(true);
32463
32509
  done(true);
32464
32510
  } catch {
32465
- refuse2("embedder_unavailable");
32511
+ refuse3("embedder_unavailable");
32466
32512
  }
32467
- }, () => refuse2("embedder_unavailable")));
32513
+ }, () => refuse3("embedder_unavailable")));
32468
32514
  });
32469
32515
  }
32470
32516
  onFrame(f) {
@@ -35459,6 +35505,213 @@ function roomIsCrossOrg(rows) {
35459
35505
  return unknown ? null : false;
35460
35506
  }
35461
35507
 
35508
+ // src/voice-channel-binding.ts
35509
+ var BOUND = Object.freeze({ bound: true });
35510
+ var refuse = (reason) => ({ bound: false, reason });
35511
+ function evaluateVoiceBinding(v, p, isVRelay) {
35512
+ if (v.ownLeafIndex === null || v.leaves.length === 0) return refuse("v_not_held");
35513
+ if (p.ownLeafIndex === null || p.leaves.length === 0) return refuse("p_not_held");
35514
+ if (v.leaves.some((l) => !l.basic || l.identity === null)) return refuse("non_basic_leaf");
35515
+ const pIds = /* @__PURE__ */ new Set();
35516
+ for (const l of p.leaves) if (l.basic && l.identity !== null) pIds.add(l.identity);
35517
+ const nonParent = /* @__PURE__ */ new Set();
35518
+ for (const l of v.leaves) if (!pIds.has(l.identity)) nonParent.add(l.identity);
35519
+ if (nonParent.size > 1) return refuse("two_non_parent");
35520
+ if (isVRelay === void 0) return nonParent.size === 0 ? BOUND : refuse("relay_unknown");
35521
+ const relays = vRelayIdentities(v, isVRelay);
35522
+ if (relays.size > 1) return refuse("two_relays");
35523
+ const [relay] = relays;
35524
+ for (const id of nonParent) if (id !== relay) return refuse("foreign_leaf");
35525
+ return BOUND;
35526
+ }
35527
+ function vRelayIdentities(v, isVRelay) {
35528
+ const relays = /* @__PURE__ */ new Set();
35529
+ for (const l of v.leaves) if (l.basic && l.identity !== null && isVRelay(l.identity)) relays.add(l.identity);
35530
+ return relays;
35531
+ }
35532
+ function voiceRelayIdentity(v, isVRelay) {
35533
+ if (isVRelay === void 0) return void 0;
35534
+ const relays = vRelayIdentities(v, isVRelay);
35535
+ return relays.size === 1 ? [...relays][0] : void 0;
35536
+ }
35537
+ function parseVoiceResolution(channelId, status, body) {
35538
+ if (status === 404) return { kind: "absent" };
35539
+ if (status !== 200) return { kind: "error" };
35540
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return { kind: "error" };
35541
+ const b = body;
35542
+ if (b.channel_id !== channelId || typeof b.channel_kind !== "string") return { kind: "error" };
35543
+ if (b.channel_kind !== "voice") return { kind: "not_voice" };
35544
+ const parent3 = b.parent_channel_id;
35545
+ if (typeof parent3 !== "string" || parent3.length === 0 || parent3 === channelId) return { kind: "error" };
35546
+ return { kind: "voice", parentChannelId: parent3 };
35547
+ }
35548
+ var VOICE_RESOLVE_RETRY_BASE_MS = 3e4;
35549
+ var VOICE_RESOLVE_RETRY_MAX_MS = 10 * 6e4;
35550
+ var VOICE_RESOLVE_ABSENT_HELD_MAX_MS = 6e4;
35551
+ var VOICE_RECEIPT_CAP = 1e4;
35552
+ var VOICE_RECEIPT_MAX_AGE_MS = 15 * 6e4;
35553
+ function voiceParentStamp(p) {
35554
+ return JSON.stringify([p.groupId, p.epoch]);
35555
+ }
35556
+ var VoiceChannelRegistry = class {
35557
+ parents = /* @__PURE__ */ new Map();
35558
+ notVoice = /* @__PURE__ */ new Set();
35559
+ failures = /* @__PURE__ */ new Map();
35560
+ bindings = /* @__PURE__ */ new Map();
35561
+ generations = /* @__PURE__ */ new Map();
35562
+ /** Fix I-1 / Task 20 R1: message id -> P's held position before the batch that first received it; null = not knowable. */
35563
+ receipts = /* @__PURE__ */ new Map();
35564
+ /** Final review M-d: arrivals not recorded because the map was full of young records. */
35565
+ capReached = 0;
35566
+ /** True when a lookup should run now: never once resolved; after a failure, only past its back-off. */
35567
+ resolveDue(channelId, now, opts = {}) {
35568
+ if (this.parents.has(channelId) || this.notVoice.has(channelId)) return false;
35569
+ const f = this.failures.get(channelId);
35570
+ if (f === void 0) return true;
35571
+ let wait = Math.min(VOICE_RESOLVE_RETRY_MAX_MS, VOICE_RESOLVE_RETRY_BASE_MS * 2 ** Math.min(f.attempts - 1, 20));
35572
+ if (f.kind === "absent" && opts.heldGroup === true) wait = Math.min(wait, VOICE_RESOLVE_ABSENT_HELD_MAX_MS);
35573
+ return now - f.at >= wait;
35574
+ }
35575
+ record(channelId, r, now) {
35576
+ if (r.kind === "voice") {
35577
+ this.parents.set(channelId, r.parentChannelId);
35578
+ this.failures.delete(channelId);
35579
+ } else if (r.kind === "not_voice") {
35580
+ this.notVoice.add(channelId);
35581
+ this.failures.delete(channelId);
35582
+ } else {
35583
+ const prev = this.failures.get(channelId);
35584
+ this.failures.set(channelId, { at: now, attempts: (prev?.attempts ?? 0) + 1, kind: r.kind });
35585
+ }
35586
+ }
35587
+ /** True once get_channel has answered for this channel (voice or not). */
35588
+ isResolved(channelId) {
35589
+ return this.parents.has(channelId) || this.notVoice.has(channelId);
35590
+ }
35591
+ /** Final review M-a: true once get_channel answered this channel's own row with a kind other than voice. */
35592
+ isNotVoice(channelId) {
35593
+ return this.notVoice.has(channelId);
35594
+ }
35595
+ /** Final review (R∈P): bound Vs whose admitted R is also a P leaf (the pre-stamp shape; a sunset signal). */
35596
+ relayInParentCount() {
35597
+ let n = 0;
35598
+ for (const [v, e] of this.bindings) {
35599
+ if (this.parents.has(v) && e.verdict.bound && e.relay !== void 0 && e.parentMembers?.has(e.relay) === true) n++;
35600
+ }
35601
+ return n;
35602
+ }
35603
+ /** True for a resolved V, bound or not. */
35604
+ isVoice(channelId) {
35605
+ return this.parents.has(channelId);
35606
+ }
35607
+ /** P of a resolved V, bound or not; undefined otherwise. Never use this to route: see {@link boundParentOf}. */
35608
+ parentOf(channelId) {
35609
+ return this.parents.get(channelId);
35610
+ }
35611
+ /** Every resolved V with its P. */
35612
+ voiceChannels() {
35613
+ return [...this.parents.entries()];
35614
+ }
35615
+ /** The gate for a channel the bridge's listing has never classified (`unlisted`), or for a resolved V. */
35616
+ gate(channelId, unlisted) {
35617
+ if (this.parents.has(channelId)) return "drop";
35618
+ if (!unlisted || this.notVoice.has(channelId)) return "normal";
35619
+ return this.failures.get(channelId)?.kind === "absent" ? "drop" : "hold";
35620
+ }
35621
+ bindingKey(channelId) {
35622
+ return this.bindings.get(channelId)?.key;
35623
+ }
35624
+ setBinding(channelId, key, verdict, positions = null, admitted = {}) {
35625
+ this.bindings.set(channelId, { key, positions, verdict, ...admitted });
35626
+ }
35627
+ /** Fix M-1: each evaluation of a V takes a generation; only the newest started may store its verdict. */
35628
+ beginEvaluation(channelId) {
35629
+ const g = (this.generations.get(channelId) ?? 0) + 1;
35630
+ this.generations.set(channelId, g);
35631
+ return g;
35632
+ }
35633
+ isLatestEvaluation(channelId, generation) {
35634
+ return this.generations.get(channelId) === generation;
35635
+ }
35636
+ /**
35637
+ * Fix I-1: records a frame's receipt stamp the FIRST time it is seen; a re-served frame keeps its first record.
35638
+ * Task 20 R2: at the cap nothing is evicted and the new arrival is not recorded (refused: no record, no parent).
35639
+ */
35640
+ noteReceipt(messageId, pStamp, now = Date.now()) {
35641
+ if (this.receipts.has(messageId)) return;
35642
+ if (this.receipts.size >= VOICE_RECEIPT_CAP) {
35643
+ for (const [id, r] of this.receipts) if (now - r.at > VOICE_RECEIPT_MAX_AGE_MS) this.receipts.delete(id);
35644
+ if (this.receipts.size >= VOICE_RECEIPT_CAP) {
35645
+ this.capReached++;
35646
+ return;
35647
+ }
35648
+ }
35649
+ this.receipts.set(messageId, { stamp: pStamp, at: now });
35650
+ }
35651
+ /** Final review M-d: arrivals refused because the receipt map was full (a number only). */
35652
+ receiptCapReached() {
35653
+ return this.capReached;
35654
+ }
35655
+ /** Test visibility: receipt records held. */
35656
+ receiptCount() {
35657
+ return this.receipts.size;
35658
+ }
35659
+ forgetReceipt(messageId) {
35660
+ this.receipts.delete(messageId);
35661
+ }
35662
+ /**
35663
+ * Fix I-1: P for one frame on V only when V is bound now AND both groups' held positions are exactly those at the
35664
+ * frame's receipt. A frame received at other positions (or with none recorded) gets undefined: dropped, never held.
35665
+ */
35666
+ boundParentForFrame(channelId, messageId, openedAt) {
35667
+ return this.frameContext(channelId, messageId, openedAt)?.parent;
35668
+ }
35669
+ /**
35670
+ * Task 20: the per-frame verdict with what the accept filter needs. Bound now AND the frame's pre-batch P stamp equals
35671
+ * the verdict's P position AND (R1) the frame's own opening position equals the verdict's V position. A frame with
35672
+ * no record, a null record, or no `openedAt` is refused.
35673
+ */
35674
+ frameContext(channelId, messageId, openedAt) {
35675
+ const parent3 = this.boundParentOf(channelId);
35676
+ if (parent3 === void 0) return void 0;
35677
+ const entry = this.bindings.get(channelId);
35678
+ const at = this.receipts.get(messageId)?.stamp;
35679
+ if (entry?.positions == null || typeof at !== "string" || at !== entry.positions.p) return void 0;
35680
+ const v = entry.positions.v;
35681
+ if (v === null || openedAt == null || openedAt.groupId !== v.groupId || openedAt.epoch !== v.epoch) return void 0;
35682
+ return { parent: parent3, relay: entry.relay, parentMembers: entry.parentMembers ?? /* @__PURE__ */ new Set() };
35683
+ }
35684
+ /** The current verdict, or undefined when never evaluated. */
35685
+ binding(channelId) {
35686
+ return this.bindings.get(channelId)?.verdict;
35687
+ }
35688
+ /** P only while V is a resolved V whose last verdict is bound; undefined otherwise. */
35689
+ boundParentOf(channelId) {
35690
+ const parent3 = this.parents.get(channelId);
35691
+ return parent3 !== void 0 && this.bindings.get(channelId)?.verdict.bound === true ? parent3 : void 0;
35692
+ }
35693
+ };
35694
+
35695
+ // src/voice-channel-frame.ts
35696
+ function classifyVoiceFrame(f) {
35697
+ if (f.senderId === f.ownIdentityId) return "drop";
35698
+ const fromRelay = f.relay !== void 0 && f.senderId === f.relay;
35699
+ const fromMember = f.parentMembers.has(f.senderId);
35700
+ if (!fromRelay && !fromMember) return "drop";
35701
+ let body;
35702
+ try {
35703
+ body = JSON.parse(new TextDecoder().decode(f.plaintext));
35704
+ } catch {
35705
+ return "drop";
35706
+ }
35707
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return "drop";
35708
+ const b = body;
35709
+ if (b.type === "call_transcript") return "call_transcript";
35710
+ if (b.type !== void 0 && b.type !== "text" || typeof b.text !== "string") return "drop";
35711
+ if (fromRelay) return "relay_turn";
35712
+ return b.text.trim() === VOICE_CALL_START_MARKER ? "greeting" : "drop";
35713
+ }
35714
+
35462
35715
  // src/grants/owner-turn.ts
35463
35716
  import { ed25519 as ed255196 } from "@noble/curves/ed25519";
35464
35717
  var HEX = /^(?:[0-9a-fA-F]{2})+$/;
@@ -35554,6 +35807,7 @@ var OWNER_DM_DEFAULT_READ_GRANTS = Object.freeze([
35554
35807
  var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
35555
35808
  Object.freeze({ tool: "current_time", mode: "read" })
35556
35809
  ]);
35810
+ var BRIDGE_CAPABILITIES = Object.freeze(["voice_channel_v1"]);
35557
35811
  var RELAYED_VOICE_GRANTS = Object.freeze([
35558
35812
  Object.freeze({ tool: "current_time", mode: "read" }),
35559
35813
  Object.freeze({ tool: "web_search", mode: "read" })
@@ -35828,12 +36082,19 @@ var RoomClassChangedSendError = class extends SendForbiddenError {
35828
36082
  this.message = `reply dropped for channel ${channelId}: the room changed after its tools were granted; no retry`;
35829
36083
  }
35830
36084
  };
36085
+ function voiceVerdictPositions(v, p) {
36086
+ return {
36087
+ v: v.groupId !== null && v.epoch !== null ? { groupId: v.groupId, epoch: v.epoch } : null,
36088
+ p: voiceParentStamp(p)
36089
+ };
36090
+ }
35831
36091
  function rosterFreshAt(entry, heldEpoch, heldGroupId) {
35832
36092
  return entry !== void 0 && typeof heldEpoch === "number" && entry.epoch === heldEpoch && typeof heldGroupId === "string" && entry.groupId === heldGroupId;
35833
36093
  }
35834
36094
  var RoomGrantEpochMovedError = class extends Error {
35835
36095
  };
35836
36096
  var ROOM_GRANT_TRACK_MAX = 4096;
36097
+ var VOICE_FRAME_PARENT_MAX = 4096;
35837
36098
  var SessionSupersededError = class extends Error {
35838
36099
  constructor() {
35839
36100
  super("session_superseded: another copy of this agent took its session during start()");
@@ -36265,6 +36526,13 @@ var BridgeAgent = class _BridgeAgent {
36265
36526
  supersedeRefundable = /* @__PURE__ */ new Set();
36266
36527
  /** M-1: channels this process has used as a FileSafe vault channel. */
36267
36528
  vaultChannelIds = /* @__PURE__ */ new Set();
36529
+ /**
36530
+ * ADR-0024 step 2a Task 19 (spec §9): hidden voice channels V resolved to their conversation P, lookup back-off, and
36531
+ * each V's binding verdict. In memory only: a restart re-resolves (one get_channel per V) and re-evaluates.
36532
+ */
36533
+ voiceChannels = new VoiceChannelRegistry();
36534
+ /** Fix M-3: channels a listing row marked channel_kind "voice"; never recorded as listed. */
36535
+ listedAsVoice = /* @__PURE__ */ new Set();
36268
36536
  /** Phase 3 F3: consecutive agent turns per channel. */
36269
36537
  agentHops = new AgentHopTracker();
36270
36538
  maxAgentHops;
@@ -36295,6 +36563,12 @@ var BridgeAgent = class _BridgeAgent {
36295
36563
  * counter is incremented. Surfaced on health()/metrics().
36296
36564
  */
36297
36565
  failedAttachments = 0;
36566
+ /** Task 20 R4: V frames dropped by the per-frame verdict (not bound, or not at the verdict's positions). A number only. */
36567
+ voiceFramesRefused = 0;
36568
+ /** Task 20: frames on a bound V the accept filter dropped (not R's voice frame, a greeting marker or a transcript). */
36569
+ voiceFramesFiltered = 0;
36570
+ /** Task 20: message id of an admitted V frame -> its P (B.8 keys). Bounded; an entry lives one ingest cycle in practice. */
36571
+ voiceFrameParent = /* @__PURE__ */ new Map();
36298
36572
  /** Files link: vault_ref bodies refused by R1 (strict parse) or R2 (enclave binding). */
36299
36573
  refusedVaultRefs = 0;
36300
36574
  /**
@@ -36708,7 +36982,9 @@ var BridgeAgent = class _BridgeAgent {
36708
36982
  // Fix round MEDIUM 2: a channel this process used as a FileSafe vault
36709
36983
  // channel is never swept (the sweeper also refuses any channel no listing
36710
36984
  // has shown, which covers a vault channel after a restart).
36711
- conversations: () => this.store.memoryConversationIds().filter((c) => !this.vaultChannelIds.has(c)),
36985
+ // Task 20 final review I-1: a voice channel V is never listed, so it is swept as its parent P: V is present exactly
36986
+ // when P is, and purgeConversation(P) purges every V of P. P is swept even when only its V holds memory.
36987
+ conversations: () => [...new Set(this.store.memoryConversationIds().filter((c) => !this.vaultChannelIds.has(c)).map((c) => this.store.voiceChannelParent(c) ?? c))],
36712
36988
  enclaveOf: (c) => this.store.getChannelMeta(c)?.enclaveId,
36713
36989
  // Audit r1 M6: the persisted flag, never the channelOwner cache. Set only
36714
36990
  // from a listing, so undefined means no listing has ever shown the channel.
@@ -37128,6 +37404,8 @@ var BridgeAgent = class _BridgeAgent {
37128
37404
  // the actual protected-memory adapter. It is an enum, never a path or
37129
37405
  // provider credential.
37130
37406
  memory_provider: this.cfg.memoryProvider ?? "securecoms",
37407
+ // Spec §2 (Task 18): the per-device capability declaration; a fixed enum list, one body for every report path.
37408
+ capabilities: [...BRIDGE_CAPABILITIES],
37131
37409
  // Spec §11.1-11.2 (OQ-E): flat keys; the string `memory` above is
37132
37410
  // unchanged. memory_notes is true only when notes are actually on (the
37133
37411
  // provider is securecoms and the parsed payload.memory keeps them on);
@@ -38375,6 +38653,7 @@ var BridgeAgent = class _BridgeAgent {
38375
38653
  * across a supersede), and the group must not move around the reads.
38376
38654
  */
38377
38655
  async reclassifyForReply(channelId, grant) {
38656
+ if (this.voiceChannels.isVoice(channelId)) return false;
38378
38657
  const startedAt = Date.now();
38379
38658
  await this.refreshChannelRoster(channelId);
38380
38659
  const held = await this.heldPosition(channelId);
@@ -38391,8 +38670,9 @@ var BridgeAgent = class _BridgeAgent {
38391
38670
  this.enforceRateFloor("a2a", channelId);
38392
38671
  return;
38393
38672
  }
38394
- const meta = this.freshMeta(channelId);
38395
- if (meta && !meta.is_dm) this.enforceRateFloor("room", channelId);
38673
+ const capChannel = this.capChannelFor(channelId);
38674
+ const meta = this.freshMeta(capChannel);
38675
+ if (meta && !meta.is_dm) this.enforceRateFloor("room", capChannel);
38396
38676
  }
38397
38677
  /** Bookkeeping after a CONFIRMED publish of `body` under `id`. */
38398
38678
  afterPublish(channelId, body, id, opts, purgeEpoch, replyCapture) {
@@ -38874,8 +39154,289 @@ var BridgeAgent = class _BridgeAgent {
38874
39154
  return probeResultFor("threw");
38875
39155
  }
38876
39156
  }
39157
+ // ── ADR-0024 step 2a Task 19: the hidden voice channel V (spec §9, B.8) ──────
39158
+ //
39159
+ // The rule and the cache live in voice-channel-binding.ts; this is the wiring: the get_channel lookup, the bridge's
39160
+ // own held ratchet trees and positions, V's member list (is_relay only), and the receive-cycle gate.
39161
+ /**
39162
+ * Task 20's accessor: P's channel id ONLY while V is bound under the spec §9 rule at both groups' held positions as
39163
+ * of the last evaluation (every receive cycle, before any frame is processed; or {@link voiceParentNow}); undefined
39164
+ * for an unbound or refused V, and for any channel that is not a resolved V.
39165
+ */
39166
+ voiceParentOf(channelId) {
39167
+ return this.voiceChannels.boundParentOf(channelId);
39168
+ }
39169
+ /** True for a channel resolved as a V, bound or not. Such a channel is never processed as a normal channel. */
39170
+ isKnownVoiceChannel(channelId) {
39171
+ return this.voiceChannels.isVoice(channelId);
39172
+ }
39173
+ /** Re-evaluates one V at the held positions now, then answers as {@link voiceParentOf}. */
39174
+ async voiceParentNow(channelId) {
39175
+ const parent3 = this.voiceChannels.parentOf(channelId);
39176
+ if (parent3 === void 0) return void 0;
39177
+ await this.reevaluateVoiceBinding(channelId, parent3);
39178
+ return this.voiceChannels.boundParentOf(channelId);
39179
+ }
39180
+ /** A channel no listing has ever classified (in memory or persisted). V is hidden from the listing, so it is one. */
39181
+ isUnlistedChannel(channelId) {
39182
+ return this.channelMeta.get(channelId) === void 0 && this.store.getChannelMeta(channelId)?.isDm === void 0;
39183
+ }
39184
+ voiceChannelGate(channelId) {
39185
+ if (this.vaultChannelIds.has(channelId)) return "normal";
39186
+ return this.voiceChannels.gate(channelId, this.isUnlistedChannel(channelId));
39187
+ }
39188
+ /**
39189
+ * Looks up one channel with get_channel (the voice arm, OQ-5 (a)). Only the classification is kept. Never throws; a
39190
+ * failure is recorded and retried (30 s doubling to 10 min). Called only from {@link resolveUnlistedChannels}, which
39191
+ * selects unlisted channels whose back-off allows.
39192
+ */
39193
+ async resolveVoiceChannel(channelId) {
39194
+ let status = "threw";
39195
+ let body;
39196
+ try {
39197
+ const url = `${this.cfg.apiBase}/api/v1/channels/${encodeURIComponent(channelId)}?caller_identity_id=${encodeURIComponent(this.cfg.identityId)}`;
39198
+ const res = await this.fetchBounded("get_channel", "read", url, { headers: { authorization: `Bearer ${this.cfg.apiKey}` } });
39199
+ if (res.ok) {
39200
+ body = await res.json();
39201
+ } else {
39202
+ try {
39203
+ await res.body?.cancel();
39204
+ } catch {
39205
+ }
39206
+ }
39207
+ status = res.status;
39208
+ } catch {
39209
+ status = "threw";
39210
+ }
39211
+ const r = parseVoiceResolution(channelId, status, body);
39212
+ this.voiceChannels.record(channelId, r, Date.now());
39213
+ if (r.kind === "voice") {
39214
+ try {
39215
+ this.store.recordVoiceChannelParent(channelId, r.parentChannelId);
39216
+ } catch {
39217
+ }
39218
+ }
39219
+ if (r.kind === "voice") writeServiceLog("info", "voice.channel_lookup", { result: ServiceLogEnum.of("voice", ["voice", "absent", "error"]) });
39220
+ else if (r.kind !== "not_voice") writeServiceLog("warn", "voice.channel_lookup", { result: ServiceLogEnum.of(r.kind, ["voice", "absent", "error"]) });
39221
+ }
39222
+ /**
39223
+ * Looks up every unlisted channel among `channelIds` whose back-off allows. A channel is "unlisted" only after this
39224
+ * cycle's listing: if any candidate remains and the cycle has not listed yet, one listing runs first, so a channel
39225
+ * that is merely new (listed, not yet cached) is never looked up. Returns true when it ran that listing.
39226
+ */
39227
+ async resolveUnlistedChannels(channelIds, listed) {
39228
+ const ids = [...new Set(channelIds)].filter((c) => !this.vaultChannelIds.has(c));
39229
+ const held = /* @__PURE__ */ new Set();
39230
+ for (const c of ids) {
39231
+ if (!this.isUnlistedChannel(c) || this.voiceChannels.isResolved(c)) continue;
39232
+ try {
39233
+ if ((await this.heldPosition(c)).groupId !== null) held.add(c);
39234
+ } catch {
39235
+ }
39236
+ }
39237
+ const due = () => ids.filter((c) => this.isUnlistedChannel(c) && this.voiceChannels.resolveDue(c, Date.now(), { heldGroup: held.has(c) }));
39238
+ let candidates = due();
39239
+ if (candidates.length === 0) return false;
39240
+ let ran = false;
39241
+ if (!listed) {
39242
+ await this.refreshChannelMeta();
39243
+ ran = true;
39244
+ candidates = due();
39245
+ }
39246
+ for (const c of candidates) await this.resolveVoiceChannel(c);
39247
+ return ran;
39248
+ }
39249
+ /** Final review M-c: V -> consecutive failed member-list reads for the binding, and when the last one ran. */
39250
+ voiceRosterRetry = /* @__PURE__ */ new Map();
39251
+ /** The first retry after a failure is immediate (next cycle); then 30 s, doubling, capped at 10 min. */
39252
+ voiceRosterRetryDue(v) {
39253
+ const f = this.voiceRosterRetry.get(v);
39254
+ if (f === void 0 || f.attempts <= 1) return true;
39255
+ const wait = Math.min(VOICE_RESOLVE_RETRY_MAX_MS, VOICE_RESOLVE_RETRY_BASE_MS * 2 ** Math.min(f.attempts - 2, 20));
39256
+ return Date.now() - f.at >= wait;
39257
+ }
39258
+ noteVoiceRosterRead(v, ok) {
39259
+ if (ok) {
39260
+ this.voiceRosterRetry.delete(v);
39261
+ return;
39262
+ }
39263
+ this.voiceRosterRetry.set(v, { at: Date.now(), attempts: (this.voiceRosterRetry.get(v)?.attempts ?? 0) + 1 });
39264
+ }
39265
+ /** Every resolved V, re-evaluated where a position or a roster moved. */
39266
+ async reevaluateVoiceBindings() {
39267
+ for (const [v, p] of this.voiceChannels.voiceChannels()) await this.reevaluateVoiceBinding(v, p);
39268
+ }
39269
+ /**
39270
+ * The spec §9 binding rule over the bridge's OWN ratchet trees (never a server list of leaves), keyed by both
39271
+ * groups' held (group, epoch) and both rosters' fetch times: a change in any of them re-evaluates. V's member list
39272
+ * is read only for `is_relay`, and only when it is fresh at V's held position. Fail closed: a tree that moved while
39273
+ * it was read, or any exception, leaves V unbound and is re-evaluated next time.
39274
+ */
39275
+ async reevaluateVoiceBinding(v, p) {
39276
+ const generation = this.voiceChannels.beginEvaluation(v);
39277
+ try {
39278
+ const vPos = await this.heldPosition(v);
39279
+ const pPos = await this.heldPosition(p);
39280
+ const positions = voiceVerdictPositions(vPos, pPos);
39281
+ const parentNotConversation = this.voiceChannels.isVoice(p) || this.isUnlistedChannel(p);
39282
+ const keyOf = () => JSON.stringify([positions, this.freshRosterEntry(v)?.at ?? null, this.freshRosterEntry(p)?.at ?? null, parentNotConversation]);
39283
+ if (this.voiceChannels.bindingKey(v) === keyOf()) return;
39284
+ if (parentNotConversation) {
39285
+ if (this.voiceChannels.isLatestEvaluation(v, generation)) this.noteVoiceBinding(v, keyOf(), { bound: false, reason: "parent_not_conversation" }, positions);
39286
+ return;
39287
+ }
39288
+ const vTree = await this.provider.leafTreeSummary(v);
39289
+ const pTree = await this.provider.leafTreeSummary(p);
39290
+ const relayCheck = () => {
39291
+ const entry = this.freshRosterEntry(v);
39292
+ return entry !== void 0 && rosterFreshAt(entry, vPos.epoch, vPos.groupId) ? (id) => entry.members.get(id)?.is_relay === true : void 0;
39293
+ };
39294
+ if (relayCheck() === void 0 && this.voiceRosterRetryDue(v)) {
39295
+ await this.refreshChannelRoster(v);
39296
+ this.noteVoiceRosterRead(v, relayCheck() !== void 0);
39297
+ }
39298
+ const isVRelay = relayCheck();
39299
+ let verdict = evaluateVoiceBinding(vTree, pTree, isVRelay);
39300
+ const vAfter = await this.heldPosition(v);
39301
+ const pAfter = await this.heldPosition(p);
39302
+ const moved = vAfter.groupId !== vPos.groupId || vAfter.epoch !== vPos.epoch || pAfter.groupId !== pPos.groupId || pAfter.epoch !== pPos.epoch;
39303
+ if (moved) verdict = { bound: false, reason: "moved" };
39304
+ const retry = !verdict.bound && (verdict.reason === "relay_unknown" || verdict.reason === "moved") || verdict.bound && isVRelay === void 0;
39305
+ if (!this.voiceChannels.isLatestEvaluation(v, generation)) return;
39306
+ const parentMembers = /* @__PURE__ */ new Set();
39307
+ for (const l of pTree.leaves) if (l.basic && l.identity !== null) parentMembers.add(l.identity);
39308
+ this.noteVoiceBinding(v, retry ? "" : keyOf(), verdict, positions, void 0, {
39309
+ relay: verdict.bound ? voiceRelayIdentity(vTree, isVRelay) : void 0,
39310
+ parentMembers
39311
+ });
39312
+ } catch (e) {
39313
+ if (this.voiceChannels.isLatestEvaluation(v, generation)) this.noteVoiceBinding(v, "", { bound: false, reason: "moved" }, null, e);
39314
+ }
39315
+ }
39316
+ /**
39317
+ * Fix I-1 / Task 20 R1: P's held position for every resolved V, read before a receive so frames can be stamped. V's
39318
+ * half is not stamped here: each frame carries the epoch that opened it (`openedAt`).
39319
+ */
39320
+ async voicePositionSnapshot() {
39321
+ const out = /* @__PURE__ */ new Map();
39322
+ for (const [v, p] of this.voiceChannels.voiceChannels()) {
39323
+ try {
39324
+ out.set(v, voiceParentStamp(await this.heldPosition(p)));
39325
+ } catch {
39326
+ }
39327
+ }
39328
+ return out;
39329
+ }
39330
+ /**
39331
+ * Fix I-1, for Task 20: P for ONE frame on V, only when V is bound now AND neither P's nor V's held (group, epoch)
39332
+ * moved between that frame's first receipt and the verdict. Otherwise undefined: the frame is dropped (acked, never
39333
+ * held, never processed). A frame first received before V was resolved has no receipt positions and is refused.
39334
+ */
39335
+ voiceParentForFrame(channelId, messageId, openedAt) {
39336
+ return this.voiceChannels.boundParentForFrame(channelId, messageId, openedAt);
39337
+ }
39338
+ /** Stores a verdict; a change of verdict logs one content-free line. */
39339
+ noteVoiceBinding(v, key, verdict, positions, error, admitted = {}) {
39340
+ const prev = this.voiceChannels.binding(v);
39341
+ this.voiceChannels.setBinding(v, key, verdict, positions, admitted);
39342
+ const state = verdict.bound ? "bound" : verdict.reason;
39343
+ const prevState = prev === void 0 ? void 0 : prev.bound ? "bound" : prev.reason;
39344
+ if (state === prevState && error === void 0) return;
39345
+ writeServiceLog(verdict.bound ? "info" : "warn", "voice.channel_binding", {
39346
+ state: ServiceLogEnum.of(state, ["bound", "v_not_held", "p_not_held", "non_basic_leaf", "foreign_leaf", "two_non_parent", "two_relays", "relay_unknown", "moved", "parent_not_conversation"])
39347
+ }, error);
39348
+ }
39349
+ /**
39350
+ * The receive-cycle gate, before any per-frame work. A resolved V is never a normal channel: a frame passes only when
39351
+ * the per-frame verdict names P (bound now, its P stamp and its own opening epoch at the verdict's positions) AND the
39352
+ * accept filter takes it (Task 20: R's voice frame or a P member's greeting marker; a call_transcript is acked, not a
39353
+ * turn). Every other V frame is acked unprocessed and never recorded. A channel get_channel answered 404 is acked
39354
+ * unprocessed (fail closed). An unlisted channel whose lookup errored or has not run is held: neither processed nor
39355
+ * acked, so it is re-served once the lookup answers. Everything else passes unchanged.
39356
+ */
39357
+ async gateVoiceEntries(entries) {
39358
+ const keep = [];
39359
+ const drop = [];
39360
+ for (const m of entries) {
39361
+ const gate = this.voiceChannelGate(m.channelId);
39362
+ if (gate === "hold") continue;
39363
+ if (gate === "normal") {
39364
+ this.voiceChannels.forgetReceipt(m.messageId);
39365
+ keep.push(m);
39366
+ continue;
39367
+ }
39368
+ if (this.voiceChannels.isVoice(m.channelId)) {
39369
+ const admitted = this.admitVoiceFrame(m);
39370
+ this.voiceChannels.forgetReceipt(m.messageId);
39371
+ if (admitted) {
39372
+ keep.push(m);
39373
+ continue;
39374
+ }
39375
+ } else {
39376
+ this.voiceChannels.forgetReceipt(m.messageId);
39377
+ }
39378
+ drop.push(m.queueId);
39379
+ }
39380
+ if (drop.length) {
39381
+ try {
39382
+ await this.provider.ackDelivery(this.cfg.deviceId, drop);
39383
+ } catch {
39384
+ }
39385
+ }
39386
+ return keep;
39387
+ }
39388
+ /**
39389
+ * Task 20: the per-frame verdict and the accept filter for one frame on a resolved V. True only for a model turn
39390
+ * (R's voice frame, a P member's greeting marker), which is then keyed per B.8 through {@link voiceFrameParent}. A
39391
+ * call_transcript is accepted but is not a turn (acked, nothing more, as step 1 on P). Counters are numbers only.
39392
+ */
39393
+ admitVoiceFrame(m) {
39394
+ const parent3 = this.voiceParentForFrame(m.channelId, m.messageId, m.openedAt ?? null);
39395
+ const ctx = parent3 === void 0 ? void 0 : this.voiceChannels.frameContext(m.channelId, m.messageId, m.openedAt ?? null);
39396
+ if (ctx === void 0) {
39397
+ this.voiceFramesRefused++;
39398
+ return false;
39399
+ }
39400
+ const kind2 = classifyVoiceFrame({
39401
+ senderId: m.envelope.sender_id,
39402
+ ownIdentityId: this.cfg.identityId,
39403
+ relay: ctx.relay,
39404
+ parentMembers: ctx.parentMembers,
39405
+ plaintext: m.plaintext
39406
+ });
39407
+ if (kind2 === "call_transcript") return false;
39408
+ if (kind2 === "drop") {
39409
+ this.voiceFramesFiltered++;
39410
+ return false;
39411
+ }
39412
+ this.voiceFrameParent.set(m.messageId, ctx.parent);
39413
+ if (this.voiceFrameParent.size > VOICE_FRAME_PARENT_MAX) this.voiceFrameParent.delete(this.voiceFrameParent.keys().next().value);
39414
+ return true;
39415
+ }
39416
+ /**
39417
+ * Task 20 (B.8): the key channel for metadata, audience, owner, manifest and caps. For an admitted V frame, its P;
39418
+ * for any other frame, its own channel.
39419
+ */
39420
+ keyChannelForFrame(m) {
39421
+ return this.voiceFrameParent.get(m.messageId) ?? m.channelId;
39422
+ }
39423
+ /**
39424
+ * Task 20 (B.8 "per-room daily caps → P", the room rate floor): the channel a cap or floor counts against. A known V
39425
+ * counts against its bound P; an unbound V against itself (no grant can be issued there, so nothing is widened).
39426
+ */
39427
+ capChannelFor(channelId) {
39428
+ return this.voiceChannels.parentOf(channelId) ?? channelId;
39429
+ }
39430
+ /** Phase 3 F1 pause, for a voice channel V also read on its P (a paused agent runs no inference on the call either). */
39431
+ pausedFor(channelId) {
39432
+ return this.pausedChannels.has(channelId) || this.pausedChannels.has(this.capChannelFor(channelId));
39433
+ }
38877
39434
  /** §5.4: one conversation's turns, facts, notes and recall, inside the memory mutex, with every in-memory copy. */
38878
39435
  async purgeConversation(channelId) {
39436
+ await this.purgeOneConversation(channelId);
39437
+ for (const v of this.store.voiceChannelsOf(channelId)) await this.purgeOneConversation(v);
39438
+ }
39439
+ async purgeOneConversation(channelId) {
38879
39440
  const scopes = this.unifiedOn ? this.store.ownerDmScopeIds().filter((s) => parseOwnerDmScopeId(s)?.channelId === channelId) : [];
38880
39441
  await this.notes.purgeChannel(channelId);
38881
39442
  for (const s of scopes) this.store.clearConversationAbsence(s);
@@ -40115,17 +40676,17 @@ var BridgeAgent = class _BridgeAgent {
40115
40676
  async applyMemoryNote(raw, ctx) {
40116
40677
  if (this.unifiedOn) return this.applyUnifiedNote(raw, ctx);
40117
40678
  const ids = echoIds(raw);
40118
- const refuse2 = (reason, echo = ids) => {
40679
+ const refuse3 = (reason, echo = ids) => {
40119
40680
  writeServiceLog("info", "memory.note_refused", {
40120
40681
  reason_class: ServiceLogEnum.of(reason, ["mode", "invalid", "message", "audience", "provenance", "secret", "cap", "cleared", "store", "budget", "purged", "not_owner_words"])
40121
40682
  });
40122
40683
  return { type: "memory_note_result", ...echo, ok: false, reason };
40123
40684
  };
40124
- if (!this.notesProviderOn()) return refuse2("mode");
40685
+ if (!this.notesProviderOn()) return refuse3("mode");
40125
40686
  const f = parseMemoryNoteFrame(raw);
40126
- if (!f) return refuse2("invalid");
40127
- if (ctx.pendingRequests.has(f.request_id)) return refuse2("invalid", { message_id: f.message_id });
40128
- if (ctx.pendingRequests.size >= MAX_PENDING_NOTE_REQUESTS) return refuse2("invalid", {});
40687
+ if (!f) return refuse3("invalid");
40688
+ if (ctx.pendingRequests.has(f.request_id)) return refuse3("invalid", { message_id: f.message_id });
40689
+ if (ctx.pendingRequests.size >= MAX_PENDING_NOTE_REQUESTS) return refuse3("invalid", {});
40129
40690
  ctx.pendingRequests.add(f.request_id);
40130
40691
  try {
40131
40692
  const w2 = () => {
@@ -40137,13 +40698,13 @@ var BridgeAgent = class _BridgeAgent {
40137
40698
  return cur.record;
40138
40699
  };
40139
40700
  const rec = w2();
40140
- if (rec === void 0) return refuse2("message");
40701
+ if (rec === void 0) return refuse3("message");
40141
40702
  const channelId = rec.channel_id;
40142
- if (rec.authorization?.memory_write !== true) return refuse2("audience");
40143
- if (!this.noteTurnClean(rec.message_id)) return refuse2("audience");
40703
+ if (rec.authorization?.memory_write !== true) return refuse3("audience");
40704
+ if (!this.noteTurnClean(rec.message_id)) return refuse3("audience");
40144
40705
  const held = await this.heldPosition(channelId);
40145
40706
  const now = this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false });
40146
- if (now.memory_write !== true) return refuse2("audience");
40707
+ if (now.memory_write !== true) return refuse3("audience");
40147
40708
  const spend = async () => {
40148
40709
  const rw = await this.store.prepareRuntimeInboxRewrite(rec.message_id, bumpW9);
40149
40710
  if (rw === "declined") return "budget";
@@ -40159,44 +40720,44 @@ var BridgeAgent = class _BridgeAgent {
40159
40720
  };
40160
40721
  if (f.op === "forget" && typeof f.id === "object") {
40161
40722
  const shown = ctx.shownRecall(f.message_id, ctx.token);
40162
- if (!shown || !shown.has(f.id.recall)) return refuse2("invalid");
40723
+ if (!shown || !shown.has(f.id.recall)) return refuse3("invalid");
40163
40724
  const rw = await spend();
40164
- if (typeof rw === "string") return refuse2(rw);
40165
- if (w2() !== rec) return refuse2("message");
40166
- if (!this.notesProviderOn()) return refuse2("mode");
40725
+ if (typeof rw === "string") return refuse3(rw);
40726
+ if (w2() !== rec) return refuse3("message");
40727
+ if (!this.notesProviderOn()) return refuse3("mode");
40167
40728
  const r2 = await this.recall.forget(channelId, f.id.recall, rw, precommit(rw));
40168
- if (r2 === "unknown_id") return refuse2("invalid");
40169
- if (r2 !== "ok") return refuse2(r2);
40729
+ if (r2 === "unknown_id") return refuse3("invalid");
40730
+ if (r2 !== "ok") return refuse3(r2);
40170
40731
  writeServiceLog("info", "memory.recall_forgotten");
40171
40732
  return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true };
40172
40733
  }
40173
40734
  let mutation;
40174
40735
  let rewrite;
40175
40736
  if (f.op === "forget") {
40176
- if (typeof f.id !== "number") return refuse2("invalid");
40737
+ if (typeof f.id !== "number") return refuse3("invalid");
40177
40738
  const rw = await spend();
40178
- if (typeof rw === "string") return refuse2(rw);
40739
+ if (typeof rw === "string") return refuse3(rw);
40179
40740
  rewrite = rw;
40180
40741
  mutation = { op: "forget", id: f.id, rewrite: rw };
40181
40742
  } else {
40182
40743
  const p = prepareStoredNote(f.text, { text: rec.text, ...rec.transcribed === true ? { transcribed: true } : {} });
40183
- if (!p.ok) return refuse2(p.reason);
40744
+ if (!p.ok) return refuse3(p.reason);
40184
40745
  if (f.op === "replace") {
40185
- if (typeof f.id !== "number") return refuse2("invalid");
40746
+ if (typeof f.id !== "number") return refuse3("invalid");
40186
40747
  const rw = await spend();
40187
- if (typeof rw === "string") return refuse2(rw);
40748
+ if (typeof rw === "string") return refuse3(rw);
40188
40749
  rewrite = rw;
40189
40750
  mutation = { op: "replace", id: f.id, text: p.text, rewrite: rw };
40190
40751
  } else {
40191
40752
  mutation = { op: "add", text: p.text };
40192
40753
  }
40193
40754
  }
40194
- if (w2() !== rec) return refuse2("message");
40195
- if (!this.notesProviderOn()) return refuse2("mode");
40755
+ if (w2() !== rec) return refuse3("message");
40756
+ if (!this.notesProviderOn()) return refuse3("mode");
40196
40757
  const stamp = this.ingestGeneration.get(rec.message_id);
40197
- if (stamp === void 0) return refuse2("cleared");
40758
+ if (stamp === void 0) return refuse3("cleared");
40198
40759
  const r = await this.notes.write(channelId, mutation, stamp, precommit(rewrite));
40199
- if (!r.ok) return refuse2(r.reason);
40760
+ if (!r.ok) return refuse3(r.reason);
40200
40761
  writeServiceLog("info", "memory.note_written", {
40201
40762
  op: ServiceLogEnum.of(f.op, ["add", "replace", "forget"]),
40202
40763
  notes_count: r.notesCount,
@@ -40205,7 +40766,7 @@ var BridgeAgent = class _BridgeAgent {
40205
40766
  return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true, id: r.id };
40206
40767
  } catch (e) {
40207
40768
  writeServiceLog("warn", "memory.note_write_failed", {}, e);
40208
- return refuse2("store");
40769
+ return refuse3("store");
40209
40770
  } finally {
40210
40771
  ctx.pendingRequests.delete(f.request_id);
40211
40772
  }
@@ -40219,17 +40780,17 @@ var BridgeAgent = class _BridgeAgent {
40219
40780
  */
40220
40781
  async applyUnifiedNote(raw, ctx) {
40221
40782
  const ids = echoIds(raw);
40222
- const refuse2 = (reason, echo = ids) => {
40783
+ const refuse3 = (reason, echo = ids) => {
40223
40784
  writeServiceLog("info", "memory.note_refused", {
40224
40785
  reason_class: ServiceLogEnum.of(reason, ["mode", "invalid", "message", "audience", "provenance", "secret", "cap", "cleared", "store", "budget", "purged", "not_owner_words"])
40225
40786
  });
40226
40787
  return { type: "memory_note_result", ...echo, ok: false, reason };
40227
40788
  };
40228
- if (!this.unifiedProviderOn()) return refuse2("mode");
40789
+ if (!this.unifiedProviderOn()) return refuse3("mode");
40229
40790
  const f = parseMemoryNoteFrame(raw);
40230
- if (!f) return refuse2("invalid");
40231
- if (ctx.pendingRequests.has(f.request_id)) return refuse2("invalid", { message_id: f.message_id });
40232
- if (ctx.pendingRequests.size >= MAX_PENDING_NOTE_REQUESTS) return refuse2("invalid", {});
40791
+ if (!f) return refuse3("invalid");
40792
+ if (ctx.pendingRequests.has(f.request_id)) return refuse3("invalid", { message_id: f.message_id });
40793
+ if (ctx.pendingRequests.size >= MAX_PENDING_NOTE_REQUESTS) return refuse3("invalid", {});
40233
40794
  ctx.pendingRequests.add(f.request_id);
40234
40795
  try {
40235
40796
  const w2 = () => {
@@ -40241,25 +40802,25 @@ var BridgeAgent = class _BridgeAgent {
40241
40802
  return cur.record;
40242
40803
  };
40243
40804
  const rec = w2();
40244
- if (rec === void 0) return refuse2("message");
40805
+ if (rec === void 0) return refuse3("message");
40245
40806
  const channelId = rec.channel_id;
40246
- if (rec.authorization?.memory_write !== true) return refuse2("audience");
40247
- if (!this.noteTurnClean(rec.message_id)) return refuse2("audience");
40807
+ if (rec.authorization?.memory_write !== true) return refuse3("audience");
40808
+ if (!this.noteTurnClean(rec.message_id)) return refuse3("audience");
40248
40809
  const held = await this.heldPosition(channelId);
40249
- if (this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false }).memory_write !== true) return refuse2("audience");
40810
+ if (this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false }).memory_write !== true) return refuse3("audience");
40250
40811
  const scopeId = this.ownerScopeNow(channelId);
40251
- if (scopeId === null) return refuse2("audience");
40812
+ if (scopeId === null) return refuse3("audience");
40252
40813
  const precommit = () => !this.unifiedProviderOn() ? "mode" : w2() !== rec ? "message" : null;
40253
- if (f.op === "replace") return refuse2("invalid");
40254
- if (f.op === "forget") return await this.forgetUnified(f, rec, channelId, ctx, refuse2, precommit);
40814
+ if (f.op === "replace") return refuse3("invalid");
40815
+ if (f.op === "forget") return await this.forgetUnified(f, rec, channelId, ctx, refuse3, precommit);
40255
40816
  const stripped = allowlistPin(f.text);
40256
- if (stripped.trim().length === 0) return refuse2("invalid");
40817
+ if (stripped.trim().length === 0) return refuse3("invalid");
40257
40818
  const shown = ctx.shownUnified(f.message_id, ctx.token) ?? /* @__PURE__ */ new Set();
40258
- if (!pinCoverage(stripped, rec.text, this.unifiedReads.coverageTexts(f.message_id, shown)).ok) return refuse2("not_owner_words");
40259
- if (scanNote(stripped) !== null) return refuse2("secret");
40260
- if (stripped.length > PIN_MAX_CHARS) return refuse2("cap");
40819
+ if (!pinCoverage(stripped, rec.text, this.unifiedReads.coverageTexts(f.message_id, shown)).ok) return refuse3("not_owner_words");
40820
+ if (scanNote(stripped) !== null) return refuse3("secret");
40821
+ if (stripped.length > PIN_MAX_CHARS) return refuse3("cap");
40261
40822
  const stamp = this.ingestGeneration.get(rec.message_id);
40262
- if (stamp === void 0) return refuse2("cleared");
40823
+ if (stamp === void 0) return refuse3("cleared");
40263
40824
  const capture = this.store.itemCapture(channelId);
40264
40825
  const r = await this.items.commit({
40265
40826
  scopeKind: "owner_dm",
@@ -40277,7 +40838,7 @@ var BridgeAgent = class _BridgeAgent {
40277
40838
  });
40278
40839
  if (!r.ok) {
40279
40840
  const map = { pin_cap: "cap", secret: "secret", cleared: "cleared", room_cleared: "cleared", disabled: "mode", mode: "mode", purged: "purged", message: "message", duplicate: "invalid", invalid: "invalid", empty: "invalid" };
40280
- return refuse2(map[r.reason] ?? "store");
40841
+ return refuse3(map[r.reason] ?? "store");
40281
40842
  }
40282
40843
  await this.notes.exclusive(async () => this.unifiedReads.bumpGeneration());
40283
40844
  void this.send(channelId, pinnedLine(stripped), void 0, { agentTurn: false }).catch((e) => writeServiceLog("warn", "memory.pinned_line_failed", {}, e));
@@ -40285,7 +40846,7 @@ var BridgeAgent = class _BridgeAgent {
40285
40846
  return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true, id: r.itemIds[0] };
40286
40847
  } catch (e) {
40287
40848
  writeServiceLog("warn", "memory.note_write_failed", {}, e);
40288
- return refuse2("store");
40849
+ return refuse3("store");
40289
40850
  } finally {
40290
40851
  ctx.pendingRequests.delete(f.request_id);
40291
40852
  }
@@ -40299,16 +40860,16 @@ var BridgeAgent = class _BridgeAgent {
40299
40860
  * is invalidated in the same mutex hold, the item leaves the ranking hot set, then the bridge publishes `Forgot:`
40300
40861
  * (OQ-P2: afterPublish keeps the line out of history and the items). Every reason is a key of UNIFIED_REFUSAL.
40301
40862
  */
40302
- async forgetUnified(f, rec, channelId, ctx, refuse2, precommit) {
40303
- if (typeof f.id !== "object") return refuse2("invalid");
40863
+ async forgetUnified(f, rec, channelId, ctx, refuse3, precommit) {
40864
+ if (typeof f.id !== "object") return refuse3("invalid");
40304
40865
  const itemId = f.id.recall;
40305
40866
  const shown = ctx.shownUnified(f.message_id, ctx.token);
40306
- if (!shown || !shown.has(itemId)) return refuse2("invalid");
40867
+ if (!shown || !shown.has(itemId)) return refuse3("invalid");
40307
40868
  const text = this.unifiedReads.shownText(f.message_id, itemId);
40308
- if (text === void 0) return refuse2("invalid");
40869
+ if (text === void 0) return refuse3("invalid");
40309
40870
  const rw = await this.store.prepareRuntimeInboxRewrite(rec.message_id, bumpW9);
40310
- if (rw === "declined") return refuse2("budget");
40311
- if (rw === "no_row") return refuse2("message");
40871
+ if (rw === "declined") return refuse3("budget");
40872
+ if (rw === "no_row") return refuse3("message");
40312
40873
  const r = await this.notes.exclusive(async () => {
40313
40874
  const veto = precommit();
40314
40875
  if (veto !== null) return { ok: false, reason: veto };
@@ -40321,7 +40882,7 @@ var BridgeAgent = class _BridgeAgent {
40321
40882
  }
40322
40883
  return out;
40323
40884
  });
40324
- if (!r.ok) return refuse2(r.reason === "unknown_id" ? "invalid" : r.reason);
40885
+ if (!r.ok) return refuse3(r.reason === "unknown_id" ? "invalid" : r.reason);
40325
40886
  this.rankingNote(() => this.ranking.removed(r.itemIds));
40326
40887
  void this.send(channelId, forgotLine(text), void 0, { agentTurn: false }).catch((e) => writeServiceLog("warn", "memory.forgot_line_failed", {}, e));
40327
40888
  writeServiceLog("info", "memory.item_forgotten");
@@ -40395,8 +40956,9 @@ var BridgeAgent = class _BridgeAgent {
40395
40956
  this.fastReplyChains.set(channelId, next);
40396
40957
  }
40397
40958
  /** Count a fast reply against FAST_REPLY_MAX_JOBS and reserve its room send-floor token; returns the release. */
40398
- reserveFastReply(channelId) {
40959
+ reserveFastReply(replyChannel) {
40399
40960
  this.fastReplyJobs++;
40961
+ const channelId = this.capChannelFor(replyChannel);
40400
40962
  this.fastReplyReserved.set(channelId, (this.fastReplyReserved.get(channelId) ?? 0) + 1);
40401
40963
  return () => {
40402
40964
  this.fastReplyJobs--;
@@ -40620,6 +41182,7 @@ var BridgeAgent = class _BridgeAgent {
40620
41182
  });
40621
41183
  if (!res.ok) throw new Error(`list_channels failed: ${res.status}`);
40622
41184
  const rows = await res.json();
41185
+ for (const r of rows) if (r.channel_kind === "voice") this.listedAsVoice.add(r.channel_id);
40623
41186
  for (const r of rows) this.store.recordChannelEnclave(r.channel_id, r.enclave_id);
40624
41187
  return rows.map((r) => ({
40625
41188
  channel_id: r.channel_id,
@@ -40703,6 +41266,7 @@ var BridgeAgent = class _BridgeAgent {
40703
41266
  */
40704
41267
  recordListedChannelMeta(channels, at) {
40705
41268
  for (const c of channels) {
41269
+ if (this.listedAsVoice.has(c.channel_id)) continue;
40706
41270
  this.channelMeta.set(c.channel_id, { is_dm: c.is_dm, name: c.name, at });
40707
41271
  this.store.recordChannelIsDm(c.channel_id, c.is_dm);
40708
41272
  }
@@ -40886,7 +41450,8 @@ var BridgeAgent = class _BridgeAgent {
40886
41450
  }
40887
41451
  const self = members.get(this.cfg.identityId);
40888
41452
  if (self?.name) this.lastOwnDisplayName = self.name;
40889
- await this.resolveAndPinOwner(channelId, liveRows);
41453
+ const unclassified = this.isUnlistedChannel(channelId) && !this.voiceChannels.isNotVoice(channelId);
41454
+ if (!this.voiceChannels.isVoice(channelId) && !unclassified) await this.resolveAndPinOwner(channelId, liveRows);
40890
41455
  } catch {
40891
41456
  }
40892
41457
  }
@@ -40922,7 +41487,7 @@ var BridgeAgent = class _BridgeAgent {
40922
41487
  * held group (never the backend's row label, M-2). NEVER throws (M-4): any
40923
41488
  * exception is `failed verify_error`, and the row is acked like any other turn.
40924
41489
  */
40925
- async verifyOwnerTurn(m) {
41490
+ async verifyOwnerTurn(m, keyChannel = m.channelId) {
40926
41491
  const pin = this.cfg.ownerGrantPin?.pin ?? null;
40927
41492
  if (!pin || m.envelope.sender_id !== pin.ownerIdentityId) return OWNER_TURN_UNCHECKED;
40928
41493
  try {
@@ -40934,9 +41499,9 @@ var BridgeAgent = class _BridgeAgent {
40934
41499
  }
40935
41500
  if (v.sender_id !== pin.ownerIdentityId) return this.ownerTurnFailed(m, "malformed");
40936
41501
  let leafClass = "ok";
40937
- if (this.freshMeta(m.channelId)?.is_dm === true) {
41502
+ if (this.freshMeta(keyChannel)?.is_dm === true) {
40938
41503
  const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
40939
- if (signedDm !== void 0 && signedDm !== m.channelId) return this.ownerTurnFailed(m, "wrong_dm_channel");
41504
+ if (signedDm !== void 0 && signedDm !== keyChannel) return this.ownerTurnFailed(m, "wrong_dm_channel");
40940
41505
  leafClass = classifyOwnerDmLeaves((await this.provider.leafTreeSummary(m.channelId)).leaves, {
40941
41506
  ownerIdentityId: pin.ownerIdentityId,
40942
41507
  agentIdentityId: this.cfg.identityId,
@@ -41100,9 +41665,14 @@ var BridgeAgent = class _BridgeAgent {
41100
41665
  });
41101
41666
  }
41102
41667
  /** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
41103
- async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId, voiceScoped) {
41668
+ async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId, voiceScoped, voiceParent) {
41104
41669
  const held = await this.heldPosition(channelId);
41105
- const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict, voiceScoped });
41670
+ const keyHeld = voiceParent === void 0 ? held : await this.heldPosition(voiceParent);
41671
+ const auth = this.authorizationFor(voiceParent ?? channelId, senderId, relaySpeakerId, keyHeld.epoch, keyHeld.groupId, {
41672
+ ownerVerdict,
41673
+ voiceScoped,
41674
+ ...voiceParent === void 0 ? {} : { voice: { channelId, heldEpoch: held.epoch, heldGroupId: held.groupId } }
41675
+ });
41106
41676
  if (!(auth.audience === "team_room" && auth.tool_grants.length > 0)) return auth;
41107
41677
  if (this.roomGrantEpochs.size >= ROOM_GRANT_TRACK_MAX || held.epoch === null || held.groupId === null) {
41108
41678
  writeServiceLog("warn", "room_grant.untracked");
@@ -41155,7 +41725,10 @@ var BridgeAgent = class _BridgeAgent {
41155
41725
  const meta = this.freshMeta(channelId);
41156
41726
  const entry = this.freshRosterEntry(channelId);
41157
41727
  const roster = entry?.members;
41158
- const sender = roster?.get(senderId);
41728
+ const vEntry = opts.voice === void 0 ? void 0 : this.freshRosterEntry(opts.voice.channelId);
41729
+ const vSender = vEntry?.members.get(senderId);
41730
+ const sender = opts.voice === void 0 ? roster?.get(senderId) : vSender === void 0 ? void 0 : roster?.get(senderId) ?? vSender;
41731
+ const senderIsRelay = opts.voice === void 0 ? sender?.is_relay === true : vSender?.is_relay === true;
41159
41732
  let audience = "unknown";
41160
41733
  if (meta && sender) {
41161
41734
  if (sender.is_agent) audience = "agent_to_agent";
@@ -41167,15 +41740,15 @@ var BridgeAgent = class _BridgeAgent {
41167
41740
  let tool_grants = NO_TOOL_GRANTS;
41168
41741
  let relaySpeaker;
41169
41742
  let memoryWrite = false;
41170
- const epochFresh = rosterFreshAt(entry, heldEpoch, heldGroupId);
41171
- const ownerDmFresh = epochFresh || this.ownerDmCycleFresh.has(channelId);
41743
+ const epochFresh = rosterFreshAt(entry, heldEpoch, heldGroupId) && (opts.voice === void 0 || rosterFreshAt(vEntry, opts.voice.heldEpoch, opts.voice.heldGroupId));
41744
+ const ownerDmFresh = epochFresh || opts.voice === void 0 && this.ownerDmCycleFresh.has(channelId);
41172
41745
  if (ownerDmFresh && this.ownerDmDefaultGrants && meta?.is_dm === true && sender) {
41173
41746
  const pinned = this.channelOwner.get(channelId);
41174
41747
  const ownerId = pinned && pinned !== "anomalous" ? pinned.identityId : void 0;
41175
41748
  if (ownerId !== void 0) {
41176
41749
  const ownerDirect = audience === "owner_dm" && senderId === ownerId;
41177
41750
  const owner = roster?.get(ownerId);
41178
- const ownerRelayed = relaySpeakerId !== void 0 && sender.is_relay && relaySpeakerId === ownerId && owner !== void 0 && !owner.is_agent && owner.is_agent_owner;
41751
+ const ownerRelayed = relaySpeakerId !== void 0 && senderIsRelay && relaySpeakerId === ownerId && owner !== void 0 && !owner.is_agent && owner.is_agent_owner;
41179
41752
  if (ownerDirect) tool_grants = opts.voiceScoped === true ? RELAYED_VOICE_GRANTS : OWNER_DM_DEFAULT_READ_GRANTS;
41180
41753
  if (ownerRelayed) tool_grants = RELAYED_VOICE_GRANTS;
41181
41754
  if (ownerRelayed) relaySpeaker = "owner";
@@ -41854,6 +42427,9 @@ var BridgeAgent = class _BridgeAgent {
41854
42427
  async ingestCycleBody(receiptIds) {
41855
42428
  if (this.closingDown) return;
41856
42429
  const cycleStart = performance.now();
42430
+ let voiceListed = false;
42431
+ const cycleStartedAt = Date.now();
42432
+ const joinedInPhase0 = /* @__PURE__ */ new Set();
41857
42433
  try {
41858
42434
  const rows = await this.latency.time("recv.pull", () => this.backend.pullDelivery(this.cfg.deviceId));
41859
42435
  this.recordRecvLag(rows);
@@ -41863,9 +42439,11 @@ var BridgeAgent = class _BridgeAgent {
41863
42439
  }
41864
42440
  for (const cid of channelIds) {
41865
42441
  if (this.joinState.get(cid) !== "JOINED") {
42442
+ joinedInPhase0.add(cid);
41866
42443
  await this.reconcileJoin(cid);
41867
42444
  }
41868
42445
  }
42446
+ if (await this.resolveUnlistedChannels(channelIds, false)) voiceListed = true;
41869
42447
  for (const cid of channelIds) {
41870
42448
  if (this.freshRoster(cid) === void 0) {
41871
42449
  await this.refreshChannelRoster(cid);
@@ -41890,6 +42468,13 @@ var BridgeAgent = class _BridgeAgent {
41890
42468
  }
41891
42469
  } catch {
41892
42470
  }
42471
+ const voiceStamp = await this.voicePositionSnapshot();
42472
+ const receiveStartedAt = Date.now();
42473
+ const stampsFor = (m) => {
42474
+ if (m.receivedAt >= receiveStartedAt) return true;
42475
+ const p = this.voiceChannels.parentOf(m.channelId);
42476
+ return m.receivedAt >= cycleStartedAt && p !== void 0 && !joinedInPhase0.has(p);
42477
+ };
41893
42478
  let entries;
41894
42479
  try {
41895
42480
  entries = await this.latency.time("recv.decrypt", () => this.provider.receiveOnce(this.cfg.deviceId));
@@ -41899,7 +42484,10 @@ var BridgeAgent = class _BridgeAgent {
41899
42484
  for (const cid of entries.supersededChannelIds ?? []) {
41900
42485
  this.joinState.set(cid, "JOINED");
41901
42486
  }
41902
- if (!entries.length) return;
42487
+ if (!entries.length) {
42488
+ await this.reevaluateVoiceBindings();
42489
+ return;
42490
+ }
41903
42491
  await this.maybePruneOwnerReplay();
41904
42492
  const receivedAt = Date.now();
41905
42493
  const purgeEpochAtReceipt = /* @__PURE__ */ new Map();
@@ -41912,16 +42500,31 @@ var BridgeAgent = class _BridgeAgent {
41912
42500
  if (this.unifiedOn && !itemCaptureAtReceipt.has(m.channelId)) itemCaptureAtReceipt.set(m.channelId, this.store.itemCapture(m.channelId));
41913
42501
  this.recallReceipt.set(m.messageId, { purgeEpoch: purgeEpochAtReceipt.get(m.channelId), modeEpoch: recallModeEpochAtReceipt });
41914
42502
  receiptIds.push(m.messageId);
42503
+ const stamp = voiceStamp.get(m.channelId);
42504
+ if (stamp !== void 0) this.voiceChannels.noteReceipt(m.messageId, stampsFor(m) ? stamp : null);
42505
+ else if (this.voiceChannels.isVoice(m.channelId) || this.isUnlistedChannel(m.channelId) && !this.voiceChannels.isResolved(m.channelId)) {
42506
+ this.voiceChannels.noteReceipt(m.messageId, null);
42507
+ }
41915
42508
  }
41916
- let metaRefreshed = false;
41917
- if (entries.some((m) => this.freshMeta(m.channelId) === void 0)) {
42509
+ if (await this.resolveUnlistedChannels(entries.map((m) => m.channelId), voiceListed)) voiceListed = true;
42510
+ await this.reevaluateVoiceBindings();
42511
+ entries = await this.gateVoiceEntries(entries);
42512
+ let metaRefreshed = voiceListed;
42513
+ if (!voiceListed && entries.some((m) => this.freshMeta(this.keyChannelForFrame(m)) === void 0)) {
41918
42514
  await this.refreshChannelMeta();
41919
42515
  metaRefreshed = true;
41920
42516
  }
41921
- await this.refreshRostersForCycle(entries.map((m) => m.channelId));
42517
+ await this.refreshRostersForCycle(entries.flatMap((m) => {
42518
+ const key = this.keyChannelForFrame(m);
42519
+ return key === m.channelId ? [m.channelId] : [m.channelId, key];
42520
+ }));
41922
42521
  const relayMissRefreshed = /* @__PURE__ */ new Set();
41923
42522
  const acked = [];
41924
42523
  for (const m of entries) {
42524
+ if (this.voiceChannels.isVoice(m.channelId) && !this.voiceFrameParent.has(m.messageId)) {
42525
+ acked.push(m.queueId);
42526
+ continue;
42527
+ }
41925
42528
  try {
41926
42529
  if (m.voiceMode === false) this.callBuffer.reset(m.channelId);
41927
42530
  const encId = m.envelope.enclave_id;
@@ -42082,7 +42685,9 @@ var BridgeAgent = class _BridgeAgent {
42082
42685
  }
42083
42686
  if (typeof parsed.text === "string") {
42084
42687
  if (parsed.text.trim() === VOICE_CALL_START_MARKER) this.callBuffer.reset(m.channelId);
42085
- const meta = this.freshMeta(m.channelId);
42688
+ const keyChannel = this.keyChannelForFrame(m);
42689
+ const voiceParent = keyChannel !== m.channelId ? keyChannel : void 0;
42690
+ const meta = this.freshMeta(keyChannel);
42086
42691
  const senderType = m.envelope.sender_type;
42087
42692
  const senderIsAgent = senderType === "agent" ? true : senderType === "human" ? false : void 0;
42088
42693
  const claimsRelaySpeaker = parsed.relay === true && typeof parsed.speaker_identity_id === "string" && typeof parsed.speaker_display_name === "string";
@@ -42129,14 +42734,15 @@ var BridgeAgent = class _BridgeAgent {
42129
42734
  senderIsRelay: this.freshRoster(m.channelId)?.get(m.envelope.sender_id)?.is_relay === true,
42130
42735
  text: parsed.text
42131
42736
  });
42132
- const ownerVerdict = await this.verifyOwnerTurn(m);
42737
+ const ownerVerdict = await this.verifyOwnerTurn(m, keyChannel);
42133
42738
  const turnAuthorization = await this.authorizeTurn(
42134
42739
  m.channelId,
42135
42740
  m.messageId,
42136
42741
  m.envelope.sender_id,
42137
42742
  ownerVerdict,
42138
42743
  speaker?.identityId,
42139
- voiceScoped
42744
+ voiceScoped,
42745
+ voiceParent
42140
42746
  );
42141
42747
  await this.prepareNotesForTurn(m.channelId, turnAuthorization);
42142
42748
  const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
@@ -42163,11 +42769,12 @@ var BridgeAgent = class _BridgeAgent {
42163
42769
  }
42164
42770
  let fastPathRoomBlocked = false;
42165
42771
  if (!coalescible && fastPathEligible && meta && meta.is_dm === false) {
42166
- const reserved = this.fastReplyReserved.get(m.channelId) ?? 0;
42167
- if (this.rateFloor.available(`room:${m.channelId}`) - reserved < 2) fastPathRoomBlocked = true;
42772
+ const floorKey = this.capChannelFor(m.channelId);
42773
+ const reserved = this.fastReplyReserved.get(floorKey) ?? 0;
42774
+ if (this.rateFloor.available(`room:${floorKey}`) - reserved < 2) fastPathRoomBlocked = true;
42168
42775
  }
42169
42776
  if (fastPathRoomBlocked) this.fastPathSkipped++;
42170
- const fastPathFiring = fastPathEligible && !fastPathRoomBlocked && !this.pausedChannels.has(m.channelId);
42777
+ const fastPathFiring = fastPathEligible && !fastPathRoomBlocked && !this.pausedFor(m.channelId);
42171
42778
  const fastGreeting = isCallStart && fastPathFiring;
42172
42779
  const runtimeVoice = (ackSent) => m.voiceMode ? {
42173
42780
  voice: true,
@@ -42273,11 +42880,12 @@ var BridgeAgent = class _BridgeAgent {
42273
42880
  void recordInboundMemory(this.memory, channelId, senderId, text, firstId, "voice", win.purgeEpoch);
42274
42881
  let roomBlocked = false;
42275
42882
  if (fastPathEligible && isRoom) {
42276
- const reserved = this.fastReplyReserved.get(channelId) ?? 0;
42277
- if (this.rateFloor.available(`room:${channelId}`) - reserved < 2) roomBlocked = true;
42883
+ const floorKey = this.capChannelFor(channelId);
42884
+ const reserved = this.fastReplyReserved.get(floorKey) ?? 0;
42885
+ if (this.rateFloor.available(`room:${floorKey}`) - reserved < 2) roomBlocked = true;
42278
42886
  }
42279
42887
  if (roomBlocked) this.fastPathSkipped++;
42280
- const plan = fastPathEligible && !roomBlocked && !this.pausedChannels.has(channelId) ? this.planFastPath(firstId, text, false) : void 0;
42888
+ const plan = fastPathEligible && !roomBlocked && !this.pausedFor(channelId) ? this.planFastPath(firstId, text, false) : void 0;
42281
42889
  if (!plan || plan.kind === "dup" || plan.kind === "skip") {
42282
42890
  if (plan?.kind === "skip" && plan.full) this.fastPathSkipped++;
42283
42891
  await this.settleHeldRuntimeMessage(merged, true);
@@ -42381,6 +42989,7 @@ var BridgeAgent = class _BridgeAgent {
42381
42989
  }
42382
42990
  }
42383
42991
  for (const m of entries) this.recallReceipt.delete(m.messageId);
42992
+ for (const m of entries) this.voiceFrameParent.delete(m.messageId);
42384
42993
  if (acked.length) {
42385
42994
  try {
42386
42995
  await this.latency.time("recv.ack", () => this.provider.ackDelivery(this.cfg.deviceId, acked));
@@ -42661,8 +43270,9 @@ var BridgeAgent = class _BridgeAgent {
42661
43270
  }
42662
43271
  }
42663
43272
  /** Spec §7.5 (T-14c, M-4): the runtime reports an executed-call cap for a room. Never throws. */
42664
- noteRoomCapReached(channelId) {
42665
- if (typeof channelId !== "string" || this.closingDown) return;
43273
+ noteRoomCapReached(reported) {
43274
+ if (typeof reported !== "string" || this.closingDown) return;
43275
+ const channelId = this.capChannelFor(reported);
42666
43276
  if (this.roomSearchDailyCap === 0) return;
42667
43277
  if (this.freshMeta(channelId)?.is_dm !== false || this.channelRoster.get(channelId)?.crossOrg !== false) return;
42668
43278
  const day = utcDayOf(Date.now());
@@ -42719,8 +43329,9 @@ ${channelId}`;
42719
43329
  const a = entry.authorization;
42720
43330
  if (!a || a.audience !== "team_room" || !a.tool_grants.some((g) => g.tool === "web_search")) return entry;
42721
43331
  const day = utcDayOf(Date.now());
43332
+ const room = this.capChannelFor(entry.channel_id);
42722
43333
  if (this.roomSearchDailyCap === 0) {
42723
- this.logRoomCap(day, entry.channel_id, "operator");
43334
+ this.logRoomCap(day, room, "operator");
42724
43335
  return this.withoutWebSearch(entry);
42725
43336
  }
42726
43337
  if (this.agentRoomSearchDailyCap === 0) {
@@ -42728,9 +43339,9 @@ ${channelId}`;
42728
43339
  return this.withoutWebSearch(entry);
42729
43340
  }
42730
43341
  try {
42731
- const n = this.store.countRoomSearchGrant(entry.channel_id, day);
42732
- if (n === this.roomSearchDailyCap && this.store.markRoomSearchCapReached(entry.channel_id, day, "bridge")) {
42733
- this.logRoomCap(day, entry.channel_id, "bridge");
43342
+ const n = this.store.countRoomSearchGrant(room, day);
43343
+ if (n === this.roomSearchDailyCap && this.store.markRoomSearchCapReached(room, day, "bridge")) {
43344
+ this.logRoomCap(day, room, "bridge");
42734
43345
  }
42735
43346
  if (n > this.roomSearchDailyCap) return this.withoutWebSearch(entry);
42736
43347
  const total = this.store.countRoomSearchGrant(AGENT_TOTAL_KEY, day);
@@ -42827,9 +43438,15 @@ ${channelId}`;
42827
43438
  * PAUSE_RECHECK_MS window, so release does not depend on new traffic.
42828
43439
  */
42829
43440
  heldForPause(channelId) {
42830
- const at = this.pausedChannels.get(channelId);
43441
+ let key = channelId;
43442
+ let at = this.pausedChannels.get(channelId);
43443
+ const parent3 = at === void 0 ? this.voiceChannels.parentOf(channelId) : void 0;
43444
+ if (parent3 !== void 0) {
43445
+ key = parent3;
43446
+ at = this.pausedChannels.get(parent3);
43447
+ }
42831
43448
  if (at === void 0) return false;
42832
- this.schedulePauseRecheck(channelId, at);
43449
+ this.schedulePauseRecheck(key, at);
42833
43450
  return true;
42834
43451
  }
42835
43452
  schedulePauseRecheck(channelId, confirmedAt) {
@@ -42858,7 +43475,7 @@ ${channelId}`;
42858
43475
  if (await this.probeOwnPause(channelId) === true) {
42859
43476
  const now = Date.now();
42860
43477
  this.pausedChannels.set(channelId, now);
42861
- if (this.buffer.some((m) => m.channel_id === channelId)) this.schedulePauseRecheck(channelId, now);
43478
+ if (this.buffer.some((m) => m.channel_id === channelId || this.voiceChannels.parentOf(m.channel_id) === channelId)) this.schedulePauseRecheck(channelId, now);
42862
43479
  return;
42863
43480
  }
42864
43481
  this.clearPause(channelId);
@@ -43337,6 +43954,10 @@ ${channelId}`;
43337
43954
  custody_store_unavailable: this.provider.custodyHealth().storeUnavailable,
43338
43955
  failed_attachments: this.failedAttachments,
43339
43956
  refused_vault_refs: this.refusedVaultRefs,
43957
+ voice_frames_refused: this.voiceFramesRefused,
43958
+ voice_frames_filtered: this.voiceFramesFiltered,
43959
+ voice_receipt_cap_reached: this.voiceChannels.receiptCapReached(),
43960
+ voice_relay_in_parent: this.voiceChannels.relayInParentCount(),
43340
43961
  fast_path_sent: this.fastPathSent,
43341
43962
  fast_path_failed: this.fastPathFailed,
43342
43963
  fast_path_skipped: this.fastPathSkipped,
@@ -46000,7 +46621,7 @@ var PIN_COPY = Object.freeze({
46000
46621
  usage: "usage: agent-bridge pin --credential <path> --file <owner-signing-key.json> --agent <agent_identity_id>"
46001
46622
  // [COPY, approved by Eric 2026-10-12] (P-4)
46002
46623
  });
46003
- var refuse = (reason) => ({ ok: false, reason });
46624
+ var refuse2 = (reason) => ({ ok: false, reason });
46004
46625
  function isSymlink(path) {
46005
46626
  try {
46006
46627
  return lstatSync8(path).isSymbolicLink();
@@ -46030,33 +46651,33 @@ function runPinCommand(opts) {
46030
46651
  const credDir = dirname12(resolve2(opts.credentialPath));
46031
46652
  const target = join23(credDir, PIN_FILE_NAME);
46032
46653
  try {
46033
- if (realpathSync2(dirname12(resolve2(opts.pinSourcePath))) !== realpathSync2(credDir)) return refuse("directory_mismatch");
46654
+ if (realpathSync2(dirname12(resolve2(opts.pinSourcePath))) !== realpathSync2(credDir)) return refuse2("directory_mismatch");
46034
46655
  } catch {
46035
- return refuse("io_error");
46656
+ return refuse2("io_error");
46036
46657
  }
46037
46658
  try {
46038
- if (isSymlink(opts.credentialPath) || isSymlink(opts.pinSourcePath) || isSymlink(target)) return refuse("symlink_refused");
46659
+ if (isSymlink(opts.credentialPath) || isSymlink(opts.pinSourcePath) || isSymlink(target)) return refuse2("symlink_refused");
46039
46660
  } catch {
46040
- return refuse("io_error");
46661
+ return refuse2("io_error");
46041
46662
  }
46042
46663
  let source;
46043
46664
  try {
46044
46665
  source = readJson(opts.pinSourcePath);
46045
46666
  } catch {
46046
- return refuse("pin_invalid");
46667
+ return refuse2("pin_invalid");
46047
46668
  }
46048
46669
  const parsed = parsePinObject(source);
46049
- if (!parsed.ok) return refuse("pin_invalid");
46670
+ if (!parsed.ok) return refuse2("pin_invalid");
46050
46671
  let cred;
46051
46672
  try {
46052
46673
  const c = readJson(opts.credentialPath);
46053
- if (typeof c !== "object" || c === null || Array.isArray(c)) return refuse("credential_invalid");
46674
+ if (typeof c !== "object" || c === null || Array.isArray(c)) return refuse2("credential_invalid");
46054
46675
  cred = c;
46055
46676
  } catch {
46056
- return refuse("credential_invalid");
46677
+ return refuse2("credential_invalid");
46057
46678
  }
46058
- for (const k of REQUIRED) if (typeof cred[k] !== "string" || !cred[k]) return refuse("credential_invalid");
46059
- if (cred.identity_id !== opts.agentIdentityId) return refuse("agent_mismatch");
46679
+ for (const k of REQUIRED) if (typeof cred[k] !== "string" || !cred[k]) return refuse2("credential_invalid");
46680
+ if (cred.identity_id !== opts.agentIdentityId) return refuse2("agent_mismatch");
46060
46681
  opts.announce(cred.identity_id);
46061
46682
  const newKey = parsed.pin.publicKeyB64;
46062
46683
  let priorFileKey;
@@ -46081,7 +46702,7 @@ function runPinCommand(opts) {
46081
46702
  } catch {
46082
46703
  }
46083
46704
  }
46084
- return refuse("io_error");
46705
+ return refuse2("io_error");
46085
46706
  };
46086
46707
  const step = (s) => opts.afterStep?.(s);
46087
46708
  try {
@@ -46589,7 +47210,7 @@ function realRunModeDeps(credentialPath, storePath) {
46589
47210
  }
46590
47211
  async function runMode(deps) {
46591
47212
  const { credentialPath, storePath, log } = deps;
46592
- const refuse2 = (reason, extra, e) => {
47213
+ const refuse3 = (reason, extra, e) => {
46593
47214
  const fields = {
46594
47215
  reason: ServiceLogEnum.of(reason, [
46595
47216
  "credential_not_found",
@@ -46621,18 +47242,18 @@ async function runMode(deps) {
46621
47242
  cfg = loadConfig({ credentialPath, storePath });
46622
47243
  daemon = parseDaemonArgs(deps.argv, deps.env);
46623
47244
  } catch (e) {
46624
- if (e instanceof ConfigError) refuse2(e.reason, { field: e.field, source: e.source }, e);
46625
- else refuse2("unknown", {}, e);
47245
+ if (e instanceof ConfigError) refuse3(e.reason, { field: e.field, source: e.source }, e);
47246
+ else refuse3("unknown", {}, e);
46626
47247
  return deps.exit(1);
46627
47248
  }
46628
47249
  try {
46629
47250
  validateBind(daemon.bind, daemon.tls);
46630
47251
  } catch (e) {
46631
- refuse2("bind_refused", {}, e);
47252
+ refuse3("bind_refused", {}, e);
46632
47253
  return deps.exit(1);
46633
47254
  }
46634
47255
  if (!CANONICAL_UUID2.test(cfg.identityId)) {
46635
- refuse2("identity_id_invalid", { field: "identity_id" });
47256
+ refuse3("identity_id_invalid", { field: "identity_id" });
46636
47257
  return deps.exit(1);
46637
47258
  }
46638
47259
  const lockDir = join25(deps.home, ".securecoms", "locks");