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