@securecoms/agent-bridge 0.31.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) {
@@ -33659,6 +33705,8 @@ var schema = z2.object({
33659
33705
  model: z2.string().min(1),
33660
33706
  api_key: z2.string().min(1).optional(),
33661
33707
  api_key_env: z2.string().min(1).optional(),
33708
+ // E.2: only the exact string "direct" opts in; any other value is ignored (today's proxy behavior), never an error.
33709
+ route: z2.unknown().optional(),
33662
33710
  timeout_ms: z2.number().int().min(500).max(1e4).default(2500),
33663
33711
  max_tokens: z2.number().int().min(16).max(400).default(80)
33664
33712
  })
@@ -33683,7 +33731,8 @@ function parseVoiceConfig(raw, env) {
33683
33731
  model: v.fast_path.model,
33684
33732
  apiKey,
33685
33733
  timeoutMs: v.fast_path.timeout_ms,
33686
- maxTokens: v.fast_path.max_tokens
33734
+ maxTokens: v.fast_path.max_tokens,
33735
+ ...v.fast_path.route === "direct" ? { route: "direct" } : {}
33687
33736
  }
33688
33737
  };
33689
33738
  }
@@ -34297,6 +34346,7 @@ function lineHash(line) {
34297
34346
  return createHash5("sha256").update(line.trim(), "utf8").digest("hex");
34298
34347
  }
34299
34348
  var LOCAL_REPORT_FILE = "local-config.json";
34349
+ var LOCAL_PERSONA_FILE_NAMES = ["IDENTITY.md", "SOUL.md", "AGENTS.md"];
34300
34350
  var SECRET_PATH_COUNT_MAX = 1e4;
34301
34351
  var isCount = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0 && v <= SECRET_PATH_COUNT_MAX;
34302
34352
  function parseSecretPathsReport(raw) {
@@ -34378,18 +34428,23 @@ function readLocalReport(home, identityId) {
34378
34428
  const hasSearchKey = doc.has_search_key === true;
34379
34429
  const hasLlmKey = doc.has_llm_key === true;
34380
34430
  const raw = doc.persona;
34381
- const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
34382
- Object.entries(raw).filter(([, v]) => typeof v === "string")
34383
- ) : {};
34431
+ const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
34432
+ const personaFiles = [];
34433
+ for (const name of LOCAL_PERSONA_FILE_NAMES) {
34434
+ const text = persona[name];
34435
+ if (typeof text === "string" && text.trim() !== "") {
34436
+ personaFiles.push({ name, bytes: Buffer.byteLength(text, "utf8") });
34437
+ }
34438
+ }
34384
34439
  const capRaw = doc.owner_dm_search_cap_effective;
34385
34440
  const ownerDmCap = typeof capRaw === "number" && Number.isInteger(capRaw) && capRaw >= 0 && capRaw <= OWNER_DM_SEARCH_DAILY_CAP_MAX ? capRaw : void 0;
34386
34441
  return {
34387
- persona,
34442
+ local_persona_files: personaFiles,
34388
34443
  has_search_key: hasSearchKey,
34389
34444
  has_llm_key: hasLlmKey,
34390
34445
  has_extract_provider: doc.has_extract_provider === true,
34391
34446
  ...ownerDmCap !== void 0 ? { owner_dm_search_cap_effective: ownerDmCap } : {},
34392
- legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source),
34447
+ legacy_memory: localLegacySources(doc).map(({ path: _path, content: _content, ...source }) => source),
34393
34448
  secret_paths: parseSecretPathsReport(doc.secret_paths)
34394
34449
  };
34395
34450
  }
@@ -34474,16 +34529,6 @@ async function applyLegacyMemoryFactDecisions(home, identityId, rawDecisions, re
34474
34529
  }
34475
34530
  return { sourcesDeleted, factsAssigned };
34476
34531
  }
34477
- function readManagedMemory(home, identityId) {
34478
- const p = join15(runtimeConfigDir(home, identityId), MEMORY_FILE);
34479
- if (!existsSync4(p)) return null;
34480
- try {
34481
- const body = readFileSync8(p, "utf8").trim();
34482
- return body || null;
34483
- } catch {
34484
- return null;
34485
- }
34486
- }
34487
34532
  function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
34488
34533
  const dir = runtimeConfigDir(home, identityId);
34489
34534
  const path = join15(dir, MEMORY_FILE);
@@ -35176,6 +35221,213 @@ function roomIsCrossOrg(rows) {
35176
35221
  return unknown ? null : false;
35177
35222
  }
35178
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
+
35179
35431
  // src/grants/owner-turn.ts
35180
35432
  import { ed25519 as ed255196 } from "@noble/curves/ed25519";
35181
35433
  var HEX = /^(?:[0-9a-fA-F]{2})+$/;
@@ -35271,6 +35523,7 @@ var OWNER_DM_DEFAULT_READ_GRANTS = Object.freeze([
35271
35523
  var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
35272
35524
  Object.freeze({ tool: "current_time", mode: "read" })
35273
35525
  ]);
35526
+ var BRIDGE_CAPABILITIES = Object.freeze(["voice_channel_v1"]);
35274
35527
  var RELAYED_VOICE_GRANTS = Object.freeze([
35275
35528
  Object.freeze({ tool: "current_time", mode: "read" }),
35276
35529
  Object.freeze({ tool: "web_search", mode: "read" })
@@ -35545,12 +35798,19 @@ var RoomClassChangedSendError = class extends SendForbiddenError {
35545
35798
  this.message = `reply dropped for channel ${channelId}: the room changed after its tools were granted; no retry`;
35546
35799
  }
35547
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
+ }
35548
35807
  function rosterFreshAt(entry, heldEpoch, heldGroupId) {
35549
35808
  return entry !== void 0 && typeof heldEpoch === "number" && entry.epoch === heldEpoch && typeof heldGroupId === "string" && entry.groupId === heldGroupId;
35550
35809
  }
35551
35810
  var RoomGrantEpochMovedError = class extends Error {
35552
35811
  };
35553
35812
  var ROOM_GRANT_TRACK_MAX = 4096;
35813
+ var VOICE_FRAME_PARENT_MAX = 4096;
35554
35814
  var SessionSupersededError = class extends Error {
35555
35815
  constructor() {
35556
35816
  super("session_superseded: another copy of this agent took its session during start()");
@@ -35773,6 +36033,9 @@ var BridgeAgent = class _BridgeAgent {
35773
36033
  this.injectedFastLlm = !!deps.fastLlm;
35774
36034
  this.fileVoiceConfig = cfg.voice;
35775
36035
  this.fastLlm = deps.fastLlm ?? (cfg.voice?.fastPath.enabled ? new FastLlmClient(cfg.voice.fastPath, this.fetchImpl) : void 0);
36036
+ if (this.fileVoiceConfig?.fastPath.route === "direct" && !this.fileVoiceConfig.fastPath.apiKey) {
36037
+ writeServiceLog2("warn", "voice.fast_path_direct_no_key");
36038
+ }
35776
36039
  this.voiceSettingsSource = cfg.voice ? "file" : "none";
35777
36040
  this.latency = deps.latency ?? new LatencyRecorder();
35778
36041
  this.voiceCoalesceWindowMs = Math.max(0, deps.voiceCoalesceWindowMs ?? voiceCoalesceWindowFromEnv());
@@ -35979,6 +36242,13 @@ var BridgeAgent = class _BridgeAgent {
35979
36242
  supersedeRefundable = /* @__PURE__ */ new Set();
35980
36243
  /** M-1: channels this process has used as a FileSafe vault channel. */
35981
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();
35982
36252
  /** Phase 3 F3: consecutive agent turns per channel. */
35983
36253
  agentHops = new AgentHopTracker();
35984
36254
  maxAgentHops;
@@ -36009,6 +36279,12 @@ var BridgeAgent = class _BridgeAgent {
36009
36279
  * counter is incremented. Surfaced on health()/metrics().
36010
36280
  */
36011
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();
36012
36288
  /** Files link: vault_ref bodies refused by R1 (strict parse) or R2 (enclave binding). */
36013
36289
  refusedVaultRefs = 0;
36014
36290
  /**
@@ -36422,7 +36698,9 @@ var BridgeAgent = class _BridgeAgent {
36422
36698
  // Fix round MEDIUM 2: a channel this process used as a FileSafe vault
36423
36699
  // channel is never swept (the sweeper also refuses any channel no listing
36424
36700
  // has shown, which covers a vault channel after a restart).
36425
- 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))],
36426
36704
  enclaveOf: (c) => this.store.getChannelMeta(c)?.enclaveId,
36427
36705
  // Audit r1 M6: the persisted flag, never the channelOwner cache. Set only
36428
36706
  // from a listing, so undefined means no listing has ever shown the channel.
@@ -36572,6 +36850,7 @@ var BridgeAgent = class _BridgeAgent {
36572
36850
  const s = parseVoiceSettings(await res.json());
36573
36851
  if (s.identityDisplayName) this.lastOwnDisplayName = s.identityDisplayName;
36574
36852
  const proxy = settingsToVoiceConfig(s, { baseUrl: this.cfg.apiBase, apiKey: this.cfg.apiKey });
36853
+ const direct = this.fileVoiceConfig?.fastPath.route === "direct" ? this.fileVoiceConfig.fastPath : void 0;
36575
36854
  if (s.version === 0 && this.fileVoiceConfig) {
36576
36855
  const merged = {
36577
36856
  ...s,
@@ -36580,16 +36859,16 @@ var BridgeAgent = class _BridgeAgent {
36580
36859
  quirks: this.fileVoiceConfig.persona.quirks
36581
36860
  };
36582
36861
  this.voiceSettings = merged;
36583
- this.cfg.voice = { persona: this.fileVoiceConfig.persona, fastPath: proxy.fastPath, settings: merged };
36862
+ this.cfg.voice = { persona: this.fileVoiceConfig.persona, fastPath: direct ?? proxy.fastPath, settings: merged };
36584
36863
  this.voiceSettingsVersion = 0;
36585
36864
  this.voiceSettingsSource = "file";
36586
36865
  } else {
36587
36866
  this.voiceSettings = s;
36588
- this.cfg.voice = proxy;
36867
+ this.cfg.voice = direct ? { ...proxy, fastPath: direct } : proxy;
36589
36868
  this.voiceSettingsVersion = s.version;
36590
36869
  this.voiceSettingsSource = "backend";
36591
36870
  }
36592
- if (!this.injectedFastLlm) {
36871
+ if (!this.injectedFastLlm && !direct) {
36593
36872
  this.fastLlm = new FastLlmClient(this.cfg.voice.fastPath, this.fetchImpl);
36594
36873
  }
36595
36874
  writeServiceLog2("info", "voice.settings_loaded", {
@@ -36818,12 +37097,10 @@ var BridgeAgent = class _BridgeAgent {
36818
37097
  const report = {
36819
37098
  applied_version: version,
36820
37099
  paths: checkConfiguredPaths(skillsJson),
36821
- // Read AFTER the prunes above, so the owner's next view of memory is
36822
- // the file as it now stands, not the one they just pruned from.
36823
- memory: readManagedMemory(this.home, this.cfg.identityId),
36824
- // E13: pass through what the runtime reported. The daemon does not
36825
- // interpret any of it.
36826
- local_persona: local?.persona ?? null,
37100
+ // F-0 (G-5): the report carries no content. No MEMORY.md text, no persona text, no legacy
37101
+ // text. E13 needs only to know which of the runtime's own persona files hold something, so
37102
+ // this is [{name, bytes}] for each non-blank file and nothing else.
37103
+ local_persona_files: local?.local_persona_files ?? [],
36827
37104
  // Only the runtime knows whether a search key file is configured — it
36828
37105
  // is an environment value on that process, invisible to the daemon and
36829
37106
  // to the backend. The backend needs it to generate a truthful tool
@@ -36843,6 +37120,8 @@ var BridgeAgent = class _BridgeAgent {
36843
37120
  // the actual protected-memory adapter. It is an enum, never a path or
36844
37121
  // provider credential.
36845
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],
36846
37125
  // Spec §11.1-11.2 (OQ-E): flat keys; the string `memory` above is
36847
37126
  // unchanged. memory_notes is true only when notes are actually on (the
36848
37127
  // provider is securecoms and the parsed payload.memory keeps them on);
@@ -36856,9 +37135,9 @@ var BridgeAgent = class _BridgeAgent {
36856
37135
  ...this.reportedClearGeneration(),
36857
37136
  // 7b spec §9.2 (Task 19): capability versus state. Read-only: the report never starts the embedder.
36858
37137
  ...this.reportedRecall(),
36859
- // Inert legacy files only. `readLocalReport` revalidates the current
36860
- // regular-file bytes and strips every local path before this leaves the
36861
- // machine.
37138
+ // Inert legacy files only, metadata only. `readLocalReport` revalidates the current
37139
+ // regular-file bytes and strips every local path and every `content` before this
37140
+ // leaves the machine.
36862
37141
  legacy_memory: local?.legacy_memory ?? [],
36863
37142
  // §B.4: counts of server-named secret paths inside / allowlisted /
36864
37143
  // outside the secrets directory, and the runtime's mode. Numbers and
@@ -38090,6 +38369,7 @@ var BridgeAgent = class _BridgeAgent {
38090
38369
  * across a supersede), and the group must not move around the reads.
38091
38370
  */
38092
38371
  async reclassifyForReply(channelId, grant) {
38372
+ if (this.voiceChannels.isVoice(channelId)) return false;
38093
38373
  const startedAt = Date.now();
38094
38374
  await this.refreshChannelRoster(channelId);
38095
38375
  const held = await this.heldPosition(channelId);
@@ -38106,8 +38386,9 @@ var BridgeAgent = class _BridgeAgent {
38106
38386
  this.enforceRateFloor("a2a", channelId);
38107
38387
  return;
38108
38388
  }
38109
- const meta = this.freshMeta(channelId);
38110
- 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);
38111
38392
  }
38112
38393
  /** Bookkeeping after a CONFIRMED publish of `body` under `id`. */
38113
38394
  afterPublish(channelId, body, id, opts, purgeEpoch, replyCapture) {
@@ -38589,8 +38870,289 @@ var BridgeAgent = class _BridgeAgent {
38589
38870
  return probeResultFor("threw");
38590
38871
  }
38591
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
+ }
38592
39150
  /** §5.4: one conversation's turns, facts, notes and recall, inside the memory mutex, with every in-memory copy. */
38593
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) {
38594
39156
  const scopes = this.unifiedOn ? this.store.ownerDmScopeIds().filter((s) => parseOwnerDmScopeId(s)?.channelId === channelId) : [];
38595
39157
  await this.notes.purgeChannel(channelId);
38596
39158
  for (const s of scopes) this.store.clearConversationAbsence(s);
@@ -39830,17 +40392,17 @@ var BridgeAgent = class _BridgeAgent {
39830
40392
  async applyMemoryNote(raw, ctx) {
39831
40393
  if (this.unifiedOn) return this.applyUnifiedNote(raw, ctx);
39832
40394
  const ids = echoIds(raw);
39833
- const refuse = (reason, echo = ids) => {
40395
+ const refuse2 = (reason, echo = ids) => {
39834
40396
  writeServiceLog2("info", "memory.note_refused", {
39835
40397
  reason_class: ServiceLogEnum.of(reason, ["mode", "invalid", "message", "audience", "provenance", "secret", "cap", "cleared", "store", "budget", "purged", "not_owner_words"])
39836
40398
  });
39837
40399
  return { type: "memory_note_result", ...echo, ok: false, reason };
39838
40400
  };
39839
- if (!this.notesProviderOn()) return refuse("mode");
40401
+ if (!this.notesProviderOn()) return refuse2("mode");
39840
40402
  const f = parseMemoryNoteFrame(raw);
39841
- if (!f) return refuse("invalid");
39842
- if (ctx.pendingRequests.has(f.request_id)) return refuse("invalid", { message_id: f.message_id });
39843
- 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", {});
39844
40406
  ctx.pendingRequests.add(f.request_id);
39845
40407
  try {
39846
40408
  const w2 = () => {
@@ -39852,13 +40414,13 @@ var BridgeAgent = class _BridgeAgent {
39852
40414
  return cur.record;
39853
40415
  };
39854
40416
  const rec = w2();
39855
- if (rec === void 0) return refuse("message");
40417
+ if (rec === void 0) return refuse2("message");
39856
40418
  const channelId = rec.channel_id;
39857
- if (rec.authorization?.memory_write !== true) return refuse("audience");
39858
- 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");
39859
40421
  const held = await this.heldPosition(channelId);
39860
40422
  const now = this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false });
39861
- if (now.memory_write !== true) return refuse("audience");
40423
+ if (now.memory_write !== true) return refuse2("audience");
39862
40424
  const spend = async () => {
39863
40425
  const rw = await this.store.prepareRuntimeInboxRewrite(rec.message_id, bumpW9);
39864
40426
  if (rw === "declined") return "budget";
@@ -39874,44 +40436,44 @@ var BridgeAgent = class _BridgeAgent {
39874
40436
  };
39875
40437
  if (f.op === "forget" && typeof f.id === "object") {
39876
40438
  const shown = ctx.shownRecall(f.message_id, ctx.token);
39877
- if (!shown || !shown.has(f.id.recall)) return refuse("invalid");
40439
+ if (!shown || !shown.has(f.id.recall)) return refuse2("invalid");
39878
40440
  const rw = await spend();
39879
- if (typeof rw === "string") return refuse(rw);
39880
- if (w2() !== rec) return refuse("message");
39881
- 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");
39882
40444
  const r2 = await this.recall.forget(channelId, f.id.recall, rw, precommit(rw));
39883
- if (r2 === "unknown_id") return refuse("invalid");
39884
- if (r2 !== "ok") return refuse(r2);
40445
+ if (r2 === "unknown_id") return refuse2("invalid");
40446
+ if (r2 !== "ok") return refuse2(r2);
39885
40447
  writeServiceLog2("info", "memory.recall_forgotten");
39886
40448
  return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true };
39887
40449
  }
39888
40450
  let mutation;
39889
40451
  let rewrite;
39890
40452
  if (f.op === "forget") {
39891
- if (typeof f.id !== "number") return refuse("invalid");
40453
+ if (typeof f.id !== "number") return refuse2("invalid");
39892
40454
  const rw = await spend();
39893
- if (typeof rw === "string") return refuse(rw);
40455
+ if (typeof rw === "string") return refuse2(rw);
39894
40456
  rewrite = rw;
39895
40457
  mutation = { op: "forget", id: f.id, rewrite: rw };
39896
40458
  } else {
39897
40459
  const p = prepareStoredNote(f.text, { text: rec.text, ...rec.transcribed === true ? { transcribed: true } : {} });
39898
- if (!p.ok) return refuse(p.reason);
40460
+ if (!p.ok) return refuse2(p.reason);
39899
40461
  if (f.op === "replace") {
39900
- if (typeof f.id !== "number") return refuse("invalid");
40462
+ if (typeof f.id !== "number") return refuse2("invalid");
39901
40463
  const rw = await spend();
39902
- if (typeof rw === "string") return refuse(rw);
40464
+ if (typeof rw === "string") return refuse2(rw);
39903
40465
  rewrite = rw;
39904
40466
  mutation = { op: "replace", id: f.id, text: p.text, rewrite: rw };
39905
40467
  } else {
39906
40468
  mutation = { op: "add", text: p.text };
39907
40469
  }
39908
40470
  }
39909
- if (w2() !== rec) return refuse("message");
39910
- if (!this.notesProviderOn()) return refuse("mode");
40471
+ if (w2() !== rec) return refuse2("message");
40472
+ if (!this.notesProviderOn()) return refuse2("mode");
39911
40473
  const stamp = this.ingestGeneration.get(rec.message_id);
39912
- if (stamp === void 0) return refuse("cleared");
40474
+ if (stamp === void 0) return refuse2("cleared");
39913
40475
  const r = await this.notes.write(channelId, mutation, stamp, precommit(rewrite));
39914
- if (!r.ok) return refuse(r.reason);
40476
+ if (!r.ok) return refuse2(r.reason);
39915
40477
  writeServiceLog2("info", "memory.note_written", {
39916
40478
  op: ServiceLogEnum.of(f.op, ["add", "replace", "forget"]),
39917
40479
  notes_count: r.notesCount,
@@ -39920,7 +40482,7 @@ var BridgeAgent = class _BridgeAgent {
39920
40482
  return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true, id: r.id };
39921
40483
  } catch (e) {
39922
40484
  writeServiceLog2("warn", "memory.note_write_failed", {}, e);
39923
- return refuse("store");
40485
+ return refuse2("store");
39924
40486
  } finally {
39925
40487
  ctx.pendingRequests.delete(f.request_id);
39926
40488
  }
@@ -39934,17 +40496,17 @@ var BridgeAgent = class _BridgeAgent {
39934
40496
  */
39935
40497
  async applyUnifiedNote(raw, ctx) {
39936
40498
  const ids = echoIds(raw);
39937
- const refuse = (reason, echo = ids) => {
40499
+ const refuse2 = (reason, echo = ids) => {
39938
40500
  writeServiceLog2("info", "memory.note_refused", {
39939
40501
  reason_class: ServiceLogEnum.of(reason, ["mode", "invalid", "message", "audience", "provenance", "secret", "cap", "cleared", "store", "budget", "purged", "not_owner_words"])
39940
40502
  });
39941
40503
  return { type: "memory_note_result", ...echo, ok: false, reason };
39942
40504
  };
39943
- if (!this.unifiedProviderOn()) return refuse("mode");
40505
+ if (!this.unifiedProviderOn()) return refuse2("mode");
39944
40506
  const f = parseMemoryNoteFrame(raw);
39945
- if (!f) return refuse("invalid");
39946
- if (ctx.pendingRequests.has(f.request_id)) return refuse("invalid", { message_id: f.message_id });
39947
- 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", {});
39948
40510
  ctx.pendingRequests.add(f.request_id);
39949
40511
  try {
39950
40512
  const w2 = () => {
@@ -39956,25 +40518,25 @@ var BridgeAgent = class _BridgeAgent {
39956
40518
  return cur.record;
39957
40519
  };
39958
40520
  const rec = w2();
39959
- if (rec === void 0) return refuse("message");
40521
+ if (rec === void 0) return refuse2("message");
39960
40522
  const channelId = rec.channel_id;
39961
- if (rec.authorization?.memory_write !== true) return refuse("audience");
39962
- 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");
39963
40525
  const held = await this.heldPosition(channelId);
39964
- 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");
39965
40527
  const scopeId = this.ownerScopeNow(channelId);
39966
- if (scopeId === null) return refuse("audience");
40528
+ if (scopeId === null) return refuse2("audience");
39967
40529
  const precommit = () => !this.unifiedProviderOn() ? "mode" : w2() !== rec ? "message" : null;
39968
- if (f.op === "replace") return refuse("invalid");
39969
- 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);
39970
40532
  const stripped = allowlistPin(f.text);
39971
- if (stripped.trim().length === 0) return refuse("invalid");
40533
+ if (stripped.trim().length === 0) return refuse2("invalid");
39972
40534
  const shown = ctx.shownUnified(f.message_id, ctx.token) ?? /* @__PURE__ */ new Set();
39973
- if (!pinCoverage(stripped, rec.text, this.unifiedReads.coverageTexts(f.message_id, shown)).ok) return refuse("not_owner_words");
39974
- if (scanNote(stripped) !== null) return refuse("secret");
39975
- 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");
39976
40538
  const stamp = this.ingestGeneration.get(rec.message_id);
39977
- if (stamp === void 0) return refuse("cleared");
40539
+ if (stamp === void 0) return refuse2("cleared");
39978
40540
  const capture = this.store.itemCapture(channelId);
39979
40541
  const r = await this.items.commit({
39980
40542
  scopeKind: "owner_dm",
@@ -39992,7 +40554,7 @@ var BridgeAgent = class _BridgeAgent {
39992
40554
  });
39993
40555
  if (!r.ok) {
39994
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" };
39995
- return refuse(map[r.reason] ?? "store");
40557
+ return refuse2(map[r.reason] ?? "store");
39996
40558
  }
39997
40559
  await this.notes.exclusive(async () => this.unifiedReads.bumpGeneration());
39998
40560
  void this.send(channelId, pinnedLine(stripped), void 0, { agentTurn: false }).catch((e) => writeServiceLog2("warn", "memory.pinned_line_failed", {}, e));
@@ -40000,7 +40562,7 @@ var BridgeAgent = class _BridgeAgent {
40000
40562
  return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true, id: r.itemIds[0] };
40001
40563
  } catch (e) {
40002
40564
  writeServiceLog2("warn", "memory.note_write_failed", {}, e);
40003
- return refuse("store");
40565
+ return refuse2("store");
40004
40566
  } finally {
40005
40567
  ctx.pendingRequests.delete(f.request_id);
40006
40568
  }
@@ -40014,16 +40576,16 @@ var BridgeAgent = class _BridgeAgent {
40014
40576
  * is invalidated in the same mutex hold, the item leaves the ranking hot set, then the bridge publishes `Forgot:`
40015
40577
  * (OQ-P2: afterPublish keeps the line out of history and the items). Every reason is a key of UNIFIED_REFUSAL.
40016
40578
  */
40017
- async forgetUnified(f, rec, channelId, ctx, refuse, precommit) {
40018
- 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");
40019
40581
  const itemId = f.id.recall;
40020
40582
  const shown = ctx.shownUnified(f.message_id, ctx.token);
40021
- if (!shown || !shown.has(itemId)) return refuse("invalid");
40583
+ if (!shown || !shown.has(itemId)) return refuse2("invalid");
40022
40584
  const text = this.unifiedReads.shownText(f.message_id, itemId);
40023
- if (text === void 0) return refuse("invalid");
40585
+ if (text === void 0) return refuse2("invalid");
40024
40586
  const rw = await this.store.prepareRuntimeInboxRewrite(rec.message_id, bumpW9);
40025
- if (rw === "declined") return refuse("budget");
40026
- if (rw === "no_row") return refuse("message");
40587
+ if (rw === "declined") return refuse2("budget");
40588
+ if (rw === "no_row") return refuse2("message");
40027
40589
  const r = await this.notes.exclusive(async () => {
40028
40590
  const veto = precommit();
40029
40591
  if (veto !== null) return { ok: false, reason: veto };
@@ -40036,7 +40598,7 @@ var BridgeAgent = class _BridgeAgent {
40036
40598
  }
40037
40599
  return out;
40038
40600
  });
40039
- 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);
40040
40602
  this.rankingNote(() => this.ranking.removed(r.itemIds));
40041
40603
  void this.send(channelId, forgotLine(text), void 0, { agentTurn: false }).catch((e) => writeServiceLog2("warn", "memory.forgot_line_failed", {}, e));
40042
40604
  writeServiceLog2("info", "memory.item_forgotten");
@@ -40110,8 +40672,9 @@ var BridgeAgent = class _BridgeAgent {
40110
40672
  this.fastReplyChains.set(channelId, next);
40111
40673
  }
40112
40674
  /** Count a fast reply against FAST_REPLY_MAX_JOBS and reserve its room send-floor token; returns the release. */
40113
- reserveFastReply(channelId) {
40675
+ reserveFastReply(replyChannel) {
40114
40676
  this.fastReplyJobs++;
40677
+ const channelId = this.capChannelFor(replyChannel);
40115
40678
  this.fastReplyReserved.set(channelId, (this.fastReplyReserved.get(channelId) ?? 0) + 1);
40116
40679
  return () => {
40117
40680
  this.fastReplyJobs--;
@@ -40335,6 +40898,7 @@ var BridgeAgent = class _BridgeAgent {
40335
40898
  });
40336
40899
  if (!res.ok) throw new Error(`list_channels failed: ${res.status}`);
40337
40900
  const rows = await res.json();
40901
+ for (const r of rows) if (r.channel_kind === "voice") this.listedAsVoice.add(r.channel_id);
40338
40902
  for (const r of rows) this.store.recordChannelEnclave(r.channel_id, r.enclave_id);
40339
40903
  return rows.map((r) => ({
40340
40904
  channel_id: r.channel_id,
@@ -40418,6 +40982,7 @@ var BridgeAgent = class _BridgeAgent {
40418
40982
  */
40419
40983
  recordListedChannelMeta(channels, at) {
40420
40984
  for (const c of channels) {
40985
+ if (this.listedAsVoice.has(c.channel_id)) continue;
40421
40986
  this.channelMeta.set(c.channel_id, { is_dm: c.is_dm, name: c.name, at });
40422
40987
  this.store.recordChannelIsDm(c.channel_id, c.is_dm);
40423
40988
  }
@@ -40601,7 +41166,8 @@ var BridgeAgent = class _BridgeAgent {
40601
41166
  }
40602
41167
  const self = members.get(this.cfg.identityId);
40603
41168
  if (self?.name) this.lastOwnDisplayName = self.name;
40604
- 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);
40605
41171
  } catch {
40606
41172
  }
40607
41173
  }
@@ -40637,7 +41203,7 @@ var BridgeAgent = class _BridgeAgent {
40637
41203
  * held group (never the backend's row label, M-2). NEVER throws (M-4): any
40638
41204
  * exception is `failed verify_error`, and the row is acked like any other turn.
40639
41205
  */
40640
- async verifyOwnerTurn(m) {
41206
+ async verifyOwnerTurn(m, keyChannel = m.channelId) {
40641
41207
  const pin = this.cfg.ownerGrantPin?.pin ?? null;
40642
41208
  if (!pin || m.envelope.sender_id !== pin.ownerIdentityId) return OWNER_TURN_UNCHECKED;
40643
41209
  try {
@@ -40649,9 +41215,9 @@ var BridgeAgent = class _BridgeAgent {
40649
41215
  }
40650
41216
  if (v.sender_id !== pin.ownerIdentityId) return this.ownerTurnFailed(m, "malformed");
40651
41217
  let leafClass = "ok";
40652
- if (this.freshMeta(m.channelId)?.is_dm === true) {
41218
+ if (this.freshMeta(keyChannel)?.is_dm === true) {
40653
41219
  const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
40654
- 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");
40655
41221
  leafClass = classifyOwnerDmLeaves((await this.provider.leafTreeSummary(m.channelId)).leaves, {
40656
41222
  ownerIdentityId: pin.ownerIdentityId,
40657
41223
  agentIdentityId: this.cfg.identityId,
@@ -40815,9 +41381,14 @@ var BridgeAgent = class _BridgeAgent {
40815
41381
  });
40816
41382
  }
40817
41383
  /** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
40818
- async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId, voiceScoped) {
41384
+ async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId, voiceScoped, voiceParent) {
40819
41385
  const held = await this.heldPosition(channelId);
40820
- 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
+ });
40821
41392
  if (!(auth.audience === "team_room" && auth.tool_grants.length > 0)) return auth;
40822
41393
  if (this.roomGrantEpochs.size >= ROOM_GRANT_TRACK_MAX || held.epoch === null || held.groupId === null) {
40823
41394
  writeServiceLog2("warn", "room_grant.untracked");
@@ -40870,7 +41441,10 @@ var BridgeAgent = class _BridgeAgent {
40870
41441
  const meta = this.freshMeta(channelId);
40871
41442
  const entry = this.freshRosterEntry(channelId);
40872
41443
  const roster = entry?.members;
40873
- 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;
40874
41448
  let audience = "unknown";
40875
41449
  if (meta && sender) {
40876
41450
  if (sender.is_agent) audience = "agent_to_agent";
@@ -40882,15 +41456,15 @@ var BridgeAgent = class _BridgeAgent {
40882
41456
  let tool_grants = NO_TOOL_GRANTS;
40883
41457
  let relaySpeaker;
40884
41458
  let memoryWrite = false;
40885
- const epochFresh = rosterFreshAt(entry, heldEpoch, heldGroupId);
40886
- 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);
40887
41461
  if (ownerDmFresh && this.ownerDmDefaultGrants && meta?.is_dm === true && sender) {
40888
41462
  const pinned = this.channelOwner.get(channelId);
40889
41463
  const ownerId = pinned && pinned !== "anomalous" ? pinned.identityId : void 0;
40890
41464
  if (ownerId !== void 0) {
40891
41465
  const ownerDirect = audience === "owner_dm" && senderId === ownerId;
40892
41466
  const owner = roster?.get(ownerId);
40893
- 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;
40894
41468
  if (ownerDirect) tool_grants = opts.voiceScoped === true ? RELAYED_VOICE_GRANTS : OWNER_DM_DEFAULT_READ_GRANTS;
40895
41469
  if (ownerRelayed) tool_grants = RELAYED_VOICE_GRANTS;
40896
41470
  if (ownerRelayed) relaySpeaker = "owner";
@@ -41569,6 +42143,9 @@ var BridgeAgent = class _BridgeAgent {
41569
42143
  async ingestCycleBody(receiptIds) {
41570
42144
  if (this.closingDown) return;
41571
42145
  const cycleStart = performance.now();
42146
+ let voiceListed = false;
42147
+ const cycleStartedAt = Date.now();
42148
+ const joinedInPhase0 = /* @__PURE__ */ new Set();
41572
42149
  try {
41573
42150
  const rows = await this.latency.time("recv.pull", () => this.backend.pullDelivery(this.cfg.deviceId));
41574
42151
  this.recordRecvLag(rows);
@@ -41578,9 +42155,11 @@ var BridgeAgent = class _BridgeAgent {
41578
42155
  }
41579
42156
  for (const cid of channelIds) {
41580
42157
  if (this.joinState.get(cid) !== "JOINED") {
42158
+ joinedInPhase0.add(cid);
41581
42159
  await this.reconcileJoin(cid);
41582
42160
  }
41583
42161
  }
42162
+ if (await this.resolveUnlistedChannels(channelIds, false)) voiceListed = true;
41584
42163
  for (const cid of channelIds) {
41585
42164
  if (this.freshRoster(cid) === void 0) {
41586
42165
  await this.refreshChannelRoster(cid);
@@ -41605,6 +42184,13 @@ var BridgeAgent = class _BridgeAgent {
41605
42184
  }
41606
42185
  } catch {
41607
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
+ };
41608
42194
  let entries;
41609
42195
  try {
41610
42196
  entries = await this.latency.time("recv.decrypt", () => this.provider.receiveOnce(this.cfg.deviceId));
@@ -41614,7 +42200,10 @@ var BridgeAgent = class _BridgeAgent {
41614
42200
  for (const cid of entries.supersededChannelIds ?? []) {
41615
42201
  this.joinState.set(cid, "JOINED");
41616
42202
  }
41617
- if (!entries.length) return;
42203
+ if (!entries.length) {
42204
+ await this.reevaluateVoiceBindings();
42205
+ return;
42206
+ }
41618
42207
  await this.maybePruneOwnerReplay();
41619
42208
  const receivedAt = Date.now();
41620
42209
  const purgeEpochAtReceipt = /* @__PURE__ */ new Map();
@@ -41627,16 +42216,31 @@ var BridgeAgent = class _BridgeAgent {
41627
42216
  if (this.unifiedOn && !itemCaptureAtReceipt.has(m.channelId)) itemCaptureAtReceipt.set(m.channelId, this.store.itemCapture(m.channelId));
41628
42217
  this.recallReceipt.set(m.messageId, { purgeEpoch: purgeEpochAtReceipt.get(m.channelId), modeEpoch: recallModeEpochAtReceipt });
41629
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
+ }
41630
42224
  }
41631
- let metaRefreshed = false;
41632
- 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)) {
41633
42230
  await this.refreshChannelMeta();
41634
42231
  metaRefreshed = true;
41635
42232
  }
41636
- 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
+ }));
41637
42237
  const relayMissRefreshed = /* @__PURE__ */ new Set();
41638
42238
  const acked = [];
41639
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
+ }
41640
42244
  try {
41641
42245
  if (m.voiceMode === false) this.callBuffer.reset(m.channelId);
41642
42246
  const encId = m.envelope.enclave_id;
@@ -41797,7 +42401,9 @@ var BridgeAgent = class _BridgeAgent {
41797
42401
  }
41798
42402
  if (typeof parsed.text === "string") {
41799
42403
  if (parsed.text.trim() === VOICE_CALL_START_MARKER) this.callBuffer.reset(m.channelId);
41800
- 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);
41801
42407
  const senderType = m.envelope.sender_type;
41802
42408
  const senderIsAgent = senderType === "agent" ? true : senderType === "human" ? false : void 0;
41803
42409
  const claimsRelaySpeaker = parsed.relay === true && typeof parsed.speaker_identity_id === "string" && typeof parsed.speaker_display_name === "string";
@@ -41844,14 +42450,15 @@ var BridgeAgent = class _BridgeAgent {
41844
42450
  senderIsRelay: this.freshRoster(m.channelId)?.get(m.envelope.sender_id)?.is_relay === true,
41845
42451
  text: parsed.text
41846
42452
  });
41847
- const ownerVerdict = await this.verifyOwnerTurn(m);
42453
+ const ownerVerdict = await this.verifyOwnerTurn(m, keyChannel);
41848
42454
  const turnAuthorization = await this.authorizeTurn(
41849
42455
  m.channelId,
41850
42456
  m.messageId,
41851
42457
  m.envelope.sender_id,
41852
42458
  ownerVerdict,
41853
42459
  speaker?.identityId,
41854
- voiceScoped
42460
+ voiceScoped,
42461
+ voiceParent
41855
42462
  );
41856
42463
  await this.prepareNotesForTurn(m.channelId, turnAuthorization);
41857
42464
  const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
@@ -41878,11 +42485,12 @@ var BridgeAgent = class _BridgeAgent {
41878
42485
  }
41879
42486
  let fastPathRoomBlocked = false;
41880
42487
  if (!coalescible && fastPathEligible && meta && meta.is_dm === false) {
41881
- const reserved = this.fastReplyReserved.get(m.channelId) ?? 0;
41882
- 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;
41883
42491
  }
41884
42492
  if (fastPathRoomBlocked) this.fastPathSkipped++;
41885
- const fastPathFiring = fastPathEligible && !fastPathRoomBlocked && !this.pausedChannels.has(m.channelId);
42493
+ const fastPathFiring = fastPathEligible && !fastPathRoomBlocked && !this.pausedFor(m.channelId);
41886
42494
  const fastGreeting = isCallStart && fastPathFiring;
41887
42495
  const runtimeVoice = (ackSent) => m.voiceMode ? {
41888
42496
  voice: true,
@@ -41988,11 +42596,12 @@ var BridgeAgent = class _BridgeAgent {
41988
42596
  void recordInboundMemory(this.memory, channelId, senderId, text, firstId, "voice", win.purgeEpoch);
41989
42597
  let roomBlocked = false;
41990
42598
  if (fastPathEligible && isRoom) {
41991
- const reserved = this.fastReplyReserved.get(channelId) ?? 0;
41992
- 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;
41993
42602
  }
41994
42603
  if (roomBlocked) this.fastPathSkipped++;
41995
- 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;
41996
42605
  if (!plan || plan.kind === "dup" || plan.kind === "skip") {
41997
42606
  if (plan?.kind === "skip" && plan.full) this.fastPathSkipped++;
41998
42607
  await this.settleHeldRuntimeMessage(merged, true);
@@ -42096,6 +42705,7 @@ var BridgeAgent = class _BridgeAgent {
42096
42705
  }
42097
42706
  }
42098
42707
  for (const m of entries) this.recallReceipt.delete(m.messageId);
42708
+ for (const m of entries) this.voiceFrameParent.delete(m.messageId);
42099
42709
  if (acked.length) {
42100
42710
  try {
42101
42711
  await this.latency.time("recv.ack", () => this.provider.ackDelivery(this.cfg.deviceId, acked));
@@ -42376,8 +42986,9 @@ var BridgeAgent = class _BridgeAgent {
42376
42986
  }
42377
42987
  }
42378
42988
  /** Spec §7.5 (T-14c, M-4): the runtime reports an executed-call cap for a room. Never throws. */
42379
- noteRoomCapReached(channelId) {
42380
- if (typeof channelId !== "string" || this.closingDown) return;
42989
+ noteRoomCapReached(reported) {
42990
+ if (typeof reported !== "string" || this.closingDown) return;
42991
+ const channelId = this.capChannelFor(reported);
42381
42992
  if (this.roomSearchDailyCap === 0) return;
42382
42993
  if (this.freshMeta(channelId)?.is_dm !== false || this.channelRoster.get(channelId)?.crossOrg !== false) return;
42383
42994
  const day = utcDayOf(Date.now());
@@ -42434,8 +43045,9 @@ ${channelId}`;
42434
43045
  const a = entry.authorization;
42435
43046
  if (!a || a.audience !== "team_room" || !a.tool_grants.some((g) => g.tool === "web_search")) return entry;
42436
43047
  const day = utcDayOf(Date.now());
43048
+ const room = this.capChannelFor(entry.channel_id);
42437
43049
  if (this.roomSearchDailyCap === 0) {
42438
- this.logRoomCap(day, entry.channel_id, "operator");
43050
+ this.logRoomCap(day, room, "operator");
42439
43051
  return this.withoutWebSearch(entry);
42440
43052
  }
42441
43053
  if (this.agentRoomSearchDailyCap === 0) {
@@ -42443,9 +43055,9 @@ ${channelId}`;
42443
43055
  return this.withoutWebSearch(entry);
42444
43056
  }
42445
43057
  try {
42446
- const n = this.store.countRoomSearchGrant(entry.channel_id, day);
42447
- if (n === this.roomSearchDailyCap && this.store.markRoomSearchCapReached(entry.channel_id, day, "bridge")) {
42448
- 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");
42449
43061
  }
42450
43062
  if (n > this.roomSearchDailyCap) return this.withoutWebSearch(entry);
42451
43063
  const total = this.store.countRoomSearchGrant(AGENT_TOTAL_KEY, day);
@@ -42542,9 +43154,15 @@ ${channelId}`;
42542
43154
  * PAUSE_RECHECK_MS window, so release does not depend on new traffic.
42543
43155
  */
42544
43156
  heldForPause(channelId) {
42545
- 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
+ }
42546
43164
  if (at === void 0) return false;
42547
- this.schedulePauseRecheck(channelId, at);
43165
+ this.schedulePauseRecheck(key, at);
42548
43166
  return true;
42549
43167
  }
42550
43168
  schedulePauseRecheck(channelId, confirmedAt) {
@@ -42573,7 +43191,7 @@ ${channelId}`;
42573
43191
  if (await this.probeOwnPause(channelId) === true) {
42574
43192
  const now = Date.now();
42575
43193
  this.pausedChannels.set(channelId, now);
42576
- 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);
42577
43195
  return;
42578
43196
  }
42579
43197
  this.clearPause(channelId);
@@ -43052,6 +43670,10 @@ ${channelId}`;
43052
43670
  custody_store_unavailable: this.provider.custodyHealth().storeUnavailable,
43053
43671
  failed_attachments: this.failedAttachments,
43054
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(),
43055
43677
  fast_path_sent: this.fastPathSent,
43056
43678
  fast_path_failed: this.fastPathFailed,
43057
43679
  fast_path_skipped: this.fastPathSkipped,