@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/index.js CHANGED
@@ -14687,6 +14687,18 @@ async function sendWithSelfHeal(session, channelId, build, publish) {
14687
14687
  // ../connect-sdk-ts/dist/groupkey/stub.js
14688
14688
  import { sha256 as sha2563 } from "@noble/hashes/sha2";
14689
14689
 
14690
+ // ../connect-sdk-ts/dist/receive/inbox.js
14691
+ function parseOpenedAt(v) {
14692
+ if (typeof v !== "object" || v === null)
14693
+ return void 0;
14694
+ const o = v;
14695
+ if (typeof o.groupId !== "string" || o.groupId.length === 0)
14696
+ return void 0;
14697
+ if (typeof o.epoch !== "number" || !Number.isSafeInteger(o.epoch) || o.epoch < 0)
14698
+ return void 0;
14699
+ return { groupId: o.groupId, epoch: o.epoch };
14700
+ }
14701
+
14690
14702
  // ../connect-sdk-ts/dist/spans/names.js
14691
14703
  var SPAN = {
14692
14704
  MESSAGE_SENT: "agentvault.connect.message.sent",
@@ -17415,7 +17427,9 @@ function decodeCustodyEntry(bytes) {
17415
17427
  if (typeof plaintextHex !== "string" || typeof rest.queueId !== "string" || typeof rest.channelId !== "string" || typeof rest.custodiedAt !== "number") {
17416
17428
  throw new Error("malformed custody entry");
17417
17429
  }
17418
- return { ...rest, plaintext: hexToBytes22(plaintextHex) };
17430
+ const { openedAt, ...others } = rest;
17431
+ const position = parseOpenedAt(openedAt);
17432
+ return { ...others, ...position ? { openedAt: position } : {}, plaintext: hexToBytes22(plaintextHex) };
17419
17433
  }
17420
17434
  function custodyAad(identityId, queueId) {
17421
17435
  return new TextEncoder().encode(`securecoms-custody-v1\0${identityId}\0${queueId}`);
@@ -22068,7 +22082,7 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
22068
22082
  * read #750 made in here is gone (ruling §8.2).
22069
22083
  */
22070
22084
  async trySupersede(channelId, row, callerHeldGroupId, owner, mode, pullRows) {
22071
- const refuse = (msg) => {
22085
+ const refuse2 = (msg) => {
22072
22086
  this.recordDecryptError(row, new Error(msg));
22073
22087
  const now = Date.now();
22074
22088
  for (const [k, v] of this.supersedeRefusals) {
@@ -22101,34 +22115,34 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
22101
22115
  att = await this.o.backend.fetchResetAttestation(row.group_id);
22102
22116
  }
22103
22117
  if (!att) {
22104
- return refuse(`supersede refused: no attestation for group ${row.group_id}`);
22118
+ return refuse2(`supersede refused: no attestation for group ${row.group_id}`);
22105
22119
  }
22106
22120
  if (att.signerIdentityId !== owner.identityId) {
22107
- return refuse("supersede refused: signer is not the pinned owner");
22121
+ return refuse2("supersede refused: signer is not the pinned owner");
22108
22122
  }
22109
22123
  let content;
22110
22124
  try {
22111
22125
  content = JSON.parse(att.signedContent);
22112
22126
  } catch {
22113
- return refuse("supersede refused: unparseable signed_content");
22127
+ return refuse2("supersede refused: unparseable signed_content");
22114
22128
  }
22115
22129
  if (content.channel_id !== row.connect_channel_id || content.new_group_id !== row.group_id) {
22116
- return refuse("supersede refused: attestation channel/group mismatch");
22130
+ return refuse2("supersede refused: attestation channel/group mismatch");
22117
22131
  }
22118
22132
  if (content.purpose !== RESET_SUPERSEDE_PURPOSE) {
22119
- return refuse("supersede refused: attestation purpose is not connect-reset-supersede");
22133
+ return refuse2("supersede refused: attestation purpose is not connect-reset-supersede");
22120
22134
  }
22121
22135
  const targetCreatedMs = signedCreatedAtMs(content);
22122
22136
  if (targetCreatedMs === null) {
22123
- return refuse("supersede refused: unparseable attestation created_at");
22137
+ return refuse2("supersede refused: unparseable attestation created_at");
22124
22138
  }
22125
22139
  const targetPrev = signedPrevLink(content, row.group_id);
22126
22140
  if (targetPrev === null) {
22127
- return refuse("supersede refused: unparseable attestation prev_group_id");
22141
+ return refuse2("supersede refused: unparseable attestation prev_group_id");
22128
22142
  }
22129
22143
  const ok = await verifyApprovalArtifact({ content, signature: att.signatureHex, signerPublicKey: owner.publicKeyHex, createdAt: "", version: 1 }, owner.publicKeyHex);
22130
22144
  if (!ok) {
22131
- return refuse("supersede refused: signature does not verify against pinned owner key");
22145
+ return refuse2("supersede refused: signature does not verify against pinned owner key");
22132
22146
  }
22133
22147
  const targetLink = targetPrev.chained ? { createdAtMs: targetCreatedMs, chained: true, prevGroupId: targetPrev.prev } : { createdAtMs: targetCreatedMs, chained: false, prevGroupId: null };
22134
22148
  if (callerHeldGroupId !== null && callerHeldGroupId !== row.group_id) {
@@ -22138,7 +22152,7 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
22138
22152
  if (v.kind === "refuse") {
22139
22153
  if (v.notNewer)
22140
22154
  this.supersedeNotNewer.add(`${channelId}:${row.group_id}`);
22141
- return refuse(v.reason);
22155
+ return refuse2(v.reason);
22142
22156
  }
22143
22157
  }
22144
22158
  const bundle = await this.o.store.loadPendingBundle(this.o.deviceId);
@@ -22146,7 +22160,7 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
22146
22160
  if (!bundle && !supersedeSlot) {
22147
22161
  if (mode === "adopt")
22148
22162
  return { kind: "no_bundle" };
22149
- return refuse("supersede refused: no pending KeyPackage bundle to open the new Welcome");
22163
+ return refuse2("supersede refused: no pending KeyPackage bundle to open the new Welcome");
22150
22164
  }
22151
22165
  return await this.locks.run(channelId, async () => {
22152
22166
  let inLockHeld;
@@ -22193,7 +22207,7 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
22193
22207
  return { kind: "unopenable", requested };
22194
22208
  }
22195
22209
  if (new TextDecoder().decode(mgr.groupId) !== row.group_id) {
22196
- return refuse("supersede refused: post-join group id != attested new_group_id");
22210
+ return refuse2("supersede refused: post-join group id != attested new_group_id");
22197
22211
  }
22198
22212
  let dead = null;
22199
22213
  try {
@@ -23318,15 +23332,20 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
23318
23332
  done();
23319
23333
  };
23320
23334
  };
23335
+ const fromManager = async (mgr) => {
23336
+ const openedAt = { groupId: new TextDecoder().decode(mgr.groupId), epoch: Number(mgr.epoch) };
23337
+ const key = await mgr.exportSecret(LABEL, buildExporterContext(envelope.enclave_id, envelope.channel_id, envelope.mls_group_epoch), 32);
23338
+ return { key, openedAt };
23339
+ };
23321
23340
  const derive = keyFrom ? () => {
23322
23341
  takeInFlight();
23323
- return keyFrom.exportSecret(LABEL, buildExporterContext(envelope.enclave_id, envelope.channel_id, envelope.mls_group_epoch), 32);
23324
- } : () => {
23342
+ return fromManager(keyFrom);
23343
+ } : async () => {
23325
23344
  takeInFlight();
23326
- return this.exporterKeyUnfenced(envelope.enclave_id, envelope.channel_id, envelope.mls_group_epoch);
23345
+ return fromManager(await this.manager(envelope.channel_id));
23327
23346
  };
23328
23347
  try {
23329
- const key = opts.lockHeld ? await derive() : await this.locks.run(envelope.channel_id, derive);
23348
+ const { key, openedAt } = opts.lockHeld ? await derive() : await this.locks.run(envelope.channel_id, derive);
23330
23349
  const aad = await buildAssociatedData({
23331
23350
  message_id: envelope.message_id,
23332
23351
  channel_id: envelope.channel_id,
@@ -23350,7 +23369,8 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
23350
23369
  voiceMode: row.voice_mode ?? void 0,
23351
23370
  voiceDefaultResponder: row.voice_default_responder ?? void 0,
23352
23371
  ...typeof row.identity_signature === "string" && row.identity_signature ? { identitySignature: row.identity_signature } : {},
23353
- payloadHex: row.payload
23372
+ payloadHex: row.payload,
23373
+ openedAt
23354
23374
  };
23355
23375
  const custodiedAt = this.nowMs();
23356
23376
  this.custody.set(row.queue_id, entry);
@@ -26084,6 +26104,7 @@ function openSchema(db) {
26084
26104
  ensureToolGrantTables(db);
26085
26105
  ensureToolGrantAppliedTable(db);
26086
26106
  ensureRoomSearchCapTable(db);
26107
+ ensureVoiceChannelParentTable(db);
26087
26108
  db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
26088
26109
  }
26089
26110
  function ensureMlsCustody(db) {
@@ -26269,6 +26290,16 @@ function ensureToolGrantAppliedTable(db) {
26269
26290
  );
26270
26291
  `);
26271
26292
  }
26293
+ function ensureVoiceChannelParentTable(db) {
26294
+ db.exec(`
26295
+ CREATE TABLE IF NOT EXISTS voice_channel_parent (
26296
+ identity_id TEXT NOT NULL,
26297
+ voice_channel_id TEXT NOT NULL,
26298
+ parent_channel_id TEXT NOT NULL,
26299
+ PRIMARY KEY (identity_id, voice_channel_id)
26300
+ );
26301
+ `);
26302
+ }
26272
26303
  function ensureRoomSearchCapTable(db) {
26273
26304
  db.exec(`
26274
26305
  CREATE TABLE IF NOT EXISTS room_search_cap (
@@ -26960,6 +26991,21 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
26960
26991
  VALUES (?,?,?,?)
26961
26992
  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());
26962
26993
  }
26994
+ /** ADR-0024 Task 20 (final review I-1): records V -> P once; a later, different parent is ignored (immutable link). */
26995
+ recordVoiceChannelParent(voiceChannelId, parentChannelId) {
26996
+ this.db.prepare(`INSERT INTO voice_channel_parent (identity_id, voice_channel_id, parent_channel_id) VALUES (?,?,?)
26997
+ ON CONFLICT(identity_id, voice_channel_id) DO NOTHING`).run(this.id, voiceChannelId, parentChannelId);
26998
+ }
26999
+ /** The recorded parent of a voice channel, if any. */
27000
+ voiceChannelParent(voiceChannelId) {
27001
+ const row = this.db.prepare("SELECT parent_channel_id FROM voice_channel_parent WHERE identity_id=? AND voice_channel_id=?").get(this.id, voiceChannelId);
27002
+ return row?.parent_channel_id;
27003
+ }
27004
+ /** Every recorded voice channel of a parent conversation. */
27005
+ voiceChannelsOf(parentChannelId) {
27006
+ const rows = this.db.prepare("SELECT voice_channel_id FROM voice_channel_parent WHERE identity_id=? AND parent_channel_id=?").all(this.id, parentChannelId);
27007
+ return rows.map((r) => r.voice_channel_id);
27008
+ }
26963
27009
  getChannelMeta(channelId) {
26964
27010
  const row = this.db.prepare("SELECT enclave_id, is_dm FROM channel_meta WHERE identity_id=? AND channel_id=?").get(this.id, channelId);
26965
27011
  if (!row)
@@ -31783,7 +31829,7 @@ var EmbedderClient = class {
31783
31829
  resolve(ok);
31784
31830
  }
31785
31831
  }
31786
- const refuse = (reason) => {
31832
+ const refuse2 = (reason) => {
31787
31833
  s.destroy();
31788
31834
  if (!settled) {
31789
31835
  this.fail(reason);
@@ -31802,22 +31848,22 @@ var EmbedderClient = class {
31802
31848
  return;
31803
31849
  }
31804
31850
  const f2 = this.chan.open(line);
31805
- if (!f2) return refuse("embedder_unavailable");
31851
+ if (!f2) return refuse2("embedder_unavailable");
31806
31852
  this.onFrame(f2);
31807
31853
  return;
31808
31854
  }
31809
31855
  const f = JSON.parse(line);
31810
- if (!f || typeof f !== "object") return refuse("embedder_unavailable");
31856
+ if (!f || typeof f !== "object") return refuse2("embedder_unavailable");
31811
31857
  if (!ns) {
31812
- if (f.t !== "challenge" || f.protocol !== EMBEDDER_PROTOCOL || typeof f.ns !== "string" || !/^[0-9a-f]{32}$/.test(f.ns)) return refuse(f.t === "challenge" ? "embedder_mismatch" : "embedder_unavailable");
31858
+ 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");
31813
31859
  ns = f.ns;
31814
31860
  s.write(JSON.stringify({ t: "hello", nc, mac_c: macClient(token, ns, nc) }) + "\n");
31815
31861
  return;
31816
31862
  }
31817
- if (f.t !== "hello" || typeof f.mac_s !== "string" || !macEqual(f.mac_s, macServer(token, ns, nc))) return refuse("embedder_unavailable");
31863
+ if (f.t !== "hello" || typeof f.mac_s !== "string" || !macEqual(f.mac_s, macServer(token, ns, nc))) return refuse2("embedder_unavailable");
31818
31864
  const model = this.m.files.find((x) => x.name === "model.onnx");
31819
31865
  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;
31820
- if (!ok) return refuse("embedder_mismatch");
31866
+ if (!ok) return refuse2("embedder_mismatch");
31821
31867
  if (this.closed) {
31822
31868
  s.destroy();
31823
31869
  return done(true);
@@ -31835,9 +31881,9 @@ var EmbedderClient = class {
31835
31881
  this.settleReady(true);
31836
31882
  done(true);
31837
31883
  } catch {
31838
- refuse("embedder_unavailable");
31884
+ refuse2("embedder_unavailable");
31839
31885
  }
31840
- }, () => refuse("embedder_unavailable")));
31886
+ }, () => refuse2("embedder_unavailable")));
31841
31887
  });
31842
31888
  }
31843
31889
  onFrame(f) {
@@ -35175,6 +35221,213 @@ function roomIsCrossOrg(rows) {
35175
35221
  return unknown ? null : false;
35176
35222
  }
35177
35223
 
35224
+ // src/voice-channel-binding.ts
35225
+ var BOUND = Object.freeze({ bound: true });
35226
+ var refuse = (reason) => ({ bound: false, reason });
35227
+ function evaluateVoiceBinding(v, p, isVRelay) {
35228
+ if (v.ownLeafIndex === null || v.leaves.length === 0) return refuse("v_not_held");
35229
+ if (p.ownLeafIndex === null || p.leaves.length === 0) return refuse("p_not_held");
35230
+ if (v.leaves.some((l) => !l.basic || l.identity === null)) return refuse("non_basic_leaf");
35231
+ const pIds = /* @__PURE__ */ new Set();
35232
+ for (const l of p.leaves) if (l.basic && l.identity !== null) pIds.add(l.identity);
35233
+ const nonParent = /* @__PURE__ */ new Set();
35234
+ for (const l of v.leaves) if (!pIds.has(l.identity)) nonParent.add(l.identity);
35235
+ if (nonParent.size > 1) return refuse("two_non_parent");
35236
+ if (isVRelay === void 0) return nonParent.size === 0 ? BOUND : refuse("relay_unknown");
35237
+ const relays = vRelayIdentities(v, isVRelay);
35238
+ if (relays.size > 1) return refuse("two_relays");
35239
+ const [relay] = relays;
35240
+ for (const id of nonParent) if (id !== relay) return refuse("foreign_leaf");
35241
+ return BOUND;
35242
+ }
35243
+ function vRelayIdentities(v, isVRelay) {
35244
+ const relays = /* @__PURE__ */ new Set();
35245
+ for (const l of v.leaves) if (l.basic && l.identity !== null && isVRelay(l.identity)) relays.add(l.identity);
35246
+ return relays;
35247
+ }
35248
+ function voiceRelayIdentity(v, isVRelay) {
35249
+ if (isVRelay === void 0) return void 0;
35250
+ const relays = vRelayIdentities(v, isVRelay);
35251
+ return relays.size === 1 ? [...relays][0] : void 0;
35252
+ }
35253
+ function parseVoiceResolution(channelId, status, body) {
35254
+ if (status === 404) return { kind: "absent" };
35255
+ if (status !== 200) return { kind: "error" };
35256
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return { kind: "error" };
35257
+ const b = body;
35258
+ if (b.channel_id !== channelId || typeof b.channel_kind !== "string") return { kind: "error" };
35259
+ if (b.channel_kind !== "voice") return { kind: "not_voice" };
35260
+ const parent3 = b.parent_channel_id;
35261
+ if (typeof parent3 !== "string" || parent3.length === 0 || parent3 === channelId) return { kind: "error" };
35262
+ return { kind: "voice", parentChannelId: parent3 };
35263
+ }
35264
+ var VOICE_RESOLVE_RETRY_BASE_MS = 3e4;
35265
+ var VOICE_RESOLVE_RETRY_MAX_MS = 10 * 6e4;
35266
+ var VOICE_RESOLVE_ABSENT_HELD_MAX_MS = 6e4;
35267
+ var VOICE_RECEIPT_CAP = 1e4;
35268
+ var VOICE_RECEIPT_MAX_AGE_MS = 15 * 6e4;
35269
+ function voiceParentStamp(p) {
35270
+ return JSON.stringify([p.groupId, p.epoch]);
35271
+ }
35272
+ var VoiceChannelRegistry = class {
35273
+ parents = /* @__PURE__ */ new Map();
35274
+ notVoice = /* @__PURE__ */ new Set();
35275
+ failures = /* @__PURE__ */ new Map();
35276
+ bindings = /* @__PURE__ */ new Map();
35277
+ generations = /* @__PURE__ */ new Map();
35278
+ /** Fix I-1 / Task 20 R1: message id -> P's held position before the batch that first received it; null = not knowable. */
35279
+ receipts = /* @__PURE__ */ new Map();
35280
+ /** Final review M-d: arrivals not recorded because the map was full of young records. */
35281
+ capReached = 0;
35282
+ /** True when a lookup should run now: never once resolved; after a failure, only past its back-off. */
35283
+ resolveDue(channelId, now, opts = {}) {
35284
+ if (this.parents.has(channelId) || this.notVoice.has(channelId)) return false;
35285
+ const f = this.failures.get(channelId);
35286
+ if (f === void 0) return true;
35287
+ let wait = Math.min(VOICE_RESOLVE_RETRY_MAX_MS, VOICE_RESOLVE_RETRY_BASE_MS * 2 ** Math.min(f.attempts - 1, 20));
35288
+ if (f.kind === "absent" && opts.heldGroup === true) wait = Math.min(wait, VOICE_RESOLVE_ABSENT_HELD_MAX_MS);
35289
+ return now - f.at >= wait;
35290
+ }
35291
+ record(channelId, r, now) {
35292
+ if (r.kind === "voice") {
35293
+ this.parents.set(channelId, r.parentChannelId);
35294
+ this.failures.delete(channelId);
35295
+ } else if (r.kind === "not_voice") {
35296
+ this.notVoice.add(channelId);
35297
+ this.failures.delete(channelId);
35298
+ } else {
35299
+ const prev = this.failures.get(channelId);
35300
+ this.failures.set(channelId, { at: now, attempts: (prev?.attempts ?? 0) + 1, kind: r.kind });
35301
+ }
35302
+ }
35303
+ /** True once get_channel has answered for this channel (voice or not). */
35304
+ isResolved(channelId) {
35305
+ return this.parents.has(channelId) || this.notVoice.has(channelId);
35306
+ }
35307
+ /** Final review M-a: true once get_channel answered this channel's own row with a kind other than voice. */
35308
+ isNotVoice(channelId) {
35309
+ return this.notVoice.has(channelId);
35310
+ }
35311
+ /** Final review (R∈P): bound Vs whose admitted R is also a P leaf (the pre-stamp shape; a sunset signal). */
35312
+ relayInParentCount() {
35313
+ let n = 0;
35314
+ for (const [v, e] of this.bindings) {
35315
+ if (this.parents.has(v) && e.verdict.bound && e.relay !== void 0 && e.parentMembers?.has(e.relay) === true) n++;
35316
+ }
35317
+ return n;
35318
+ }
35319
+ /** True for a resolved V, bound or not. */
35320
+ isVoice(channelId) {
35321
+ return this.parents.has(channelId);
35322
+ }
35323
+ /** P of a resolved V, bound or not; undefined otherwise. Never use this to route: see {@link boundParentOf}. */
35324
+ parentOf(channelId) {
35325
+ return this.parents.get(channelId);
35326
+ }
35327
+ /** Every resolved V with its P. */
35328
+ voiceChannels() {
35329
+ return [...this.parents.entries()];
35330
+ }
35331
+ /** The gate for a channel the bridge's listing has never classified (`unlisted`), or for a resolved V. */
35332
+ gate(channelId, unlisted) {
35333
+ if (this.parents.has(channelId)) return "drop";
35334
+ if (!unlisted || this.notVoice.has(channelId)) return "normal";
35335
+ return this.failures.get(channelId)?.kind === "absent" ? "drop" : "hold";
35336
+ }
35337
+ bindingKey(channelId) {
35338
+ return this.bindings.get(channelId)?.key;
35339
+ }
35340
+ setBinding(channelId, key, verdict, positions = null, admitted = {}) {
35341
+ this.bindings.set(channelId, { key, positions, verdict, ...admitted });
35342
+ }
35343
+ /** Fix M-1: each evaluation of a V takes a generation; only the newest started may store its verdict. */
35344
+ beginEvaluation(channelId) {
35345
+ const g = (this.generations.get(channelId) ?? 0) + 1;
35346
+ this.generations.set(channelId, g);
35347
+ return g;
35348
+ }
35349
+ isLatestEvaluation(channelId, generation) {
35350
+ return this.generations.get(channelId) === generation;
35351
+ }
35352
+ /**
35353
+ * Fix I-1: records a frame's receipt stamp the FIRST time it is seen; a re-served frame keeps its first record.
35354
+ * Task 20 R2: at the cap nothing is evicted and the new arrival is not recorded (refused: no record, no parent).
35355
+ */
35356
+ noteReceipt(messageId, pStamp, now = Date.now()) {
35357
+ if (this.receipts.has(messageId)) return;
35358
+ if (this.receipts.size >= VOICE_RECEIPT_CAP) {
35359
+ for (const [id, r] of this.receipts) if (now - r.at > VOICE_RECEIPT_MAX_AGE_MS) this.receipts.delete(id);
35360
+ if (this.receipts.size >= VOICE_RECEIPT_CAP) {
35361
+ this.capReached++;
35362
+ return;
35363
+ }
35364
+ }
35365
+ this.receipts.set(messageId, { stamp: pStamp, at: now });
35366
+ }
35367
+ /** Final review M-d: arrivals refused because the receipt map was full (a number only). */
35368
+ receiptCapReached() {
35369
+ return this.capReached;
35370
+ }
35371
+ /** Test visibility: receipt records held. */
35372
+ receiptCount() {
35373
+ return this.receipts.size;
35374
+ }
35375
+ forgetReceipt(messageId) {
35376
+ this.receipts.delete(messageId);
35377
+ }
35378
+ /**
35379
+ * 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
35380
+ * frame's receipt. A frame received at other positions (or with none recorded) gets undefined: dropped, never held.
35381
+ */
35382
+ boundParentForFrame(channelId, messageId, openedAt) {
35383
+ return this.frameContext(channelId, messageId, openedAt)?.parent;
35384
+ }
35385
+ /**
35386
+ * Task 20: the per-frame verdict with what the accept filter needs. Bound now AND the frame's pre-batch P stamp equals
35387
+ * the verdict's P position AND (R1) the frame's own opening position equals the verdict's V position. A frame with
35388
+ * no record, a null record, or no `openedAt` is refused.
35389
+ */
35390
+ frameContext(channelId, messageId, openedAt) {
35391
+ const parent3 = this.boundParentOf(channelId);
35392
+ if (parent3 === void 0) return void 0;
35393
+ const entry = this.bindings.get(channelId);
35394
+ const at = this.receipts.get(messageId)?.stamp;
35395
+ if (entry?.positions == null || typeof at !== "string" || at !== entry.positions.p) return void 0;
35396
+ const v = entry.positions.v;
35397
+ if (v === null || openedAt == null || openedAt.groupId !== v.groupId || openedAt.epoch !== v.epoch) return void 0;
35398
+ return { parent: parent3, relay: entry.relay, parentMembers: entry.parentMembers ?? /* @__PURE__ */ new Set() };
35399
+ }
35400
+ /** The current verdict, or undefined when never evaluated. */
35401
+ binding(channelId) {
35402
+ return this.bindings.get(channelId)?.verdict;
35403
+ }
35404
+ /** P only while V is a resolved V whose last verdict is bound; undefined otherwise. */
35405
+ boundParentOf(channelId) {
35406
+ const parent3 = this.parents.get(channelId);
35407
+ return parent3 !== void 0 && this.bindings.get(channelId)?.verdict.bound === true ? parent3 : void 0;
35408
+ }
35409
+ };
35410
+
35411
+ // src/voice-channel-frame.ts
35412
+ function classifyVoiceFrame(f) {
35413
+ if (f.senderId === f.ownIdentityId) return "drop";
35414
+ const fromRelay = f.relay !== void 0 && f.senderId === f.relay;
35415
+ const fromMember = f.parentMembers.has(f.senderId);
35416
+ if (!fromRelay && !fromMember) return "drop";
35417
+ let body;
35418
+ try {
35419
+ body = JSON.parse(new TextDecoder().decode(f.plaintext));
35420
+ } catch {
35421
+ return "drop";
35422
+ }
35423
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return "drop";
35424
+ const b = body;
35425
+ if (b.type === "call_transcript") return "call_transcript";
35426
+ if (b.type !== void 0 && b.type !== "text" || typeof b.text !== "string") return "drop";
35427
+ if (fromRelay) return "relay_turn";
35428
+ return b.text.trim() === VOICE_CALL_START_MARKER ? "greeting" : "drop";
35429
+ }
35430
+
35178
35431
  // src/grants/owner-turn.ts
35179
35432
  import { ed25519 as ed255196 } from "@noble/curves/ed25519";
35180
35433
  var HEX = /^(?:[0-9a-fA-F]{2})+$/;
@@ -35270,6 +35523,7 @@ var OWNER_DM_DEFAULT_READ_GRANTS = Object.freeze([
35270
35523
  var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
35271
35524
  Object.freeze({ tool: "current_time", mode: "read" })
35272
35525
  ]);
35526
+ var BRIDGE_CAPABILITIES = Object.freeze(["voice_channel_v1"]);
35273
35527
  var RELAYED_VOICE_GRANTS = Object.freeze([
35274
35528
  Object.freeze({ tool: "current_time", mode: "read" }),
35275
35529
  Object.freeze({ tool: "web_search", mode: "read" })
@@ -35544,12 +35798,19 @@ var RoomClassChangedSendError = class extends SendForbiddenError {
35544
35798
  this.message = `reply dropped for channel ${channelId}: the room changed after its tools were granted; no retry`;
35545
35799
  }
35546
35800
  };
35801
+ function voiceVerdictPositions(v, p) {
35802
+ return {
35803
+ v: v.groupId !== null && v.epoch !== null ? { groupId: v.groupId, epoch: v.epoch } : null,
35804
+ p: voiceParentStamp(p)
35805
+ };
35806
+ }
35547
35807
  function rosterFreshAt(entry, heldEpoch, heldGroupId) {
35548
35808
  return entry !== void 0 && typeof heldEpoch === "number" && entry.epoch === heldEpoch && typeof heldGroupId === "string" && entry.groupId === heldGroupId;
35549
35809
  }
35550
35810
  var RoomGrantEpochMovedError = class extends Error {
35551
35811
  };
35552
35812
  var ROOM_GRANT_TRACK_MAX = 4096;
35813
+ var VOICE_FRAME_PARENT_MAX = 4096;
35553
35814
  var SessionSupersededError = class extends Error {
35554
35815
  constructor() {
35555
35816
  super("session_superseded: another copy of this agent took its session during start()");
@@ -35981,6 +36242,13 @@ var BridgeAgent = class _BridgeAgent {
35981
36242
  supersedeRefundable = /* @__PURE__ */ new Set();
35982
36243
  /** M-1: channels this process has used as a FileSafe vault channel. */
35983
36244
  vaultChannelIds = /* @__PURE__ */ new Set();
36245
+ /**
36246
+ * ADR-0024 step 2a Task 19 (spec §9): hidden voice channels V resolved to their conversation P, lookup back-off, and
36247
+ * each V's binding verdict. In memory only: a restart re-resolves (one get_channel per V) and re-evaluates.
36248
+ */
36249
+ voiceChannels = new VoiceChannelRegistry();
36250
+ /** Fix M-3: channels a listing row marked channel_kind "voice"; never recorded as listed. */
36251
+ listedAsVoice = /* @__PURE__ */ new Set();
35984
36252
  /** Phase 3 F3: consecutive agent turns per channel. */
35985
36253
  agentHops = new AgentHopTracker();
35986
36254
  maxAgentHops;
@@ -36011,6 +36279,12 @@ var BridgeAgent = class _BridgeAgent {
36011
36279
  * counter is incremented. Surfaced on health()/metrics().
36012
36280
  */
36013
36281
  failedAttachments = 0;
36282
+ /** Task 20 R4: V frames dropped by the per-frame verdict (not bound, or not at the verdict's positions). A number only. */
36283
+ voiceFramesRefused = 0;
36284
+ /** Task 20: frames on a bound V the accept filter dropped (not R's voice frame, a greeting marker or a transcript). */
36285
+ voiceFramesFiltered = 0;
36286
+ /** Task 20: message id of an admitted V frame -> its P (B.8 keys). Bounded; an entry lives one ingest cycle in practice. */
36287
+ voiceFrameParent = /* @__PURE__ */ new Map();
36014
36288
  /** Files link: vault_ref bodies refused by R1 (strict parse) or R2 (enclave binding). */
36015
36289
  refusedVaultRefs = 0;
36016
36290
  /**
@@ -36424,7 +36698,9 @@ var BridgeAgent = class _BridgeAgent {
36424
36698
  // Fix round MEDIUM 2: a channel this process used as a FileSafe vault
36425
36699
  // channel is never swept (the sweeper also refuses any channel no listing
36426
36700
  // has shown, which covers a vault channel after a restart).
36427
- conversations: () => this.store.memoryConversationIds().filter((c) => !this.vaultChannelIds.has(c)),
36701
+ // 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
36702
+ // when P is, and purgeConversation(P) purges every V of P. P is swept even when only its V holds memory.
36703
+ conversations: () => [...new Set(this.store.memoryConversationIds().filter((c) => !this.vaultChannelIds.has(c)).map((c) => this.store.voiceChannelParent(c) ?? c))],
36428
36704
  enclaveOf: (c) => this.store.getChannelMeta(c)?.enclaveId,
36429
36705
  // Audit r1 M6: the persisted flag, never the channelOwner cache. Set only
36430
36706
  // from a listing, so undefined means no listing has ever shown the channel.
@@ -36844,6 +37120,8 @@ var BridgeAgent = class _BridgeAgent {
36844
37120
  // the actual protected-memory adapter. It is an enum, never a path or
36845
37121
  // provider credential.
36846
37122
  memory_provider: this.cfg.memoryProvider ?? "securecoms",
37123
+ // Spec §2 (Task 18): the per-device capability declaration; a fixed enum list, one body for every report path.
37124
+ capabilities: [...BRIDGE_CAPABILITIES],
36847
37125
  // Spec §11.1-11.2 (OQ-E): flat keys; the string `memory` above is
36848
37126
  // unchanged. memory_notes is true only when notes are actually on (the
36849
37127
  // provider is securecoms and the parsed payload.memory keeps them on);
@@ -38091,6 +38369,7 @@ var BridgeAgent = class _BridgeAgent {
38091
38369
  * across a supersede), and the group must not move around the reads.
38092
38370
  */
38093
38371
  async reclassifyForReply(channelId, grant) {
38372
+ if (this.voiceChannels.isVoice(channelId)) return false;
38094
38373
  const startedAt = Date.now();
38095
38374
  await this.refreshChannelRoster(channelId);
38096
38375
  const held = await this.heldPosition(channelId);
@@ -38107,8 +38386,9 @@ var BridgeAgent = class _BridgeAgent {
38107
38386
  this.enforceRateFloor("a2a", channelId);
38108
38387
  return;
38109
38388
  }
38110
- const meta = this.freshMeta(channelId);
38111
- if (meta && !meta.is_dm) this.enforceRateFloor("room", channelId);
38389
+ const capChannel = this.capChannelFor(channelId);
38390
+ const meta = this.freshMeta(capChannel);
38391
+ if (meta && !meta.is_dm) this.enforceRateFloor("room", capChannel);
38112
38392
  }
38113
38393
  /** Bookkeeping after a CONFIRMED publish of `body` under `id`. */
38114
38394
  afterPublish(channelId, body, id, opts, purgeEpoch, replyCapture) {
@@ -38590,8 +38870,289 @@ var BridgeAgent = class _BridgeAgent {
38590
38870
  return probeResultFor("threw");
38591
38871
  }
38592
38872
  }
38873
+ // ── ADR-0024 step 2a Task 19: the hidden voice channel V (spec §9, B.8) ──────
38874
+ //
38875
+ // The rule and the cache live in voice-channel-binding.ts; this is the wiring: the get_channel lookup, the bridge's
38876
+ // own held ratchet trees and positions, V's member list (is_relay only), and the receive-cycle gate.
38877
+ /**
38878
+ * Task 20's accessor: P's channel id ONLY while V is bound under the spec §9 rule at both groups' held positions as
38879
+ * of the last evaluation (every receive cycle, before any frame is processed; or {@link voiceParentNow}); undefined
38880
+ * for an unbound or refused V, and for any channel that is not a resolved V.
38881
+ */
38882
+ voiceParentOf(channelId) {
38883
+ return this.voiceChannels.boundParentOf(channelId);
38884
+ }
38885
+ /** True for a channel resolved as a V, bound or not. Such a channel is never processed as a normal channel. */
38886
+ isKnownVoiceChannel(channelId) {
38887
+ return this.voiceChannels.isVoice(channelId);
38888
+ }
38889
+ /** Re-evaluates one V at the held positions now, then answers as {@link voiceParentOf}. */
38890
+ async voiceParentNow(channelId) {
38891
+ const parent3 = this.voiceChannels.parentOf(channelId);
38892
+ if (parent3 === void 0) return void 0;
38893
+ await this.reevaluateVoiceBinding(channelId, parent3);
38894
+ return this.voiceChannels.boundParentOf(channelId);
38895
+ }
38896
+ /** A channel no listing has ever classified (in memory or persisted). V is hidden from the listing, so it is one. */
38897
+ isUnlistedChannel(channelId) {
38898
+ return this.channelMeta.get(channelId) === void 0 && this.store.getChannelMeta(channelId)?.isDm === void 0;
38899
+ }
38900
+ voiceChannelGate(channelId) {
38901
+ if (this.vaultChannelIds.has(channelId)) return "normal";
38902
+ return this.voiceChannels.gate(channelId, this.isUnlistedChannel(channelId));
38903
+ }
38904
+ /**
38905
+ * Looks up one channel with get_channel (the voice arm, OQ-5 (a)). Only the classification is kept. Never throws; a
38906
+ * failure is recorded and retried (30 s doubling to 10 min). Called only from {@link resolveUnlistedChannels}, which
38907
+ * selects unlisted channels whose back-off allows.
38908
+ */
38909
+ async resolveVoiceChannel(channelId) {
38910
+ let status = "threw";
38911
+ let body;
38912
+ try {
38913
+ const url = `${this.cfg.apiBase}/api/v1/channels/${encodeURIComponent(channelId)}?caller_identity_id=${encodeURIComponent(this.cfg.identityId)}`;
38914
+ const res = await this.fetchBounded("get_channel", "read", url, { headers: { authorization: `Bearer ${this.cfg.apiKey}` } });
38915
+ if (res.ok) {
38916
+ body = await res.json();
38917
+ } else {
38918
+ try {
38919
+ await res.body?.cancel();
38920
+ } catch {
38921
+ }
38922
+ }
38923
+ status = res.status;
38924
+ } catch {
38925
+ status = "threw";
38926
+ }
38927
+ const r = parseVoiceResolution(channelId, status, body);
38928
+ this.voiceChannels.record(channelId, r, Date.now());
38929
+ if (r.kind === "voice") {
38930
+ try {
38931
+ this.store.recordVoiceChannelParent(channelId, r.parentChannelId);
38932
+ } catch {
38933
+ }
38934
+ }
38935
+ if (r.kind === "voice") writeServiceLog2("info", "voice.channel_lookup", { result: ServiceLogEnum.of("voice", ["voice", "absent", "error"]) });
38936
+ else if (r.kind !== "not_voice") writeServiceLog2("warn", "voice.channel_lookup", { result: ServiceLogEnum.of(r.kind, ["voice", "absent", "error"]) });
38937
+ }
38938
+ /**
38939
+ * Looks up every unlisted channel among `channelIds` whose back-off allows. A channel is "unlisted" only after this
38940
+ * cycle's listing: if any candidate remains and the cycle has not listed yet, one listing runs first, so a channel
38941
+ * that is merely new (listed, not yet cached) is never looked up. Returns true when it ran that listing.
38942
+ */
38943
+ async resolveUnlistedChannels(channelIds, listed) {
38944
+ const ids = [...new Set(channelIds)].filter((c) => !this.vaultChannelIds.has(c));
38945
+ const held = /* @__PURE__ */ new Set();
38946
+ for (const c of ids) {
38947
+ if (!this.isUnlistedChannel(c) || this.voiceChannels.isResolved(c)) continue;
38948
+ try {
38949
+ if ((await this.heldPosition(c)).groupId !== null) held.add(c);
38950
+ } catch {
38951
+ }
38952
+ }
38953
+ const due = () => ids.filter((c) => this.isUnlistedChannel(c) && this.voiceChannels.resolveDue(c, Date.now(), { heldGroup: held.has(c) }));
38954
+ let candidates = due();
38955
+ if (candidates.length === 0) return false;
38956
+ let ran = false;
38957
+ if (!listed) {
38958
+ await this.refreshChannelMeta();
38959
+ ran = true;
38960
+ candidates = due();
38961
+ }
38962
+ for (const c of candidates) await this.resolveVoiceChannel(c);
38963
+ return ran;
38964
+ }
38965
+ /** Final review M-c: V -> consecutive failed member-list reads for the binding, and when the last one ran. */
38966
+ voiceRosterRetry = /* @__PURE__ */ new Map();
38967
+ /** The first retry after a failure is immediate (next cycle); then 30 s, doubling, capped at 10 min. */
38968
+ voiceRosterRetryDue(v) {
38969
+ const f = this.voiceRosterRetry.get(v);
38970
+ if (f === void 0 || f.attempts <= 1) return true;
38971
+ const wait = Math.min(VOICE_RESOLVE_RETRY_MAX_MS, VOICE_RESOLVE_RETRY_BASE_MS * 2 ** Math.min(f.attempts - 2, 20));
38972
+ return Date.now() - f.at >= wait;
38973
+ }
38974
+ noteVoiceRosterRead(v, ok) {
38975
+ if (ok) {
38976
+ this.voiceRosterRetry.delete(v);
38977
+ return;
38978
+ }
38979
+ this.voiceRosterRetry.set(v, { at: Date.now(), attempts: (this.voiceRosterRetry.get(v)?.attempts ?? 0) + 1 });
38980
+ }
38981
+ /** Every resolved V, re-evaluated where a position or a roster moved. */
38982
+ async reevaluateVoiceBindings() {
38983
+ for (const [v, p] of this.voiceChannels.voiceChannels()) await this.reevaluateVoiceBinding(v, p);
38984
+ }
38985
+ /**
38986
+ * The spec §9 binding rule over the bridge's OWN ratchet trees (never a server list of leaves), keyed by both
38987
+ * groups' held (group, epoch) and both rosters' fetch times: a change in any of them re-evaluates. V's member list
38988
+ * is read only for `is_relay`, and only when it is fresh at V's held position. Fail closed: a tree that moved while
38989
+ * it was read, or any exception, leaves V unbound and is re-evaluated next time.
38990
+ */
38991
+ async reevaluateVoiceBinding(v, p) {
38992
+ const generation = this.voiceChannels.beginEvaluation(v);
38993
+ try {
38994
+ const vPos = await this.heldPosition(v);
38995
+ const pPos = await this.heldPosition(p);
38996
+ const positions = voiceVerdictPositions(vPos, pPos);
38997
+ const parentNotConversation = this.voiceChannels.isVoice(p) || this.isUnlistedChannel(p);
38998
+ const keyOf = () => JSON.stringify([positions, this.freshRosterEntry(v)?.at ?? null, this.freshRosterEntry(p)?.at ?? null, parentNotConversation]);
38999
+ if (this.voiceChannels.bindingKey(v) === keyOf()) return;
39000
+ if (parentNotConversation) {
39001
+ if (this.voiceChannels.isLatestEvaluation(v, generation)) this.noteVoiceBinding(v, keyOf(), { bound: false, reason: "parent_not_conversation" }, positions);
39002
+ return;
39003
+ }
39004
+ const vTree = await this.provider.leafTreeSummary(v);
39005
+ const pTree = await this.provider.leafTreeSummary(p);
39006
+ const relayCheck = () => {
39007
+ const entry = this.freshRosterEntry(v);
39008
+ return entry !== void 0 && rosterFreshAt(entry, vPos.epoch, vPos.groupId) ? (id) => entry.members.get(id)?.is_relay === true : void 0;
39009
+ };
39010
+ if (relayCheck() === void 0 && this.voiceRosterRetryDue(v)) {
39011
+ await this.refreshChannelRoster(v);
39012
+ this.noteVoiceRosterRead(v, relayCheck() !== void 0);
39013
+ }
39014
+ const isVRelay = relayCheck();
39015
+ let verdict = evaluateVoiceBinding(vTree, pTree, isVRelay);
39016
+ const vAfter = await this.heldPosition(v);
39017
+ const pAfter = await this.heldPosition(p);
39018
+ const moved = vAfter.groupId !== vPos.groupId || vAfter.epoch !== vPos.epoch || pAfter.groupId !== pPos.groupId || pAfter.epoch !== pPos.epoch;
39019
+ if (moved) verdict = { bound: false, reason: "moved" };
39020
+ const retry = !verdict.bound && (verdict.reason === "relay_unknown" || verdict.reason === "moved") || verdict.bound && isVRelay === void 0;
39021
+ if (!this.voiceChannels.isLatestEvaluation(v, generation)) return;
39022
+ const parentMembers = /* @__PURE__ */ new Set();
39023
+ for (const l of pTree.leaves) if (l.basic && l.identity !== null) parentMembers.add(l.identity);
39024
+ this.noteVoiceBinding(v, retry ? "" : keyOf(), verdict, positions, void 0, {
39025
+ relay: verdict.bound ? voiceRelayIdentity(vTree, isVRelay) : void 0,
39026
+ parentMembers
39027
+ });
39028
+ } catch (e) {
39029
+ if (this.voiceChannels.isLatestEvaluation(v, generation)) this.noteVoiceBinding(v, "", { bound: false, reason: "moved" }, null, e);
39030
+ }
39031
+ }
39032
+ /**
39033
+ * 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
39034
+ * half is not stamped here: each frame carries the epoch that opened it (`openedAt`).
39035
+ */
39036
+ async voicePositionSnapshot() {
39037
+ const out = /* @__PURE__ */ new Map();
39038
+ for (const [v, p] of this.voiceChannels.voiceChannels()) {
39039
+ try {
39040
+ out.set(v, voiceParentStamp(await this.heldPosition(p)));
39041
+ } catch {
39042
+ }
39043
+ }
39044
+ return out;
39045
+ }
39046
+ /**
39047
+ * 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)
39048
+ * moved between that frame's first receipt and the verdict. Otherwise undefined: the frame is dropped (acked, never
39049
+ * held, never processed). A frame first received before V was resolved has no receipt positions and is refused.
39050
+ */
39051
+ voiceParentForFrame(channelId, messageId, openedAt) {
39052
+ return this.voiceChannels.boundParentForFrame(channelId, messageId, openedAt);
39053
+ }
39054
+ /** Stores a verdict; a change of verdict logs one content-free line. */
39055
+ noteVoiceBinding(v, key, verdict, positions, error, admitted = {}) {
39056
+ const prev = this.voiceChannels.binding(v);
39057
+ this.voiceChannels.setBinding(v, key, verdict, positions, admitted);
39058
+ const state = verdict.bound ? "bound" : verdict.reason;
39059
+ const prevState = prev === void 0 ? void 0 : prev.bound ? "bound" : prev.reason;
39060
+ if (state === prevState && error === void 0) return;
39061
+ writeServiceLog2(verdict.bound ? "info" : "warn", "voice.channel_binding", {
39062
+ 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"])
39063
+ }, error);
39064
+ }
39065
+ /**
39066
+ * The receive-cycle gate, before any per-frame work. A resolved V is never a normal channel: a frame passes only when
39067
+ * the per-frame verdict names P (bound now, its P stamp and its own opening epoch at the verdict's positions) AND the
39068
+ * accept filter takes it (Task 20: R's voice frame or a P member's greeting marker; a call_transcript is acked, not a
39069
+ * turn). Every other V frame is acked unprocessed and never recorded. A channel get_channel answered 404 is acked
39070
+ * unprocessed (fail closed). An unlisted channel whose lookup errored or has not run is held: neither processed nor
39071
+ * acked, so it is re-served once the lookup answers. Everything else passes unchanged.
39072
+ */
39073
+ async gateVoiceEntries(entries) {
39074
+ const keep = [];
39075
+ const drop = [];
39076
+ for (const m of entries) {
39077
+ const gate = this.voiceChannelGate(m.channelId);
39078
+ if (gate === "hold") continue;
39079
+ if (gate === "normal") {
39080
+ this.voiceChannels.forgetReceipt(m.messageId);
39081
+ keep.push(m);
39082
+ continue;
39083
+ }
39084
+ if (this.voiceChannels.isVoice(m.channelId)) {
39085
+ const admitted = this.admitVoiceFrame(m);
39086
+ this.voiceChannels.forgetReceipt(m.messageId);
39087
+ if (admitted) {
39088
+ keep.push(m);
39089
+ continue;
39090
+ }
39091
+ } else {
39092
+ this.voiceChannels.forgetReceipt(m.messageId);
39093
+ }
39094
+ drop.push(m.queueId);
39095
+ }
39096
+ if (drop.length) {
39097
+ try {
39098
+ await this.provider.ackDelivery(this.cfg.deviceId, drop);
39099
+ } catch {
39100
+ }
39101
+ }
39102
+ return keep;
39103
+ }
39104
+ /**
39105
+ * Task 20: the per-frame verdict and the accept filter for one frame on a resolved V. True only for a model turn
39106
+ * (R's voice frame, a P member's greeting marker), which is then keyed per B.8 through {@link voiceFrameParent}. A
39107
+ * call_transcript is accepted but is not a turn (acked, nothing more, as step 1 on P). Counters are numbers only.
39108
+ */
39109
+ admitVoiceFrame(m) {
39110
+ const parent3 = this.voiceParentForFrame(m.channelId, m.messageId, m.openedAt ?? null);
39111
+ const ctx = parent3 === void 0 ? void 0 : this.voiceChannels.frameContext(m.channelId, m.messageId, m.openedAt ?? null);
39112
+ if (ctx === void 0) {
39113
+ this.voiceFramesRefused++;
39114
+ return false;
39115
+ }
39116
+ const kind2 = classifyVoiceFrame({
39117
+ senderId: m.envelope.sender_id,
39118
+ ownIdentityId: this.cfg.identityId,
39119
+ relay: ctx.relay,
39120
+ parentMembers: ctx.parentMembers,
39121
+ plaintext: m.plaintext
39122
+ });
39123
+ if (kind2 === "call_transcript") return false;
39124
+ if (kind2 === "drop") {
39125
+ this.voiceFramesFiltered++;
39126
+ return false;
39127
+ }
39128
+ this.voiceFrameParent.set(m.messageId, ctx.parent);
39129
+ if (this.voiceFrameParent.size > VOICE_FRAME_PARENT_MAX) this.voiceFrameParent.delete(this.voiceFrameParent.keys().next().value);
39130
+ return true;
39131
+ }
39132
+ /**
39133
+ * Task 20 (B.8): the key channel for metadata, audience, owner, manifest and caps. For an admitted V frame, its P;
39134
+ * for any other frame, its own channel.
39135
+ */
39136
+ keyChannelForFrame(m) {
39137
+ return this.voiceFrameParent.get(m.messageId) ?? m.channelId;
39138
+ }
39139
+ /**
39140
+ * Task 20 (B.8 "per-room daily caps → P", the room rate floor): the channel a cap or floor counts against. A known V
39141
+ * counts against its bound P; an unbound V against itself (no grant can be issued there, so nothing is widened).
39142
+ */
39143
+ capChannelFor(channelId) {
39144
+ return this.voiceChannels.parentOf(channelId) ?? channelId;
39145
+ }
39146
+ /** Phase 3 F1 pause, for a voice channel V also read on its P (a paused agent runs no inference on the call either). */
39147
+ pausedFor(channelId) {
39148
+ return this.pausedChannels.has(channelId) || this.pausedChannels.has(this.capChannelFor(channelId));
39149
+ }
38593
39150
  /** §5.4: one conversation's turns, facts, notes and recall, inside the memory mutex, with every in-memory copy. */
38594
39151
  async purgeConversation(channelId) {
39152
+ await this.purgeOneConversation(channelId);
39153
+ for (const v of this.store.voiceChannelsOf(channelId)) await this.purgeOneConversation(v);
39154
+ }
39155
+ async purgeOneConversation(channelId) {
38595
39156
  const scopes = this.unifiedOn ? this.store.ownerDmScopeIds().filter((s) => parseOwnerDmScopeId(s)?.channelId === channelId) : [];
38596
39157
  await this.notes.purgeChannel(channelId);
38597
39158
  for (const s of scopes) this.store.clearConversationAbsence(s);
@@ -39831,17 +40392,17 @@ var BridgeAgent = class _BridgeAgent {
39831
40392
  async applyMemoryNote(raw, ctx) {
39832
40393
  if (this.unifiedOn) return this.applyUnifiedNote(raw, ctx);
39833
40394
  const ids = echoIds(raw);
39834
- const refuse = (reason, echo = ids) => {
40395
+ const refuse2 = (reason, echo = ids) => {
39835
40396
  writeServiceLog2("info", "memory.note_refused", {
39836
40397
  reason_class: ServiceLogEnum.of(reason, ["mode", "invalid", "message", "audience", "provenance", "secret", "cap", "cleared", "store", "budget", "purged", "not_owner_words"])
39837
40398
  });
39838
40399
  return { type: "memory_note_result", ...echo, ok: false, reason };
39839
40400
  };
39840
- if (!this.notesProviderOn()) return refuse("mode");
40401
+ if (!this.notesProviderOn()) return refuse2("mode");
39841
40402
  const f = parseMemoryNoteFrame(raw);
39842
- if (!f) return refuse("invalid");
39843
- if (ctx.pendingRequests.has(f.request_id)) return refuse("invalid", { message_id: f.message_id });
39844
- if (ctx.pendingRequests.size >= MAX_PENDING_NOTE_REQUESTS) return refuse("invalid", {});
40403
+ if (!f) return refuse2("invalid");
40404
+ if (ctx.pendingRequests.has(f.request_id)) return refuse2("invalid", { message_id: f.message_id });
40405
+ if (ctx.pendingRequests.size >= MAX_PENDING_NOTE_REQUESTS) return refuse2("invalid", {});
39845
40406
  ctx.pendingRequests.add(f.request_id);
39846
40407
  try {
39847
40408
  const w2 = () => {
@@ -39853,13 +40414,13 @@ var BridgeAgent = class _BridgeAgent {
39853
40414
  return cur.record;
39854
40415
  };
39855
40416
  const rec = w2();
39856
- if (rec === void 0) return refuse("message");
40417
+ if (rec === void 0) return refuse2("message");
39857
40418
  const channelId = rec.channel_id;
39858
- if (rec.authorization?.memory_write !== true) return refuse("audience");
39859
- if (!this.noteTurnClean(rec.message_id)) return refuse("audience");
40419
+ if (rec.authorization?.memory_write !== true) return refuse2("audience");
40420
+ if (!this.noteTurnClean(rec.message_id)) return refuse2("audience");
39860
40421
  const held = await this.heldPosition(channelId);
39861
40422
  const now = this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false });
39862
- if (now.memory_write !== true) return refuse("audience");
40423
+ if (now.memory_write !== true) return refuse2("audience");
39863
40424
  const spend = async () => {
39864
40425
  const rw = await this.store.prepareRuntimeInboxRewrite(rec.message_id, bumpW9);
39865
40426
  if (rw === "declined") return "budget";
@@ -39875,44 +40436,44 @@ var BridgeAgent = class _BridgeAgent {
39875
40436
  };
39876
40437
  if (f.op === "forget" && typeof f.id === "object") {
39877
40438
  const shown = ctx.shownRecall(f.message_id, ctx.token);
39878
- if (!shown || !shown.has(f.id.recall)) return refuse("invalid");
40439
+ if (!shown || !shown.has(f.id.recall)) return refuse2("invalid");
39879
40440
  const rw = await spend();
39880
- if (typeof rw === "string") return refuse(rw);
39881
- if (w2() !== rec) return refuse("message");
39882
- if (!this.notesProviderOn()) return refuse("mode");
40441
+ if (typeof rw === "string") return refuse2(rw);
40442
+ if (w2() !== rec) return refuse2("message");
40443
+ if (!this.notesProviderOn()) return refuse2("mode");
39883
40444
  const r2 = await this.recall.forget(channelId, f.id.recall, rw, precommit(rw));
39884
- if (r2 === "unknown_id") return refuse("invalid");
39885
- if (r2 !== "ok") return refuse(r2);
40445
+ if (r2 === "unknown_id") return refuse2("invalid");
40446
+ if (r2 !== "ok") return refuse2(r2);
39886
40447
  writeServiceLog2("info", "memory.recall_forgotten");
39887
40448
  return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true };
39888
40449
  }
39889
40450
  let mutation;
39890
40451
  let rewrite;
39891
40452
  if (f.op === "forget") {
39892
- if (typeof f.id !== "number") return refuse("invalid");
40453
+ if (typeof f.id !== "number") return refuse2("invalid");
39893
40454
  const rw = await spend();
39894
- if (typeof rw === "string") return refuse(rw);
40455
+ if (typeof rw === "string") return refuse2(rw);
39895
40456
  rewrite = rw;
39896
40457
  mutation = { op: "forget", id: f.id, rewrite: rw };
39897
40458
  } else {
39898
40459
  const p = prepareStoredNote(f.text, { text: rec.text, ...rec.transcribed === true ? { transcribed: true } : {} });
39899
- if (!p.ok) return refuse(p.reason);
40460
+ if (!p.ok) return refuse2(p.reason);
39900
40461
  if (f.op === "replace") {
39901
- if (typeof f.id !== "number") return refuse("invalid");
40462
+ if (typeof f.id !== "number") return refuse2("invalid");
39902
40463
  const rw = await spend();
39903
- if (typeof rw === "string") return refuse(rw);
40464
+ if (typeof rw === "string") return refuse2(rw);
39904
40465
  rewrite = rw;
39905
40466
  mutation = { op: "replace", id: f.id, text: p.text, rewrite: rw };
39906
40467
  } else {
39907
40468
  mutation = { op: "add", text: p.text };
39908
40469
  }
39909
40470
  }
39910
- if (w2() !== rec) return refuse("message");
39911
- if (!this.notesProviderOn()) return refuse("mode");
40471
+ if (w2() !== rec) return refuse2("message");
40472
+ if (!this.notesProviderOn()) return refuse2("mode");
39912
40473
  const stamp = this.ingestGeneration.get(rec.message_id);
39913
- if (stamp === void 0) return refuse("cleared");
40474
+ if (stamp === void 0) return refuse2("cleared");
39914
40475
  const r = await this.notes.write(channelId, mutation, stamp, precommit(rewrite));
39915
- if (!r.ok) return refuse(r.reason);
40476
+ if (!r.ok) return refuse2(r.reason);
39916
40477
  writeServiceLog2("info", "memory.note_written", {
39917
40478
  op: ServiceLogEnum.of(f.op, ["add", "replace", "forget"]),
39918
40479
  notes_count: r.notesCount,
@@ -39921,7 +40482,7 @@ var BridgeAgent = class _BridgeAgent {
39921
40482
  return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true, id: r.id };
39922
40483
  } catch (e) {
39923
40484
  writeServiceLog2("warn", "memory.note_write_failed", {}, e);
39924
- return refuse("store");
40485
+ return refuse2("store");
39925
40486
  } finally {
39926
40487
  ctx.pendingRequests.delete(f.request_id);
39927
40488
  }
@@ -39935,17 +40496,17 @@ var BridgeAgent = class _BridgeAgent {
39935
40496
  */
39936
40497
  async applyUnifiedNote(raw, ctx) {
39937
40498
  const ids = echoIds(raw);
39938
- const refuse = (reason, echo = ids) => {
40499
+ const refuse2 = (reason, echo = ids) => {
39939
40500
  writeServiceLog2("info", "memory.note_refused", {
39940
40501
  reason_class: ServiceLogEnum.of(reason, ["mode", "invalid", "message", "audience", "provenance", "secret", "cap", "cleared", "store", "budget", "purged", "not_owner_words"])
39941
40502
  });
39942
40503
  return { type: "memory_note_result", ...echo, ok: false, reason };
39943
40504
  };
39944
- if (!this.unifiedProviderOn()) return refuse("mode");
40505
+ if (!this.unifiedProviderOn()) return refuse2("mode");
39945
40506
  const f = parseMemoryNoteFrame(raw);
39946
- if (!f) return refuse("invalid");
39947
- if (ctx.pendingRequests.has(f.request_id)) return refuse("invalid", { message_id: f.message_id });
39948
- if (ctx.pendingRequests.size >= MAX_PENDING_NOTE_REQUESTS) return refuse("invalid", {});
40507
+ if (!f) return refuse2("invalid");
40508
+ if (ctx.pendingRequests.has(f.request_id)) return refuse2("invalid", { message_id: f.message_id });
40509
+ if (ctx.pendingRequests.size >= MAX_PENDING_NOTE_REQUESTS) return refuse2("invalid", {});
39949
40510
  ctx.pendingRequests.add(f.request_id);
39950
40511
  try {
39951
40512
  const w2 = () => {
@@ -39957,25 +40518,25 @@ var BridgeAgent = class _BridgeAgent {
39957
40518
  return cur.record;
39958
40519
  };
39959
40520
  const rec = w2();
39960
- if (rec === void 0) return refuse("message");
40521
+ if (rec === void 0) return refuse2("message");
39961
40522
  const channelId = rec.channel_id;
39962
- if (rec.authorization?.memory_write !== true) return refuse("audience");
39963
- if (!this.noteTurnClean(rec.message_id)) return refuse("audience");
40523
+ if (rec.authorization?.memory_write !== true) return refuse2("audience");
40524
+ if (!this.noteTurnClean(rec.message_id)) return refuse2("audience");
39964
40525
  const held = await this.heldPosition(channelId);
39965
- if (this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false }).memory_write !== true) return refuse("audience");
40526
+ if (this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false }).memory_write !== true) return refuse2("audience");
39966
40527
  const scopeId = this.ownerScopeNow(channelId);
39967
- if (scopeId === null) return refuse("audience");
40528
+ if (scopeId === null) return refuse2("audience");
39968
40529
  const precommit = () => !this.unifiedProviderOn() ? "mode" : w2() !== rec ? "message" : null;
39969
- if (f.op === "replace") return refuse("invalid");
39970
- if (f.op === "forget") return await this.forgetUnified(f, rec, channelId, ctx, refuse, precommit);
40530
+ if (f.op === "replace") return refuse2("invalid");
40531
+ if (f.op === "forget") return await this.forgetUnified(f, rec, channelId, ctx, refuse2, precommit);
39971
40532
  const stripped = allowlistPin(f.text);
39972
- if (stripped.trim().length === 0) return refuse("invalid");
40533
+ if (stripped.trim().length === 0) return refuse2("invalid");
39973
40534
  const shown = ctx.shownUnified(f.message_id, ctx.token) ?? /* @__PURE__ */ new Set();
39974
- if (!pinCoverage(stripped, rec.text, this.unifiedReads.coverageTexts(f.message_id, shown)).ok) return refuse("not_owner_words");
39975
- if (scanNote(stripped) !== null) return refuse("secret");
39976
- if (stripped.length > PIN_MAX_CHARS) return refuse("cap");
40535
+ if (!pinCoverage(stripped, rec.text, this.unifiedReads.coverageTexts(f.message_id, shown)).ok) return refuse2("not_owner_words");
40536
+ if (scanNote(stripped) !== null) return refuse2("secret");
40537
+ if (stripped.length > PIN_MAX_CHARS) return refuse2("cap");
39977
40538
  const stamp = this.ingestGeneration.get(rec.message_id);
39978
- if (stamp === void 0) return refuse("cleared");
40539
+ if (stamp === void 0) return refuse2("cleared");
39979
40540
  const capture = this.store.itemCapture(channelId);
39980
40541
  const r = await this.items.commit({
39981
40542
  scopeKind: "owner_dm",
@@ -39993,7 +40554,7 @@ var BridgeAgent = class _BridgeAgent {
39993
40554
  });
39994
40555
  if (!r.ok) {
39995
40556
  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" };
39996
- return refuse(map[r.reason] ?? "store");
40557
+ return refuse2(map[r.reason] ?? "store");
39997
40558
  }
39998
40559
  await this.notes.exclusive(async () => this.unifiedReads.bumpGeneration());
39999
40560
  void this.send(channelId, pinnedLine(stripped), void 0, { agentTurn: false }).catch((e) => writeServiceLog2("warn", "memory.pinned_line_failed", {}, e));
@@ -40001,7 +40562,7 @@ var BridgeAgent = class _BridgeAgent {
40001
40562
  return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true, id: r.itemIds[0] };
40002
40563
  } catch (e) {
40003
40564
  writeServiceLog2("warn", "memory.note_write_failed", {}, e);
40004
- return refuse("store");
40565
+ return refuse2("store");
40005
40566
  } finally {
40006
40567
  ctx.pendingRequests.delete(f.request_id);
40007
40568
  }
@@ -40015,16 +40576,16 @@ var BridgeAgent = class _BridgeAgent {
40015
40576
  * is invalidated in the same mutex hold, the item leaves the ranking hot set, then the bridge publishes `Forgot:`
40016
40577
  * (OQ-P2: afterPublish keeps the line out of history and the items). Every reason is a key of UNIFIED_REFUSAL.
40017
40578
  */
40018
- async forgetUnified(f, rec, channelId, ctx, refuse, precommit) {
40019
- if (typeof f.id !== "object") return refuse("invalid");
40579
+ async forgetUnified(f, rec, channelId, ctx, refuse2, precommit) {
40580
+ if (typeof f.id !== "object") return refuse2("invalid");
40020
40581
  const itemId = f.id.recall;
40021
40582
  const shown = ctx.shownUnified(f.message_id, ctx.token);
40022
- if (!shown || !shown.has(itemId)) return refuse("invalid");
40583
+ if (!shown || !shown.has(itemId)) return refuse2("invalid");
40023
40584
  const text = this.unifiedReads.shownText(f.message_id, itemId);
40024
- if (text === void 0) return refuse("invalid");
40585
+ if (text === void 0) return refuse2("invalid");
40025
40586
  const rw = await this.store.prepareRuntimeInboxRewrite(rec.message_id, bumpW9);
40026
- if (rw === "declined") return refuse("budget");
40027
- if (rw === "no_row") return refuse("message");
40587
+ if (rw === "declined") return refuse2("budget");
40588
+ if (rw === "no_row") return refuse2("message");
40028
40589
  const r = await this.notes.exclusive(async () => {
40029
40590
  const veto = precommit();
40030
40591
  if (veto !== null) return { ok: false, reason: veto };
@@ -40037,7 +40598,7 @@ var BridgeAgent = class _BridgeAgent {
40037
40598
  }
40038
40599
  return out;
40039
40600
  });
40040
- if (!r.ok) return refuse(r.reason === "unknown_id" ? "invalid" : r.reason);
40601
+ if (!r.ok) return refuse2(r.reason === "unknown_id" ? "invalid" : r.reason);
40041
40602
  this.rankingNote(() => this.ranking.removed(r.itemIds));
40042
40603
  void this.send(channelId, forgotLine(text), void 0, { agentTurn: false }).catch((e) => writeServiceLog2("warn", "memory.forgot_line_failed", {}, e));
40043
40604
  writeServiceLog2("info", "memory.item_forgotten");
@@ -40111,8 +40672,9 @@ var BridgeAgent = class _BridgeAgent {
40111
40672
  this.fastReplyChains.set(channelId, next);
40112
40673
  }
40113
40674
  /** Count a fast reply against FAST_REPLY_MAX_JOBS and reserve its room send-floor token; returns the release. */
40114
- reserveFastReply(channelId) {
40675
+ reserveFastReply(replyChannel) {
40115
40676
  this.fastReplyJobs++;
40677
+ const channelId = this.capChannelFor(replyChannel);
40116
40678
  this.fastReplyReserved.set(channelId, (this.fastReplyReserved.get(channelId) ?? 0) + 1);
40117
40679
  return () => {
40118
40680
  this.fastReplyJobs--;
@@ -40336,6 +40898,7 @@ var BridgeAgent = class _BridgeAgent {
40336
40898
  });
40337
40899
  if (!res.ok) throw new Error(`list_channels failed: ${res.status}`);
40338
40900
  const rows = await res.json();
40901
+ for (const r of rows) if (r.channel_kind === "voice") this.listedAsVoice.add(r.channel_id);
40339
40902
  for (const r of rows) this.store.recordChannelEnclave(r.channel_id, r.enclave_id);
40340
40903
  return rows.map((r) => ({
40341
40904
  channel_id: r.channel_id,
@@ -40419,6 +40982,7 @@ var BridgeAgent = class _BridgeAgent {
40419
40982
  */
40420
40983
  recordListedChannelMeta(channels, at) {
40421
40984
  for (const c of channels) {
40985
+ if (this.listedAsVoice.has(c.channel_id)) continue;
40422
40986
  this.channelMeta.set(c.channel_id, { is_dm: c.is_dm, name: c.name, at });
40423
40987
  this.store.recordChannelIsDm(c.channel_id, c.is_dm);
40424
40988
  }
@@ -40602,7 +41166,8 @@ var BridgeAgent = class _BridgeAgent {
40602
41166
  }
40603
41167
  const self = members.get(this.cfg.identityId);
40604
41168
  if (self?.name) this.lastOwnDisplayName = self.name;
40605
- await this.resolveAndPinOwner(channelId, liveRows);
41169
+ const unclassified = this.isUnlistedChannel(channelId) && !this.voiceChannels.isNotVoice(channelId);
41170
+ if (!this.voiceChannels.isVoice(channelId) && !unclassified) await this.resolveAndPinOwner(channelId, liveRows);
40606
41171
  } catch {
40607
41172
  }
40608
41173
  }
@@ -40638,7 +41203,7 @@ var BridgeAgent = class _BridgeAgent {
40638
41203
  * held group (never the backend's row label, M-2). NEVER throws (M-4): any
40639
41204
  * exception is `failed verify_error`, and the row is acked like any other turn.
40640
41205
  */
40641
- async verifyOwnerTurn(m) {
41206
+ async verifyOwnerTurn(m, keyChannel = m.channelId) {
40642
41207
  const pin = this.cfg.ownerGrantPin?.pin ?? null;
40643
41208
  if (!pin || m.envelope.sender_id !== pin.ownerIdentityId) return OWNER_TURN_UNCHECKED;
40644
41209
  try {
@@ -40650,9 +41215,9 @@ var BridgeAgent = class _BridgeAgent {
40650
41215
  }
40651
41216
  if (v.sender_id !== pin.ownerIdentityId) return this.ownerTurnFailed(m, "malformed");
40652
41217
  let leafClass = "ok";
40653
- if (this.freshMeta(m.channelId)?.is_dm === true) {
41218
+ if (this.freshMeta(keyChannel)?.is_dm === true) {
40654
41219
  const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
40655
- if (signedDm !== void 0 && signedDm !== m.channelId) return this.ownerTurnFailed(m, "wrong_dm_channel");
41220
+ if (signedDm !== void 0 && signedDm !== keyChannel) return this.ownerTurnFailed(m, "wrong_dm_channel");
40656
41221
  leafClass = classifyOwnerDmLeaves((await this.provider.leafTreeSummary(m.channelId)).leaves, {
40657
41222
  ownerIdentityId: pin.ownerIdentityId,
40658
41223
  agentIdentityId: this.cfg.identityId,
@@ -40816,9 +41381,14 @@ var BridgeAgent = class _BridgeAgent {
40816
41381
  });
40817
41382
  }
40818
41383
  /** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
40819
- async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId, voiceScoped) {
41384
+ async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId, voiceScoped, voiceParent) {
40820
41385
  const held = await this.heldPosition(channelId);
40821
- const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict, voiceScoped });
41386
+ const keyHeld = voiceParent === void 0 ? held : await this.heldPosition(voiceParent);
41387
+ const auth = this.authorizationFor(voiceParent ?? channelId, senderId, relaySpeakerId, keyHeld.epoch, keyHeld.groupId, {
41388
+ ownerVerdict,
41389
+ voiceScoped,
41390
+ ...voiceParent === void 0 ? {} : { voice: { channelId, heldEpoch: held.epoch, heldGroupId: held.groupId } }
41391
+ });
40822
41392
  if (!(auth.audience === "team_room" && auth.tool_grants.length > 0)) return auth;
40823
41393
  if (this.roomGrantEpochs.size >= ROOM_GRANT_TRACK_MAX || held.epoch === null || held.groupId === null) {
40824
41394
  writeServiceLog2("warn", "room_grant.untracked");
@@ -40871,7 +41441,10 @@ var BridgeAgent = class _BridgeAgent {
40871
41441
  const meta = this.freshMeta(channelId);
40872
41442
  const entry = this.freshRosterEntry(channelId);
40873
41443
  const roster = entry?.members;
40874
- const sender = roster?.get(senderId);
41444
+ const vEntry = opts.voice === void 0 ? void 0 : this.freshRosterEntry(opts.voice.channelId);
41445
+ const vSender = vEntry?.members.get(senderId);
41446
+ const sender = opts.voice === void 0 ? roster?.get(senderId) : vSender === void 0 ? void 0 : roster?.get(senderId) ?? vSender;
41447
+ const senderIsRelay = opts.voice === void 0 ? sender?.is_relay === true : vSender?.is_relay === true;
40875
41448
  let audience = "unknown";
40876
41449
  if (meta && sender) {
40877
41450
  if (sender.is_agent) audience = "agent_to_agent";
@@ -40883,15 +41456,15 @@ var BridgeAgent = class _BridgeAgent {
40883
41456
  let tool_grants = NO_TOOL_GRANTS;
40884
41457
  let relaySpeaker;
40885
41458
  let memoryWrite = false;
40886
- const epochFresh = rosterFreshAt(entry, heldEpoch, heldGroupId);
40887
- const ownerDmFresh = epochFresh || this.ownerDmCycleFresh.has(channelId);
41459
+ const epochFresh = rosterFreshAt(entry, heldEpoch, heldGroupId) && (opts.voice === void 0 || rosterFreshAt(vEntry, opts.voice.heldEpoch, opts.voice.heldGroupId));
41460
+ const ownerDmFresh = epochFresh || opts.voice === void 0 && this.ownerDmCycleFresh.has(channelId);
40888
41461
  if (ownerDmFresh && this.ownerDmDefaultGrants && meta?.is_dm === true && sender) {
40889
41462
  const pinned = this.channelOwner.get(channelId);
40890
41463
  const ownerId = pinned && pinned !== "anomalous" ? pinned.identityId : void 0;
40891
41464
  if (ownerId !== void 0) {
40892
41465
  const ownerDirect = audience === "owner_dm" && senderId === ownerId;
40893
41466
  const owner = roster?.get(ownerId);
40894
- const ownerRelayed = relaySpeakerId !== void 0 && sender.is_relay && relaySpeakerId === ownerId && owner !== void 0 && !owner.is_agent && owner.is_agent_owner;
41467
+ const ownerRelayed = relaySpeakerId !== void 0 && senderIsRelay && relaySpeakerId === ownerId && owner !== void 0 && !owner.is_agent && owner.is_agent_owner;
40895
41468
  if (ownerDirect) tool_grants = opts.voiceScoped === true ? RELAYED_VOICE_GRANTS : OWNER_DM_DEFAULT_READ_GRANTS;
40896
41469
  if (ownerRelayed) tool_grants = RELAYED_VOICE_GRANTS;
40897
41470
  if (ownerRelayed) relaySpeaker = "owner";
@@ -41570,6 +42143,9 @@ var BridgeAgent = class _BridgeAgent {
41570
42143
  async ingestCycleBody(receiptIds) {
41571
42144
  if (this.closingDown) return;
41572
42145
  const cycleStart = performance.now();
42146
+ let voiceListed = false;
42147
+ const cycleStartedAt = Date.now();
42148
+ const joinedInPhase0 = /* @__PURE__ */ new Set();
41573
42149
  try {
41574
42150
  const rows = await this.latency.time("recv.pull", () => this.backend.pullDelivery(this.cfg.deviceId));
41575
42151
  this.recordRecvLag(rows);
@@ -41579,9 +42155,11 @@ var BridgeAgent = class _BridgeAgent {
41579
42155
  }
41580
42156
  for (const cid of channelIds) {
41581
42157
  if (this.joinState.get(cid) !== "JOINED") {
42158
+ joinedInPhase0.add(cid);
41582
42159
  await this.reconcileJoin(cid);
41583
42160
  }
41584
42161
  }
42162
+ if (await this.resolveUnlistedChannels(channelIds, false)) voiceListed = true;
41585
42163
  for (const cid of channelIds) {
41586
42164
  if (this.freshRoster(cid) === void 0) {
41587
42165
  await this.refreshChannelRoster(cid);
@@ -41606,6 +42184,13 @@ var BridgeAgent = class _BridgeAgent {
41606
42184
  }
41607
42185
  } catch {
41608
42186
  }
42187
+ const voiceStamp = await this.voicePositionSnapshot();
42188
+ const receiveStartedAt = Date.now();
42189
+ const stampsFor = (m) => {
42190
+ if (m.receivedAt >= receiveStartedAt) return true;
42191
+ const p = this.voiceChannels.parentOf(m.channelId);
42192
+ return m.receivedAt >= cycleStartedAt && p !== void 0 && !joinedInPhase0.has(p);
42193
+ };
41609
42194
  let entries;
41610
42195
  try {
41611
42196
  entries = await this.latency.time("recv.decrypt", () => this.provider.receiveOnce(this.cfg.deviceId));
@@ -41615,7 +42200,10 @@ var BridgeAgent = class _BridgeAgent {
41615
42200
  for (const cid of entries.supersededChannelIds ?? []) {
41616
42201
  this.joinState.set(cid, "JOINED");
41617
42202
  }
41618
- if (!entries.length) return;
42203
+ if (!entries.length) {
42204
+ await this.reevaluateVoiceBindings();
42205
+ return;
42206
+ }
41619
42207
  await this.maybePruneOwnerReplay();
41620
42208
  const receivedAt = Date.now();
41621
42209
  const purgeEpochAtReceipt = /* @__PURE__ */ new Map();
@@ -41628,16 +42216,31 @@ var BridgeAgent = class _BridgeAgent {
41628
42216
  if (this.unifiedOn && !itemCaptureAtReceipt.has(m.channelId)) itemCaptureAtReceipt.set(m.channelId, this.store.itemCapture(m.channelId));
41629
42217
  this.recallReceipt.set(m.messageId, { purgeEpoch: purgeEpochAtReceipt.get(m.channelId), modeEpoch: recallModeEpochAtReceipt });
41630
42218
  receiptIds.push(m.messageId);
42219
+ const stamp = voiceStamp.get(m.channelId);
42220
+ if (stamp !== void 0) this.voiceChannels.noteReceipt(m.messageId, stampsFor(m) ? stamp : null);
42221
+ else if (this.voiceChannels.isVoice(m.channelId) || this.isUnlistedChannel(m.channelId) && !this.voiceChannels.isResolved(m.channelId)) {
42222
+ this.voiceChannels.noteReceipt(m.messageId, null);
42223
+ }
41631
42224
  }
41632
- let metaRefreshed = false;
41633
- if (entries.some((m) => this.freshMeta(m.channelId) === void 0)) {
42225
+ if (await this.resolveUnlistedChannels(entries.map((m) => m.channelId), voiceListed)) voiceListed = true;
42226
+ await this.reevaluateVoiceBindings();
42227
+ entries = await this.gateVoiceEntries(entries);
42228
+ let metaRefreshed = voiceListed;
42229
+ if (!voiceListed && entries.some((m) => this.freshMeta(this.keyChannelForFrame(m)) === void 0)) {
41634
42230
  await this.refreshChannelMeta();
41635
42231
  metaRefreshed = true;
41636
42232
  }
41637
- await this.refreshRostersForCycle(entries.map((m) => m.channelId));
42233
+ await this.refreshRostersForCycle(entries.flatMap((m) => {
42234
+ const key = this.keyChannelForFrame(m);
42235
+ return key === m.channelId ? [m.channelId] : [m.channelId, key];
42236
+ }));
41638
42237
  const relayMissRefreshed = /* @__PURE__ */ new Set();
41639
42238
  const acked = [];
41640
42239
  for (const m of entries) {
42240
+ if (this.voiceChannels.isVoice(m.channelId) && !this.voiceFrameParent.has(m.messageId)) {
42241
+ acked.push(m.queueId);
42242
+ continue;
42243
+ }
41641
42244
  try {
41642
42245
  if (m.voiceMode === false) this.callBuffer.reset(m.channelId);
41643
42246
  const encId = m.envelope.enclave_id;
@@ -41798,7 +42401,9 @@ var BridgeAgent = class _BridgeAgent {
41798
42401
  }
41799
42402
  if (typeof parsed.text === "string") {
41800
42403
  if (parsed.text.trim() === VOICE_CALL_START_MARKER) this.callBuffer.reset(m.channelId);
41801
- const meta = this.freshMeta(m.channelId);
42404
+ const keyChannel = this.keyChannelForFrame(m);
42405
+ const voiceParent = keyChannel !== m.channelId ? keyChannel : void 0;
42406
+ const meta = this.freshMeta(keyChannel);
41802
42407
  const senderType = m.envelope.sender_type;
41803
42408
  const senderIsAgent = senderType === "agent" ? true : senderType === "human" ? false : void 0;
41804
42409
  const claimsRelaySpeaker = parsed.relay === true && typeof parsed.speaker_identity_id === "string" && typeof parsed.speaker_display_name === "string";
@@ -41845,14 +42450,15 @@ var BridgeAgent = class _BridgeAgent {
41845
42450
  senderIsRelay: this.freshRoster(m.channelId)?.get(m.envelope.sender_id)?.is_relay === true,
41846
42451
  text: parsed.text
41847
42452
  });
41848
- const ownerVerdict = await this.verifyOwnerTurn(m);
42453
+ const ownerVerdict = await this.verifyOwnerTurn(m, keyChannel);
41849
42454
  const turnAuthorization = await this.authorizeTurn(
41850
42455
  m.channelId,
41851
42456
  m.messageId,
41852
42457
  m.envelope.sender_id,
41853
42458
  ownerVerdict,
41854
42459
  speaker?.identityId,
41855
- voiceScoped
42460
+ voiceScoped,
42461
+ voiceParent
41856
42462
  );
41857
42463
  await this.prepareNotesForTurn(m.channelId, turnAuthorization);
41858
42464
  const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
@@ -41879,11 +42485,12 @@ var BridgeAgent = class _BridgeAgent {
41879
42485
  }
41880
42486
  let fastPathRoomBlocked = false;
41881
42487
  if (!coalescible && fastPathEligible && meta && meta.is_dm === false) {
41882
- const reserved = this.fastReplyReserved.get(m.channelId) ?? 0;
41883
- if (this.rateFloor.available(`room:${m.channelId}`) - reserved < 2) fastPathRoomBlocked = true;
42488
+ const floorKey = this.capChannelFor(m.channelId);
42489
+ const reserved = this.fastReplyReserved.get(floorKey) ?? 0;
42490
+ if (this.rateFloor.available(`room:${floorKey}`) - reserved < 2) fastPathRoomBlocked = true;
41884
42491
  }
41885
42492
  if (fastPathRoomBlocked) this.fastPathSkipped++;
41886
- const fastPathFiring = fastPathEligible && !fastPathRoomBlocked && !this.pausedChannels.has(m.channelId);
42493
+ const fastPathFiring = fastPathEligible && !fastPathRoomBlocked && !this.pausedFor(m.channelId);
41887
42494
  const fastGreeting = isCallStart && fastPathFiring;
41888
42495
  const runtimeVoice = (ackSent) => m.voiceMode ? {
41889
42496
  voice: true,
@@ -41989,11 +42596,12 @@ var BridgeAgent = class _BridgeAgent {
41989
42596
  void recordInboundMemory(this.memory, channelId, senderId, text, firstId, "voice", win.purgeEpoch);
41990
42597
  let roomBlocked = false;
41991
42598
  if (fastPathEligible && isRoom) {
41992
- const reserved = this.fastReplyReserved.get(channelId) ?? 0;
41993
- if (this.rateFloor.available(`room:${channelId}`) - reserved < 2) roomBlocked = true;
42599
+ const floorKey = this.capChannelFor(channelId);
42600
+ const reserved = this.fastReplyReserved.get(floorKey) ?? 0;
42601
+ if (this.rateFloor.available(`room:${floorKey}`) - reserved < 2) roomBlocked = true;
41994
42602
  }
41995
42603
  if (roomBlocked) this.fastPathSkipped++;
41996
- const plan = fastPathEligible && !roomBlocked && !this.pausedChannels.has(channelId) ? this.planFastPath(firstId, text, false) : void 0;
42604
+ const plan = fastPathEligible && !roomBlocked && !this.pausedFor(channelId) ? this.planFastPath(firstId, text, false) : void 0;
41997
42605
  if (!plan || plan.kind === "dup" || plan.kind === "skip") {
41998
42606
  if (plan?.kind === "skip" && plan.full) this.fastPathSkipped++;
41999
42607
  await this.settleHeldRuntimeMessage(merged, true);
@@ -42097,6 +42705,7 @@ var BridgeAgent = class _BridgeAgent {
42097
42705
  }
42098
42706
  }
42099
42707
  for (const m of entries) this.recallReceipt.delete(m.messageId);
42708
+ for (const m of entries) this.voiceFrameParent.delete(m.messageId);
42100
42709
  if (acked.length) {
42101
42710
  try {
42102
42711
  await this.latency.time("recv.ack", () => this.provider.ackDelivery(this.cfg.deviceId, acked));
@@ -42377,8 +42986,9 @@ var BridgeAgent = class _BridgeAgent {
42377
42986
  }
42378
42987
  }
42379
42988
  /** Spec §7.5 (T-14c, M-4): the runtime reports an executed-call cap for a room. Never throws. */
42380
- noteRoomCapReached(channelId) {
42381
- if (typeof channelId !== "string" || this.closingDown) return;
42989
+ noteRoomCapReached(reported) {
42990
+ if (typeof reported !== "string" || this.closingDown) return;
42991
+ const channelId = this.capChannelFor(reported);
42382
42992
  if (this.roomSearchDailyCap === 0) return;
42383
42993
  if (this.freshMeta(channelId)?.is_dm !== false || this.channelRoster.get(channelId)?.crossOrg !== false) return;
42384
42994
  const day = utcDayOf(Date.now());
@@ -42435,8 +43045,9 @@ ${channelId}`;
42435
43045
  const a = entry.authorization;
42436
43046
  if (!a || a.audience !== "team_room" || !a.tool_grants.some((g) => g.tool === "web_search")) return entry;
42437
43047
  const day = utcDayOf(Date.now());
43048
+ const room = this.capChannelFor(entry.channel_id);
42438
43049
  if (this.roomSearchDailyCap === 0) {
42439
- this.logRoomCap(day, entry.channel_id, "operator");
43050
+ this.logRoomCap(day, room, "operator");
42440
43051
  return this.withoutWebSearch(entry);
42441
43052
  }
42442
43053
  if (this.agentRoomSearchDailyCap === 0) {
@@ -42444,9 +43055,9 @@ ${channelId}`;
42444
43055
  return this.withoutWebSearch(entry);
42445
43056
  }
42446
43057
  try {
42447
- const n = this.store.countRoomSearchGrant(entry.channel_id, day);
42448
- if (n === this.roomSearchDailyCap && this.store.markRoomSearchCapReached(entry.channel_id, day, "bridge")) {
42449
- this.logRoomCap(day, entry.channel_id, "bridge");
43058
+ const n = this.store.countRoomSearchGrant(room, day);
43059
+ if (n === this.roomSearchDailyCap && this.store.markRoomSearchCapReached(room, day, "bridge")) {
43060
+ this.logRoomCap(day, room, "bridge");
42450
43061
  }
42451
43062
  if (n > this.roomSearchDailyCap) return this.withoutWebSearch(entry);
42452
43063
  const total = this.store.countRoomSearchGrant(AGENT_TOTAL_KEY, day);
@@ -42543,9 +43154,15 @@ ${channelId}`;
42543
43154
  * PAUSE_RECHECK_MS window, so release does not depend on new traffic.
42544
43155
  */
42545
43156
  heldForPause(channelId) {
42546
- const at = this.pausedChannels.get(channelId);
43157
+ let key = channelId;
43158
+ let at = this.pausedChannels.get(channelId);
43159
+ const parent3 = at === void 0 ? this.voiceChannels.parentOf(channelId) : void 0;
43160
+ if (parent3 !== void 0) {
43161
+ key = parent3;
43162
+ at = this.pausedChannels.get(parent3);
43163
+ }
42547
43164
  if (at === void 0) return false;
42548
- this.schedulePauseRecheck(channelId, at);
43165
+ this.schedulePauseRecheck(key, at);
42549
43166
  return true;
42550
43167
  }
42551
43168
  schedulePauseRecheck(channelId, confirmedAt) {
@@ -42574,7 +43191,7 @@ ${channelId}`;
42574
43191
  if (await this.probeOwnPause(channelId) === true) {
42575
43192
  const now = Date.now();
42576
43193
  this.pausedChannels.set(channelId, now);
42577
- if (this.buffer.some((m) => m.channel_id === channelId)) this.schedulePauseRecheck(channelId, now);
43194
+ if (this.buffer.some((m) => m.channel_id === channelId || this.voiceChannels.parentOf(m.channel_id) === channelId)) this.schedulePauseRecheck(channelId, now);
42578
43195
  return;
42579
43196
  }
42580
43197
  this.clearPause(channelId);
@@ -43053,6 +43670,10 @@ ${channelId}`;
43053
43670
  custody_store_unavailable: this.provider.custodyHealth().storeUnavailable,
43054
43671
  failed_attachments: this.failedAttachments,
43055
43672
  refused_vault_refs: this.refusedVaultRefs,
43673
+ voice_frames_refused: this.voiceFramesRefused,
43674
+ voice_frames_filtered: this.voiceFramesFiltered,
43675
+ voice_receipt_cap_reached: this.voiceChannels.receiptCapReached(),
43676
+ voice_relay_in_parent: this.voiceChannels.relayInParentCount(),
43056
43677
  fast_path_sent: this.fastPathSent,
43057
43678
  fast_path_failed: this.fastPathFailed,
43058
43679
  fast_path_skipped: this.fastPathSkipped,