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