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