@securecoms/agent-bridge 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/bin.js +780 -158
- package/dist/bridge-agent.d.ts +112 -1
- package/dist/index.js +763 -141
- package/dist/service/runtime-config-dir.d.ts +8 -2
- package/dist/version.d.ts +1 -1
- package/dist/voice/persona.d.ts +4 -0
- 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) {
|
|
@@ -33659,6 +33705,8 @@ var schema = z2.object({
|
|
|
33659
33705
|
model: z2.string().min(1),
|
|
33660
33706
|
api_key: z2.string().min(1).optional(),
|
|
33661
33707
|
api_key_env: z2.string().min(1).optional(),
|
|
33708
|
+
// E.2: only the exact string "direct" opts in; any other value is ignored (today's proxy behavior), never an error.
|
|
33709
|
+
route: z2.unknown().optional(),
|
|
33662
33710
|
timeout_ms: z2.number().int().min(500).max(1e4).default(2500),
|
|
33663
33711
|
max_tokens: z2.number().int().min(16).max(400).default(80)
|
|
33664
33712
|
})
|
|
@@ -33683,7 +33731,8 @@ function parseVoiceConfig(raw, env) {
|
|
|
33683
33731
|
model: v.fast_path.model,
|
|
33684
33732
|
apiKey,
|
|
33685
33733
|
timeoutMs: v.fast_path.timeout_ms,
|
|
33686
|
-
maxTokens: v.fast_path.max_tokens
|
|
33734
|
+
maxTokens: v.fast_path.max_tokens,
|
|
33735
|
+
...v.fast_path.route === "direct" ? { route: "direct" } : {}
|
|
33687
33736
|
}
|
|
33688
33737
|
};
|
|
33689
33738
|
}
|
|
@@ -34297,6 +34346,7 @@ function lineHash(line) {
|
|
|
34297
34346
|
return createHash5("sha256").update(line.trim(), "utf8").digest("hex");
|
|
34298
34347
|
}
|
|
34299
34348
|
var LOCAL_REPORT_FILE = "local-config.json";
|
|
34349
|
+
var LOCAL_PERSONA_FILE_NAMES = ["IDENTITY.md", "SOUL.md", "AGENTS.md"];
|
|
34300
34350
|
var SECRET_PATH_COUNT_MAX = 1e4;
|
|
34301
34351
|
var isCount = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0 && v <= SECRET_PATH_COUNT_MAX;
|
|
34302
34352
|
function parseSecretPathsReport(raw) {
|
|
@@ -34378,18 +34428,23 @@ function readLocalReport(home, identityId) {
|
|
|
34378
34428
|
const hasSearchKey = doc.has_search_key === true;
|
|
34379
34429
|
const hasLlmKey = doc.has_llm_key === true;
|
|
34380
34430
|
const raw = doc.persona;
|
|
34381
|
-
const persona = raw && typeof raw === "object" && !Array.isArray(raw) ?
|
|
34382
|
-
|
|
34383
|
-
)
|
|
34431
|
+
const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
34432
|
+
const personaFiles = [];
|
|
34433
|
+
for (const name of LOCAL_PERSONA_FILE_NAMES) {
|
|
34434
|
+
const text = persona[name];
|
|
34435
|
+
if (typeof text === "string" && text.trim() !== "") {
|
|
34436
|
+
personaFiles.push({ name, bytes: Buffer.byteLength(text, "utf8") });
|
|
34437
|
+
}
|
|
34438
|
+
}
|
|
34384
34439
|
const capRaw = doc.owner_dm_search_cap_effective;
|
|
34385
34440
|
const ownerDmCap = typeof capRaw === "number" && Number.isInteger(capRaw) && capRaw >= 0 && capRaw <= OWNER_DM_SEARCH_DAILY_CAP_MAX ? capRaw : void 0;
|
|
34386
34441
|
return {
|
|
34387
|
-
|
|
34442
|
+
local_persona_files: personaFiles,
|
|
34388
34443
|
has_search_key: hasSearchKey,
|
|
34389
34444
|
has_llm_key: hasLlmKey,
|
|
34390
34445
|
has_extract_provider: doc.has_extract_provider === true,
|
|
34391
34446
|
...ownerDmCap !== void 0 ? { owner_dm_search_cap_effective: ownerDmCap } : {},
|
|
34392
|
-
legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source),
|
|
34447
|
+
legacy_memory: localLegacySources(doc).map(({ path: _path, content: _content, ...source }) => source),
|
|
34393
34448
|
secret_paths: parseSecretPathsReport(doc.secret_paths)
|
|
34394
34449
|
};
|
|
34395
34450
|
}
|
|
@@ -34474,16 +34529,6 @@ async function applyLegacyMemoryFactDecisions(home, identityId, rawDecisions, re
|
|
|
34474
34529
|
}
|
|
34475
34530
|
return { sourcesDeleted, factsAssigned };
|
|
34476
34531
|
}
|
|
34477
|
-
function readManagedMemory(home, identityId) {
|
|
34478
|
-
const p = join15(runtimeConfigDir(home, identityId), MEMORY_FILE);
|
|
34479
|
-
if (!existsSync4(p)) return null;
|
|
34480
|
-
try {
|
|
34481
|
-
const body = readFileSync8(p, "utf8").trim();
|
|
34482
|
-
return body || null;
|
|
34483
|
-
} catch {
|
|
34484
|
-
return null;
|
|
34485
|
-
}
|
|
34486
|
-
}
|
|
34487
34532
|
function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
34488
34533
|
const dir = runtimeConfigDir(home, identityId);
|
|
34489
34534
|
const path = join15(dir, MEMORY_FILE);
|
|
@@ -35176,6 +35221,213 @@ function roomIsCrossOrg(rows) {
|
|
|
35176
35221
|
return unknown ? null : false;
|
|
35177
35222
|
}
|
|
35178
35223
|
|
|
35224
|
+
// src/voice-channel-binding.ts
|
|
35225
|
+
var BOUND = Object.freeze({ bound: true });
|
|
35226
|
+
var refuse = (reason) => ({ bound: false, reason });
|
|
35227
|
+
function evaluateVoiceBinding(v, p, isVRelay) {
|
|
35228
|
+
if (v.ownLeafIndex === null || v.leaves.length === 0) return refuse("v_not_held");
|
|
35229
|
+
if (p.ownLeafIndex === null || p.leaves.length === 0) return refuse("p_not_held");
|
|
35230
|
+
if (v.leaves.some((l) => !l.basic || l.identity === null)) return refuse("non_basic_leaf");
|
|
35231
|
+
const pIds = /* @__PURE__ */ new Set();
|
|
35232
|
+
for (const l of p.leaves) if (l.basic && l.identity !== null) pIds.add(l.identity);
|
|
35233
|
+
const nonParent = /* @__PURE__ */ new Set();
|
|
35234
|
+
for (const l of v.leaves) if (!pIds.has(l.identity)) nonParent.add(l.identity);
|
|
35235
|
+
if (nonParent.size > 1) return refuse("two_non_parent");
|
|
35236
|
+
if (isVRelay === void 0) return nonParent.size === 0 ? BOUND : refuse("relay_unknown");
|
|
35237
|
+
const relays = vRelayIdentities(v, isVRelay);
|
|
35238
|
+
if (relays.size > 1) return refuse("two_relays");
|
|
35239
|
+
const [relay] = relays;
|
|
35240
|
+
for (const id of nonParent) if (id !== relay) return refuse("foreign_leaf");
|
|
35241
|
+
return BOUND;
|
|
35242
|
+
}
|
|
35243
|
+
function vRelayIdentities(v, isVRelay) {
|
|
35244
|
+
const relays = /* @__PURE__ */ new Set();
|
|
35245
|
+
for (const l of v.leaves) if (l.basic && l.identity !== null && isVRelay(l.identity)) relays.add(l.identity);
|
|
35246
|
+
return relays;
|
|
35247
|
+
}
|
|
35248
|
+
function voiceRelayIdentity(v, isVRelay) {
|
|
35249
|
+
if (isVRelay === void 0) return void 0;
|
|
35250
|
+
const relays = vRelayIdentities(v, isVRelay);
|
|
35251
|
+
return relays.size === 1 ? [...relays][0] : void 0;
|
|
35252
|
+
}
|
|
35253
|
+
function parseVoiceResolution(channelId, status, body) {
|
|
35254
|
+
if (status === 404) return { kind: "absent" };
|
|
35255
|
+
if (status !== 200) return { kind: "error" };
|
|
35256
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return { kind: "error" };
|
|
35257
|
+
const b = body;
|
|
35258
|
+
if (b.channel_id !== channelId || typeof b.channel_kind !== "string") return { kind: "error" };
|
|
35259
|
+
if (b.channel_kind !== "voice") return { kind: "not_voice" };
|
|
35260
|
+
const parent3 = b.parent_channel_id;
|
|
35261
|
+
if (typeof parent3 !== "string" || parent3.length === 0 || parent3 === channelId) return { kind: "error" };
|
|
35262
|
+
return { kind: "voice", parentChannelId: parent3 };
|
|
35263
|
+
}
|
|
35264
|
+
var VOICE_RESOLVE_RETRY_BASE_MS = 3e4;
|
|
35265
|
+
var VOICE_RESOLVE_RETRY_MAX_MS = 10 * 6e4;
|
|
35266
|
+
var VOICE_RESOLVE_ABSENT_HELD_MAX_MS = 6e4;
|
|
35267
|
+
var VOICE_RECEIPT_CAP = 1e4;
|
|
35268
|
+
var VOICE_RECEIPT_MAX_AGE_MS = 15 * 6e4;
|
|
35269
|
+
function voiceParentStamp(p) {
|
|
35270
|
+
return JSON.stringify([p.groupId, p.epoch]);
|
|
35271
|
+
}
|
|
35272
|
+
var VoiceChannelRegistry = class {
|
|
35273
|
+
parents = /* @__PURE__ */ new Map();
|
|
35274
|
+
notVoice = /* @__PURE__ */ new Set();
|
|
35275
|
+
failures = /* @__PURE__ */ new Map();
|
|
35276
|
+
bindings = /* @__PURE__ */ new Map();
|
|
35277
|
+
generations = /* @__PURE__ */ new Map();
|
|
35278
|
+
/** Fix I-1 / Task 20 R1: message id -> P's held position before the batch that first received it; null = not knowable. */
|
|
35279
|
+
receipts = /* @__PURE__ */ new Map();
|
|
35280
|
+
/** Final review M-d: arrivals not recorded because the map was full of young records. */
|
|
35281
|
+
capReached = 0;
|
|
35282
|
+
/** True when a lookup should run now: never once resolved; after a failure, only past its back-off. */
|
|
35283
|
+
resolveDue(channelId, now, opts = {}) {
|
|
35284
|
+
if (this.parents.has(channelId) || this.notVoice.has(channelId)) return false;
|
|
35285
|
+
const f = this.failures.get(channelId);
|
|
35286
|
+
if (f === void 0) return true;
|
|
35287
|
+
let wait = Math.min(VOICE_RESOLVE_RETRY_MAX_MS, VOICE_RESOLVE_RETRY_BASE_MS * 2 ** Math.min(f.attempts - 1, 20));
|
|
35288
|
+
if (f.kind === "absent" && opts.heldGroup === true) wait = Math.min(wait, VOICE_RESOLVE_ABSENT_HELD_MAX_MS);
|
|
35289
|
+
return now - f.at >= wait;
|
|
35290
|
+
}
|
|
35291
|
+
record(channelId, r, now) {
|
|
35292
|
+
if (r.kind === "voice") {
|
|
35293
|
+
this.parents.set(channelId, r.parentChannelId);
|
|
35294
|
+
this.failures.delete(channelId);
|
|
35295
|
+
} else if (r.kind === "not_voice") {
|
|
35296
|
+
this.notVoice.add(channelId);
|
|
35297
|
+
this.failures.delete(channelId);
|
|
35298
|
+
} else {
|
|
35299
|
+
const prev = this.failures.get(channelId);
|
|
35300
|
+
this.failures.set(channelId, { at: now, attempts: (prev?.attempts ?? 0) + 1, kind: r.kind });
|
|
35301
|
+
}
|
|
35302
|
+
}
|
|
35303
|
+
/** True once get_channel has answered for this channel (voice or not). */
|
|
35304
|
+
isResolved(channelId) {
|
|
35305
|
+
return this.parents.has(channelId) || this.notVoice.has(channelId);
|
|
35306
|
+
}
|
|
35307
|
+
/** Final review M-a: true once get_channel answered this channel's own row with a kind other than voice. */
|
|
35308
|
+
isNotVoice(channelId) {
|
|
35309
|
+
return this.notVoice.has(channelId);
|
|
35310
|
+
}
|
|
35311
|
+
/** Final review (R∈P): bound Vs whose admitted R is also a P leaf (the pre-stamp shape; a sunset signal). */
|
|
35312
|
+
relayInParentCount() {
|
|
35313
|
+
let n = 0;
|
|
35314
|
+
for (const [v, e] of this.bindings) {
|
|
35315
|
+
if (this.parents.has(v) && e.verdict.bound && e.relay !== void 0 && e.parentMembers?.has(e.relay) === true) n++;
|
|
35316
|
+
}
|
|
35317
|
+
return n;
|
|
35318
|
+
}
|
|
35319
|
+
/** True for a resolved V, bound or not. */
|
|
35320
|
+
isVoice(channelId) {
|
|
35321
|
+
return this.parents.has(channelId);
|
|
35322
|
+
}
|
|
35323
|
+
/** P of a resolved V, bound or not; undefined otherwise. Never use this to route: see {@link boundParentOf}. */
|
|
35324
|
+
parentOf(channelId) {
|
|
35325
|
+
return this.parents.get(channelId);
|
|
35326
|
+
}
|
|
35327
|
+
/** Every resolved V with its P. */
|
|
35328
|
+
voiceChannels() {
|
|
35329
|
+
return [...this.parents.entries()];
|
|
35330
|
+
}
|
|
35331
|
+
/** The gate for a channel the bridge's listing has never classified (`unlisted`), or for a resolved V. */
|
|
35332
|
+
gate(channelId, unlisted) {
|
|
35333
|
+
if (this.parents.has(channelId)) return "drop";
|
|
35334
|
+
if (!unlisted || this.notVoice.has(channelId)) return "normal";
|
|
35335
|
+
return this.failures.get(channelId)?.kind === "absent" ? "drop" : "hold";
|
|
35336
|
+
}
|
|
35337
|
+
bindingKey(channelId) {
|
|
35338
|
+
return this.bindings.get(channelId)?.key;
|
|
35339
|
+
}
|
|
35340
|
+
setBinding(channelId, key, verdict, positions = null, admitted = {}) {
|
|
35341
|
+
this.bindings.set(channelId, { key, positions, verdict, ...admitted });
|
|
35342
|
+
}
|
|
35343
|
+
/** Fix M-1: each evaluation of a V takes a generation; only the newest started may store its verdict. */
|
|
35344
|
+
beginEvaluation(channelId) {
|
|
35345
|
+
const g = (this.generations.get(channelId) ?? 0) + 1;
|
|
35346
|
+
this.generations.set(channelId, g);
|
|
35347
|
+
return g;
|
|
35348
|
+
}
|
|
35349
|
+
isLatestEvaluation(channelId, generation) {
|
|
35350
|
+
return this.generations.get(channelId) === generation;
|
|
35351
|
+
}
|
|
35352
|
+
/**
|
|
35353
|
+
* Fix I-1: records a frame's receipt stamp the FIRST time it is seen; a re-served frame keeps its first record.
|
|
35354
|
+
* Task 20 R2: at the cap nothing is evicted and the new arrival is not recorded (refused: no record, no parent).
|
|
35355
|
+
*/
|
|
35356
|
+
noteReceipt(messageId, pStamp, now = Date.now()) {
|
|
35357
|
+
if (this.receipts.has(messageId)) return;
|
|
35358
|
+
if (this.receipts.size >= VOICE_RECEIPT_CAP) {
|
|
35359
|
+
for (const [id, r] of this.receipts) if (now - r.at > VOICE_RECEIPT_MAX_AGE_MS) this.receipts.delete(id);
|
|
35360
|
+
if (this.receipts.size >= VOICE_RECEIPT_CAP) {
|
|
35361
|
+
this.capReached++;
|
|
35362
|
+
return;
|
|
35363
|
+
}
|
|
35364
|
+
}
|
|
35365
|
+
this.receipts.set(messageId, { stamp: pStamp, at: now });
|
|
35366
|
+
}
|
|
35367
|
+
/** Final review M-d: arrivals refused because the receipt map was full (a number only). */
|
|
35368
|
+
receiptCapReached() {
|
|
35369
|
+
return this.capReached;
|
|
35370
|
+
}
|
|
35371
|
+
/** Test visibility: receipt records held. */
|
|
35372
|
+
receiptCount() {
|
|
35373
|
+
return this.receipts.size;
|
|
35374
|
+
}
|
|
35375
|
+
forgetReceipt(messageId) {
|
|
35376
|
+
this.receipts.delete(messageId);
|
|
35377
|
+
}
|
|
35378
|
+
/**
|
|
35379
|
+
* Fix I-1: P for one frame on V only when V is bound now AND both groups' held positions are exactly those at the
|
|
35380
|
+
* frame's receipt. A frame received at other positions (or with none recorded) gets undefined: dropped, never held.
|
|
35381
|
+
*/
|
|
35382
|
+
boundParentForFrame(channelId, messageId, openedAt) {
|
|
35383
|
+
return this.frameContext(channelId, messageId, openedAt)?.parent;
|
|
35384
|
+
}
|
|
35385
|
+
/**
|
|
35386
|
+
* Task 20: the per-frame verdict with what the accept filter needs. Bound now AND the frame's pre-batch P stamp equals
|
|
35387
|
+
* the verdict's P position AND (R1) the frame's own opening position equals the verdict's V position. A frame with
|
|
35388
|
+
* no record, a null record, or no `openedAt` is refused.
|
|
35389
|
+
*/
|
|
35390
|
+
frameContext(channelId, messageId, openedAt) {
|
|
35391
|
+
const parent3 = this.boundParentOf(channelId);
|
|
35392
|
+
if (parent3 === void 0) return void 0;
|
|
35393
|
+
const entry = this.bindings.get(channelId);
|
|
35394
|
+
const at = this.receipts.get(messageId)?.stamp;
|
|
35395
|
+
if (entry?.positions == null || typeof at !== "string" || at !== entry.positions.p) return void 0;
|
|
35396
|
+
const v = entry.positions.v;
|
|
35397
|
+
if (v === null || openedAt == null || openedAt.groupId !== v.groupId || openedAt.epoch !== v.epoch) return void 0;
|
|
35398
|
+
return { parent: parent3, relay: entry.relay, parentMembers: entry.parentMembers ?? /* @__PURE__ */ new Set() };
|
|
35399
|
+
}
|
|
35400
|
+
/** The current verdict, or undefined when never evaluated. */
|
|
35401
|
+
binding(channelId) {
|
|
35402
|
+
return this.bindings.get(channelId)?.verdict;
|
|
35403
|
+
}
|
|
35404
|
+
/** P only while V is a resolved V whose last verdict is bound; undefined otherwise. */
|
|
35405
|
+
boundParentOf(channelId) {
|
|
35406
|
+
const parent3 = this.parents.get(channelId);
|
|
35407
|
+
return parent3 !== void 0 && this.bindings.get(channelId)?.verdict.bound === true ? parent3 : void 0;
|
|
35408
|
+
}
|
|
35409
|
+
};
|
|
35410
|
+
|
|
35411
|
+
// src/voice-channel-frame.ts
|
|
35412
|
+
function classifyVoiceFrame(f) {
|
|
35413
|
+
if (f.senderId === f.ownIdentityId) return "drop";
|
|
35414
|
+
const fromRelay = f.relay !== void 0 && f.senderId === f.relay;
|
|
35415
|
+
const fromMember = f.parentMembers.has(f.senderId);
|
|
35416
|
+
if (!fromRelay && !fromMember) return "drop";
|
|
35417
|
+
let body;
|
|
35418
|
+
try {
|
|
35419
|
+
body = JSON.parse(new TextDecoder().decode(f.plaintext));
|
|
35420
|
+
} catch {
|
|
35421
|
+
return "drop";
|
|
35422
|
+
}
|
|
35423
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return "drop";
|
|
35424
|
+
const b = body;
|
|
35425
|
+
if (b.type === "call_transcript") return "call_transcript";
|
|
35426
|
+
if (b.type !== void 0 && b.type !== "text" || typeof b.text !== "string") return "drop";
|
|
35427
|
+
if (fromRelay) return "relay_turn";
|
|
35428
|
+
return b.text.trim() === VOICE_CALL_START_MARKER ? "greeting" : "drop";
|
|
35429
|
+
}
|
|
35430
|
+
|
|
35179
35431
|
// src/grants/owner-turn.ts
|
|
35180
35432
|
import { ed25519 as ed255196 } from "@noble/curves/ed25519";
|
|
35181
35433
|
var HEX = /^(?:[0-9a-fA-F]{2})+$/;
|
|
@@ -35271,6 +35523,7 @@ var OWNER_DM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
|
35271
35523
|
var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
35272
35524
|
Object.freeze({ tool: "current_time", mode: "read" })
|
|
35273
35525
|
]);
|
|
35526
|
+
var BRIDGE_CAPABILITIES = Object.freeze(["voice_channel_v1"]);
|
|
35274
35527
|
var RELAYED_VOICE_GRANTS = Object.freeze([
|
|
35275
35528
|
Object.freeze({ tool: "current_time", mode: "read" }),
|
|
35276
35529
|
Object.freeze({ tool: "web_search", mode: "read" })
|
|
@@ -35545,12 +35798,19 @@ var RoomClassChangedSendError = class extends SendForbiddenError {
|
|
|
35545
35798
|
this.message = `reply dropped for channel ${channelId}: the room changed after its tools were granted; no retry`;
|
|
35546
35799
|
}
|
|
35547
35800
|
};
|
|
35801
|
+
function voiceVerdictPositions(v, p) {
|
|
35802
|
+
return {
|
|
35803
|
+
v: v.groupId !== null && v.epoch !== null ? { groupId: v.groupId, epoch: v.epoch } : null,
|
|
35804
|
+
p: voiceParentStamp(p)
|
|
35805
|
+
};
|
|
35806
|
+
}
|
|
35548
35807
|
function rosterFreshAt(entry, heldEpoch, heldGroupId) {
|
|
35549
35808
|
return entry !== void 0 && typeof heldEpoch === "number" && entry.epoch === heldEpoch && typeof heldGroupId === "string" && entry.groupId === heldGroupId;
|
|
35550
35809
|
}
|
|
35551
35810
|
var RoomGrantEpochMovedError = class extends Error {
|
|
35552
35811
|
};
|
|
35553
35812
|
var ROOM_GRANT_TRACK_MAX = 4096;
|
|
35813
|
+
var VOICE_FRAME_PARENT_MAX = 4096;
|
|
35554
35814
|
var SessionSupersededError = class extends Error {
|
|
35555
35815
|
constructor() {
|
|
35556
35816
|
super("session_superseded: another copy of this agent took its session during start()");
|
|
@@ -35773,6 +36033,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35773
36033
|
this.injectedFastLlm = !!deps.fastLlm;
|
|
35774
36034
|
this.fileVoiceConfig = cfg.voice;
|
|
35775
36035
|
this.fastLlm = deps.fastLlm ?? (cfg.voice?.fastPath.enabled ? new FastLlmClient(cfg.voice.fastPath, this.fetchImpl) : void 0);
|
|
36036
|
+
if (this.fileVoiceConfig?.fastPath.route === "direct" && !this.fileVoiceConfig.fastPath.apiKey) {
|
|
36037
|
+
writeServiceLog2("warn", "voice.fast_path_direct_no_key");
|
|
36038
|
+
}
|
|
35776
36039
|
this.voiceSettingsSource = cfg.voice ? "file" : "none";
|
|
35777
36040
|
this.latency = deps.latency ?? new LatencyRecorder();
|
|
35778
36041
|
this.voiceCoalesceWindowMs = Math.max(0, deps.voiceCoalesceWindowMs ?? voiceCoalesceWindowFromEnv());
|
|
@@ -35979,6 +36242,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35979
36242
|
supersedeRefundable = /* @__PURE__ */ new Set();
|
|
35980
36243
|
/** M-1: channels this process has used as a FileSafe vault channel. */
|
|
35981
36244
|
vaultChannelIds = /* @__PURE__ */ new Set();
|
|
36245
|
+
/**
|
|
36246
|
+
* ADR-0024 step 2a Task 19 (spec §9): hidden voice channels V resolved to their conversation P, lookup back-off, and
|
|
36247
|
+
* each V's binding verdict. In memory only: a restart re-resolves (one get_channel per V) and re-evaluates.
|
|
36248
|
+
*/
|
|
36249
|
+
voiceChannels = new VoiceChannelRegistry();
|
|
36250
|
+
/** Fix M-3: channels a listing row marked channel_kind "voice"; never recorded as listed. */
|
|
36251
|
+
listedAsVoice = /* @__PURE__ */ new Set();
|
|
35982
36252
|
/** Phase 3 F3: consecutive agent turns per channel. */
|
|
35983
36253
|
agentHops = new AgentHopTracker();
|
|
35984
36254
|
maxAgentHops;
|
|
@@ -36009,6 +36279,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36009
36279
|
* counter is incremented. Surfaced on health()/metrics().
|
|
36010
36280
|
*/
|
|
36011
36281
|
failedAttachments = 0;
|
|
36282
|
+
/** Task 20 R4: V frames dropped by the per-frame verdict (not bound, or not at the verdict's positions). A number only. */
|
|
36283
|
+
voiceFramesRefused = 0;
|
|
36284
|
+
/** Task 20: frames on a bound V the accept filter dropped (not R's voice frame, a greeting marker or a transcript). */
|
|
36285
|
+
voiceFramesFiltered = 0;
|
|
36286
|
+
/** Task 20: message id of an admitted V frame -> its P (B.8 keys). Bounded; an entry lives one ingest cycle in practice. */
|
|
36287
|
+
voiceFrameParent = /* @__PURE__ */ new Map();
|
|
36012
36288
|
/** Files link: vault_ref bodies refused by R1 (strict parse) or R2 (enclave binding). */
|
|
36013
36289
|
refusedVaultRefs = 0;
|
|
36014
36290
|
/**
|
|
@@ -36422,7 +36698,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36422
36698
|
// Fix round MEDIUM 2: a channel this process used as a FileSafe vault
|
|
36423
36699
|
// channel is never swept (the sweeper also refuses any channel no listing
|
|
36424
36700
|
// has shown, which covers a vault channel after a restart).
|
|
36425
|
-
|
|
36701
|
+
// Task 20 final review I-1: a voice channel V is never listed, so it is swept as its parent P: V is present exactly
|
|
36702
|
+
// when P is, and purgeConversation(P) purges every V of P. P is swept even when only its V holds memory.
|
|
36703
|
+
conversations: () => [...new Set(this.store.memoryConversationIds().filter((c) => !this.vaultChannelIds.has(c)).map((c) => this.store.voiceChannelParent(c) ?? c))],
|
|
36426
36704
|
enclaveOf: (c) => this.store.getChannelMeta(c)?.enclaveId,
|
|
36427
36705
|
// Audit r1 M6: the persisted flag, never the channelOwner cache. Set only
|
|
36428
36706
|
// from a listing, so undefined means no listing has ever shown the channel.
|
|
@@ -36572,6 +36850,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36572
36850
|
const s = parseVoiceSettings(await res.json());
|
|
36573
36851
|
if (s.identityDisplayName) this.lastOwnDisplayName = s.identityDisplayName;
|
|
36574
36852
|
const proxy = settingsToVoiceConfig(s, { baseUrl: this.cfg.apiBase, apiKey: this.cfg.apiKey });
|
|
36853
|
+
const direct = this.fileVoiceConfig?.fastPath.route === "direct" ? this.fileVoiceConfig.fastPath : void 0;
|
|
36575
36854
|
if (s.version === 0 && this.fileVoiceConfig) {
|
|
36576
36855
|
const merged = {
|
|
36577
36856
|
...s,
|
|
@@ -36580,16 +36859,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36580
36859
|
quirks: this.fileVoiceConfig.persona.quirks
|
|
36581
36860
|
};
|
|
36582
36861
|
this.voiceSettings = merged;
|
|
36583
|
-
this.cfg.voice = { persona: this.fileVoiceConfig.persona, fastPath: proxy.fastPath, settings: merged };
|
|
36862
|
+
this.cfg.voice = { persona: this.fileVoiceConfig.persona, fastPath: direct ?? proxy.fastPath, settings: merged };
|
|
36584
36863
|
this.voiceSettingsVersion = 0;
|
|
36585
36864
|
this.voiceSettingsSource = "file";
|
|
36586
36865
|
} else {
|
|
36587
36866
|
this.voiceSettings = s;
|
|
36588
|
-
this.cfg.voice = proxy;
|
|
36867
|
+
this.cfg.voice = direct ? { ...proxy, fastPath: direct } : proxy;
|
|
36589
36868
|
this.voiceSettingsVersion = s.version;
|
|
36590
36869
|
this.voiceSettingsSource = "backend";
|
|
36591
36870
|
}
|
|
36592
|
-
if (!this.injectedFastLlm) {
|
|
36871
|
+
if (!this.injectedFastLlm && !direct) {
|
|
36593
36872
|
this.fastLlm = new FastLlmClient(this.cfg.voice.fastPath, this.fetchImpl);
|
|
36594
36873
|
}
|
|
36595
36874
|
writeServiceLog2("info", "voice.settings_loaded", {
|
|
@@ -36818,12 +37097,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36818
37097
|
const report = {
|
|
36819
37098
|
applied_version: version,
|
|
36820
37099
|
paths: checkConfiguredPaths(skillsJson),
|
|
36821
|
-
//
|
|
36822
|
-
//
|
|
36823
|
-
|
|
36824
|
-
|
|
36825
|
-
// interpret any of it.
|
|
36826
|
-
local_persona: local?.persona ?? null,
|
|
37100
|
+
// F-0 (G-5): the report carries no content. No MEMORY.md text, no persona text, no legacy
|
|
37101
|
+
// text. E13 needs only to know which of the runtime's own persona files hold something, so
|
|
37102
|
+
// this is [{name, bytes}] for each non-blank file and nothing else.
|
|
37103
|
+
local_persona_files: local?.local_persona_files ?? [],
|
|
36827
37104
|
// Only the runtime knows whether a search key file is configured — it
|
|
36828
37105
|
// is an environment value on that process, invisible to the daemon and
|
|
36829
37106
|
// to the backend. The backend needs it to generate a truthful tool
|
|
@@ -36843,6 +37120,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36843
37120
|
// the actual protected-memory adapter. It is an enum, never a path or
|
|
36844
37121
|
// provider credential.
|
|
36845
37122
|
memory_provider: this.cfg.memoryProvider ?? "securecoms",
|
|
37123
|
+
// Spec §2 (Task 18): the per-device capability declaration; a fixed enum list, one body for every report path.
|
|
37124
|
+
capabilities: [...BRIDGE_CAPABILITIES],
|
|
36846
37125
|
// Spec §11.1-11.2 (OQ-E): flat keys; the string `memory` above is
|
|
36847
37126
|
// unchanged. memory_notes is true only when notes are actually on (the
|
|
36848
37127
|
// provider is securecoms and the parsed payload.memory keeps them on);
|
|
@@ -36856,9 +37135,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36856
37135
|
...this.reportedClearGeneration(),
|
|
36857
37136
|
// 7b spec §9.2 (Task 19): capability versus state. Read-only: the report never starts the embedder.
|
|
36858
37137
|
...this.reportedRecall(),
|
|
36859
|
-
// Inert legacy files only. `readLocalReport` revalidates the current
|
|
36860
|
-
// regular-file bytes and strips every local path before this
|
|
36861
|
-
// machine.
|
|
37138
|
+
// Inert legacy files only, metadata only. `readLocalReport` revalidates the current
|
|
37139
|
+
// regular-file bytes and strips every local path and every `content` before this
|
|
37140
|
+
// leaves the machine.
|
|
36862
37141
|
legacy_memory: local?.legacy_memory ?? [],
|
|
36863
37142
|
// §B.4: counts of server-named secret paths inside / allowlisted /
|
|
36864
37143
|
// outside the secrets directory, and the runtime's mode. Numbers and
|
|
@@ -38090,6 +38369,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38090
38369
|
* across a supersede), and the group must not move around the reads.
|
|
38091
38370
|
*/
|
|
38092
38371
|
async reclassifyForReply(channelId, grant) {
|
|
38372
|
+
if (this.voiceChannels.isVoice(channelId)) return false;
|
|
38093
38373
|
const startedAt = Date.now();
|
|
38094
38374
|
await this.refreshChannelRoster(channelId);
|
|
38095
38375
|
const held = await this.heldPosition(channelId);
|
|
@@ -38106,8 +38386,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38106
38386
|
this.enforceRateFloor("a2a", channelId);
|
|
38107
38387
|
return;
|
|
38108
38388
|
}
|
|
38109
|
-
const
|
|
38110
|
-
|
|
38389
|
+
const capChannel = this.capChannelFor(channelId);
|
|
38390
|
+
const meta = this.freshMeta(capChannel);
|
|
38391
|
+
if (meta && !meta.is_dm) this.enforceRateFloor("room", capChannel);
|
|
38111
38392
|
}
|
|
38112
38393
|
/** Bookkeeping after a CONFIRMED publish of `body` under `id`. */
|
|
38113
38394
|
afterPublish(channelId, body, id, opts, purgeEpoch, replyCapture) {
|
|
@@ -38589,8 +38870,289 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38589
38870
|
return probeResultFor("threw");
|
|
38590
38871
|
}
|
|
38591
38872
|
}
|
|
38873
|
+
// ── ADR-0024 step 2a Task 19: the hidden voice channel V (spec §9, B.8) ──────
|
|
38874
|
+
//
|
|
38875
|
+
// The rule and the cache live in voice-channel-binding.ts; this is the wiring: the get_channel lookup, the bridge's
|
|
38876
|
+
// own held ratchet trees and positions, V's member list (is_relay only), and the receive-cycle gate.
|
|
38877
|
+
/**
|
|
38878
|
+
* Task 20's accessor: P's channel id ONLY while V is bound under the spec §9 rule at both groups' held positions as
|
|
38879
|
+
* of the last evaluation (every receive cycle, before any frame is processed; or {@link voiceParentNow}); undefined
|
|
38880
|
+
* for an unbound or refused V, and for any channel that is not a resolved V.
|
|
38881
|
+
*/
|
|
38882
|
+
voiceParentOf(channelId) {
|
|
38883
|
+
return this.voiceChannels.boundParentOf(channelId);
|
|
38884
|
+
}
|
|
38885
|
+
/** True for a channel resolved as a V, bound or not. Such a channel is never processed as a normal channel. */
|
|
38886
|
+
isKnownVoiceChannel(channelId) {
|
|
38887
|
+
return this.voiceChannels.isVoice(channelId);
|
|
38888
|
+
}
|
|
38889
|
+
/** Re-evaluates one V at the held positions now, then answers as {@link voiceParentOf}. */
|
|
38890
|
+
async voiceParentNow(channelId) {
|
|
38891
|
+
const parent3 = this.voiceChannels.parentOf(channelId);
|
|
38892
|
+
if (parent3 === void 0) return void 0;
|
|
38893
|
+
await this.reevaluateVoiceBinding(channelId, parent3);
|
|
38894
|
+
return this.voiceChannels.boundParentOf(channelId);
|
|
38895
|
+
}
|
|
38896
|
+
/** A channel no listing has ever classified (in memory or persisted). V is hidden from the listing, so it is one. */
|
|
38897
|
+
isUnlistedChannel(channelId) {
|
|
38898
|
+
return this.channelMeta.get(channelId) === void 0 && this.store.getChannelMeta(channelId)?.isDm === void 0;
|
|
38899
|
+
}
|
|
38900
|
+
voiceChannelGate(channelId) {
|
|
38901
|
+
if (this.vaultChannelIds.has(channelId)) return "normal";
|
|
38902
|
+
return this.voiceChannels.gate(channelId, this.isUnlistedChannel(channelId));
|
|
38903
|
+
}
|
|
38904
|
+
/**
|
|
38905
|
+
* Looks up one channel with get_channel (the voice arm, OQ-5 (a)). Only the classification is kept. Never throws; a
|
|
38906
|
+
* failure is recorded and retried (30 s doubling to 10 min). Called only from {@link resolveUnlistedChannels}, which
|
|
38907
|
+
* selects unlisted channels whose back-off allows.
|
|
38908
|
+
*/
|
|
38909
|
+
async resolveVoiceChannel(channelId) {
|
|
38910
|
+
let status = "threw";
|
|
38911
|
+
let body;
|
|
38912
|
+
try {
|
|
38913
|
+
const url = `${this.cfg.apiBase}/api/v1/channels/${encodeURIComponent(channelId)}?caller_identity_id=${encodeURIComponent(this.cfg.identityId)}`;
|
|
38914
|
+
const res = await this.fetchBounded("get_channel", "read", url, { headers: { authorization: `Bearer ${this.cfg.apiKey}` } });
|
|
38915
|
+
if (res.ok) {
|
|
38916
|
+
body = await res.json();
|
|
38917
|
+
} else {
|
|
38918
|
+
try {
|
|
38919
|
+
await res.body?.cancel();
|
|
38920
|
+
} catch {
|
|
38921
|
+
}
|
|
38922
|
+
}
|
|
38923
|
+
status = res.status;
|
|
38924
|
+
} catch {
|
|
38925
|
+
status = "threw";
|
|
38926
|
+
}
|
|
38927
|
+
const r = parseVoiceResolution(channelId, status, body);
|
|
38928
|
+
this.voiceChannels.record(channelId, r, Date.now());
|
|
38929
|
+
if (r.kind === "voice") {
|
|
38930
|
+
try {
|
|
38931
|
+
this.store.recordVoiceChannelParent(channelId, r.parentChannelId);
|
|
38932
|
+
} catch {
|
|
38933
|
+
}
|
|
38934
|
+
}
|
|
38935
|
+
if (r.kind === "voice") writeServiceLog2("info", "voice.channel_lookup", { result: ServiceLogEnum.of("voice", ["voice", "absent", "error"]) });
|
|
38936
|
+
else if (r.kind !== "not_voice") writeServiceLog2("warn", "voice.channel_lookup", { result: ServiceLogEnum.of(r.kind, ["voice", "absent", "error"]) });
|
|
38937
|
+
}
|
|
38938
|
+
/**
|
|
38939
|
+
* Looks up every unlisted channel among `channelIds` whose back-off allows. A channel is "unlisted" only after this
|
|
38940
|
+
* cycle's listing: if any candidate remains and the cycle has not listed yet, one listing runs first, so a channel
|
|
38941
|
+
* that is merely new (listed, not yet cached) is never looked up. Returns true when it ran that listing.
|
|
38942
|
+
*/
|
|
38943
|
+
async resolveUnlistedChannels(channelIds, listed) {
|
|
38944
|
+
const ids = [...new Set(channelIds)].filter((c) => !this.vaultChannelIds.has(c));
|
|
38945
|
+
const held = /* @__PURE__ */ new Set();
|
|
38946
|
+
for (const c of ids) {
|
|
38947
|
+
if (!this.isUnlistedChannel(c) || this.voiceChannels.isResolved(c)) continue;
|
|
38948
|
+
try {
|
|
38949
|
+
if ((await this.heldPosition(c)).groupId !== null) held.add(c);
|
|
38950
|
+
} catch {
|
|
38951
|
+
}
|
|
38952
|
+
}
|
|
38953
|
+
const due = () => ids.filter((c) => this.isUnlistedChannel(c) && this.voiceChannels.resolveDue(c, Date.now(), { heldGroup: held.has(c) }));
|
|
38954
|
+
let candidates = due();
|
|
38955
|
+
if (candidates.length === 0) return false;
|
|
38956
|
+
let ran = false;
|
|
38957
|
+
if (!listed) {
|
|
38958
|
+
await this.refreshChannelMeta();
|
|
38959
|
+
ran = true;
|
|
38960
|
+
candidates = due();
|
|
38961
|
+
}
|
|
38962
|
+
for (const c of candidates) await this.resolveVoiceChannel(c);
|
|
38963
|
+
return ran;
|
|
38964
|
+
}
|
|
38965
|
+
/** Final review M-c: V -> consecutive failed member-list reads for the binding, and when the last one ran. */
|
|
38966
|
+
voiceRosterRetry = /* @__PURE__ */ new Map();
|
|
38967
|
+
/** The first retry after a failure is immediate (next cycle); then 30 s, doubling, capped at 10 min. */
|
|
38968
|
+
voiceRosterRetryDue(v) {
|
|
38969
|
+
const f = this.voiceRosterRetry.get(v);
|
|
38970
|
+
if (f === void 0 || f.attempts <= 1) return true;
|
|
38971
|
+
const wait = Math.min(VOICE_RESOLVE_RETRY_MAX_MS, VOICE_RESOLVE_RETRY_BASE_MS * 2 ** Math.min(f.attempts - 2, 20));
|
|
38972
|
+
return Date.now() - f.at >= wait;
|
|
38973
|
+
}
|
|
38974
|
+
noteVoiceRosterRead(v, ok) {
|
|
38975
|
+
if (ok) {
|
|
38976
|
+
this.voiceRosterRetry.delete(v);
|
|
38977
|
+
return;
|
|
38978
|
+
}
|
|
38979
|
+
this.voiceRosterRetry.set(v, { at: Date.now(), attempts: (this.voiceRosterRetry.get(v)?.attempts ?? 0) + 1 });
|
|
38980
|
+
}
|
|
38981
|
+
/** Every resolved V, re-evaluated where a position or a roster moved. */
|
|
38982
|
+
async reevaluateVoiceBindings() {
|
|
38983
|
+
for (const [v, p] of this.voiceChannels.voiceChannels()) await this.reevaluateVoiceBinding(v, p);
|
|
38984
|
+
}
|
|
38985
|
+
/**
|
|
38986
|
+
* The spec §9 binding rule over the bridge's OWN ratchet trees (never a server list of leaves), keyed by both
|
|
38987
|
+
* groups' held (group, epoch) and both rosters' fetch times: a change in any of them re-evaluates. V's member list
|
|
38988
|
+
* is read only for `is_relay`, and only when it is fresh at V's held position. Fail closed: a tree that moved while
|
|
38989
|
+
* it was read, or any exception, leaves V unbound and is re-evaluated next time.
|
|
38990
|
+
*/
|
|
38991
|
+
async reevaluateVoiceBinding(v, p) {
|
|
38992
|
+
const generation = this.voiceChannels.beginEvaluation(v);
|
|
38993
|
+
try {
|
|
38994
|
+
const vPos = await this.heldPosition(v);
|
|
38995
|
+
const pPos = await this.heldPosition(p);
|
|
38996
|
+
const positions = voiceVerdictPositions(vPos, pPos);
|
|
38997
|
+
const parentNotConversation = this.voiceChannels.isVoice(p) || this.isUnlistedChannel(p);
|
|
38998
|
+
const keyOf = () => JSON.stringify([positions, this.freshRosterEntry(v)?.at ?? null, this.freshRosterEntry(p)?.at ?? null, parentNotConversation]);
|
|
38999
|
+
if (this.voiceChannels.bindingKey(v) === keyOf()) return;
|
|
39000
|
+
if (parentNotConversation) {
|
|
39001
|
+
if (this.voiceChannels.isLatestEvaluation(v, generation)) this.noteVoiceBinding(v, keyOf(), { bound: false, reason: "parent_not_conversation" }, positions);
|
|
39002
|
+
return;
|
|
39003
|
+
}
|
|
39004
|
+
const vTree = await this.provider.leafTreeSummary(v);
|
|
39005
|
+
const pTree = await this.provider.leafTreeSummary(p);
|
|
39006
|
+
const relayCheck = () => {
|
|
39007
|
+
const entry = this.freshRosterEntry(v);
|
|
39008
|
+
return entry !== void 0 && rosterFreshAt(entry, vPos.epoch, vPos.groupId) ? (id) => entry.members.get(id)?.is_relay === true : void 0;
|
|
39009
|
+
};
|
|
39010
|
+
if (relayCheck() === void 0 && this.voiceRosterRetryDue(v)) {
|
|
39011
|
+
await this.refreshChannelRoster(v);
|
|
39012
|
+
this.noteVoiceRosterRead(v, relayCheck() !== void 0);
|
|
39013
|
+
}
|
|
39014
|
+
const isVRelay = relayCheck();
|
|
39015
|
+
let verdict = evaluateVoiceBinding(vTree, pTree, isVRelay);
|
|
39016
|
+
const vAfter = await this.heldPosition(v);
|
|
39017
|
+
const pAfter = await this.heldPosition(p);
|
|
39018
|
+
const moved = vAfter.groupId !== vPos.groupId || vAfter.epoch !== vPos.epoch || pAfter.groupId !== pPos.groupId || pAfter.epoch !== pPos.epoch;
|
|
39019
|
+
if (moved) verdict = { bound: false, reason: "moved" };
|
|
39020
|
+
const retry = !verdict.bound && (verdict.reason === "relay_unknown" || verdict.reason === "moved") || verdict.bound && isVRelay === void 0;
|
|
39021
|
+
if (!this.voiceChannels.isLatestEvaluation(v, generation)) return;
|
|
39022
|
+
const parentMembers = /* @__PURE__ */ new Set();
|
|
39023
|
+
for (const l of pTree.leaves) if (l.basic && l.identity !== null) parentMembers.add(l.identity);
|
|
39024
|
+
this.noteVoiceBinding(v, retry ? "" : keyOf(), verdict, positions, void 0, {
|
|
39025
|
+
relay: verdict.bound ? voiceRelayIdentity(vTree, isVRelay) : void 0,
|
|
39026
|
+
parentMembers
|
|
39027
|
+
});
|
|
39028
|
+
} catch (e) {
|
|
39029
|
+
if (this.voiceChannels.isLatestEvaluation(v, generation)) this.noteVoiceBinding(v, "", { bound: false, reason: "moved" }, null, e);
|
|
39030
|
+
}
|
|
39031
|
+
}
|
|
39032
|
+
/**
|
|
39033
|
+
* Fix I-1 / Task 20 R1: P's held position for every resolved V, read before a receive so frames can be stamped. V's
|
|
39034
|
+
* half is not stamped here: each frame carries the epoch that opened it (`openedAt`).
|
|
39035
|
+
*/
|
|
39036
|
+
async voicePositionSnapshot() {
|
|
39037
|
+
const out = /* @__PURE__ */ new Map();
|
|
39038
|
+
for (const [v, p] of this.voiceChannels.voiceChannels()) {
|
|
39039
|
+
try {
|
|
39040
|
+
out.set(v, voiceParentStamp(await this.heldPosition(p)));
|
|
39041
|
+
} catch {
|
|
39042
|
+
}
|
|
39043
|
+
}
|
|
39044
|
+
return out;
|
|
39045
|
+
}
|
|
39046
|
+
/**
|
|
39047
|
+
* Fix I-1, for Task 20: P for ONE frame on V, only when V is bound now AND neither P's nor V's held (group, epoch)
|
|
39048
|
+
* moved between that frame's first receipt and the verdict. Otherwise undefined: the frame is dropped (acked, never
|
|
39049
|
+
* held, never processed). A frame first received before V was resolved has no receipt positions and is refused.
|
|
39050
|
+
*/
|
|
39051
|
+
voiceParentForFrame(channelId, messageId, openedAt) {
|
|
39052
|
+
return this.voiceChannels.boundParentForFrame(channelId, messageId, openedAt);
|
|
39053
|
+
}
|
|
39054
|
+
/** Stores a verdict; a change of verdict logs one content-free line. */
|
|
39055
|
+
noteVoiceBinding(v, key, verdict, positions, error, admitted = {}) {
|
|
39056
|
+
const prev = this.voiceChannels.binding(v);
|
|
39057
|
+
this.voiceChannels.setBinding(v, key, verdict, positions, admitted);
|
|
39058
|
+
const state = verdict.bound ? "bound" : verdict.reason;
|
|
39059
|
+
const prevState = prev === void 0 ? void 0 : prev.bound ? "bound" : prev.reason;
|
|
39060
|
+
if (state === prevState && error === void 0) return;
|
|
39061
|
+
writeServiceLog2(verdict.bound ? "info" : "warn", "voice.channel_binding", {
|
|
39062
|
+
state: ServiceLogEnum.of(state, ["bound", "v_not_held", "p_not_held", "non_basic_leaf", "foreign_leaf", "two_non_parent", "two_relays", "relay_unknown", "moved", "parent_not_conversation"])
|
|
39063
|
+
}, error);
|
|
39064
|
+
}
|
|
39065
|
+
/**
|
|
39066
|
+
* The receive-cycle gate, before any per-frame work. A resolved V is never a normal channel: a frame passes only when
|
|
39067
|
+
* the per-frame verdict names P (bound now, its P stamp and its own opening epoch at the verdict's positions) AND the
|
|
39068
|
+
* accept filter takes it (Task 20: R's voice frame or a P member's greeting marker; a call_transcript is acked, not a
|
|
39069
|
+
* turn). Every other V frame is acked unprocessed and never recorded. A channel get_channel answered 404 is acked
|
|
39070
|
+
* unprocessed (fail closed). An unlisted channel whose lookup errored or has not run is held: neither processed nor
|
|
39071
|
+
* acked, so it is re-served once the lookup answers. Everything else passes unchanged.
|
|
39072
|
+
*/
|
|
39073
|
+
async gateVoiceEntries(entries) {
|
|
39074
|
+
const keep = [];
|
|
39075
|
+
const drop = [];
|
|
39076
|
+
for (const m of entries) {
|
|
39077
|
+
const gate = this.voiceChannelGate(m.channelId);
|
|
39078
|
+
if (gate === "hold") continue;
|
|
39079
|
+
if (gate === "normal") {
|
|
39080
|
+
this.voiceChannels.forgetReceipt(m.messageId);
|
|
39081
|
+
keep.push(m);
|
|
39082
|
+
continue;
|
|
39083
|
+
}
|
|
39084
|
+
if (this.voiceChannels.isVoice(m.channelId)) {
|
|
39085
|
+
const admitted = this.admitVoiceFrame(m);
|
|
39086
|
+
this.voiceChannels.forgetReceipt(m.messageId);
|
|
39087
|
+
if (admitted) {
|
|
39088
|
+
keep.push(m);
|
|
39089
|
+
continue;
|
|
39090
|
+
}
|
|
39091
|
+
} else {
|
|
39092
|
+
this.voiceChannels.forgetReceipt(m.messageId);
|
|
39093
|
+
}
|
|
39094
|
+
drop.push(m.queueId);
|
|
39095
|
+
}
|
|
39096
|
+
if (drop.length) {
|
|
39097
|
+
try {
|
|
39098
|
+
await this.provider.ackDelivery(this.cfg.deviceId, drop);
|
|
39099
|
+
} catch {
|
|
39100
|
+
}
|
|
39101
|
+
}
|
|
39102
|
+
return keep;
|
|
39103
|
+
}
|
|
39104
|
+
/**
|
|
39105
|
+
* Task 20: the per-frame verdict and the accept filter for one frame on a resolved V. True only for a model turn
|
|
39106
|
+
* (R's voice frame, a P member's greeting marker), which is then keyed per B.8 through {@link voiceFrameParent}. A
|
|
39107
|
+
* call_transcript is accepted but is not a turn (acked, nothing more, as step 1 on P). Counters are numbers only.
|
|
39108
|
+
*/
|
|
39109
|
+
admitVoiceFrame(m) {
|
|
39110
|
+
const parent3 = this.voiceParentForFrame(m.channelId, m.messageId, m.openedAt ?? null);
|
|
39111
|
+
const ctx = parent3 === void 0 ? void 0 : this.voiceChannels.frameContext(m.channelId, m.messageId, m.openedAt ?? null);
|
|
39112
|
+
if (ctx === void 0) {
|
|
39113
|
+
this.voiceFramesRefused++;
|
|
39114
|
+
return false;
|
|
39115
|
+
}
|
|
39116
|
+
const kind2 = classifyVoiceFrame({
|
|
39117
|
+
senderId: m.envelope.sender_id,
|
|
39118
|
+
ownIdentityId: this.cfg.identityId,
|
|
39119
|
+
relay: ctx.relay,
|
|
39120
|
+
parentMembers: ctx.parentMembers,
|
|
39121
|
+
plaintext: m.plaintext
|
|
39122
|
+
});
|
|
39123
|
+
if (kind2 === "call_transcript") return false;
|
|
39124
|
+
if (kind2 === "drop") {
|
|
39125
|
+
this.voiceFramesFiltered++;
|
|
39126
|
+
return false;
|
|
39127
|
+
}
|
|
39128
|
+
this.voiceFrameParent.set(m.messageId, ctx.parent);
|
|
39129
|
+
if (this.voiceFrameParent.size > VOICE_FRAME_PARENT_MAX) this.voiceFrameParent.delete(this.voiceFrameParent.keys().next().value);
|
|
39130
|
+
return true;
|
|
39131
|
+
}
|
|
39132
|
+
/**
|
|
39133
|
+
* Task 20 (B.8): the key channel for metadata, audience, owner, manifest and caps. For an admitted V frame, its P;
|
|
39134
|
+
* for any other frame, its own channel.
|
|
39135
|
+
*/
|
|
39136
|
+
keyChannelForFrame(m) {
|
|
39137
|
+
return this.voiceFrameParent.get(m.messageId) ?? m.channelId;
|
|
39138
|
+
}
|
|
39139
|
+
/**
|
|
39140
|
+
* Task 20 (B.8 "per-room daily caps → P", the room rate floor): the channel a cap or floor counts against. A known V
|
|
39141
|
+
* counts against its bound P; an unbound V against itself (no grant can be issued there, so nothing is widened).
|
|
39142
|
+
*/
|
|
39143
|
+
capChannelFor(channelId) {
|
|
39144
|
+
return this.voiceChannels.parentOf(channelId) ?? channelId;
|
|
39145
|
+
}
|
|
39146
|
+
/** Phase 3 F1 pause, for a voice channel V also read on its P (a paused agent runs no inference on the call either). */
|
|
39147
|
+
pausedFor(channelId) {
|
|
39148
|
+
return this.pausedChannels.has(channelId) || this.pausedChannels.has(this.capChannelFor(channelId));
|
|
39149
|
+
}
|
|
38592
39150
|
/** §5.4: one conversation's turns, facts, notes and recall, inside the memory mutex, with every in-memory copy. */
|
|
38593
39151
|
async purgeConversation(channelId) {
|
|
39152
|
+
await this.purgeOneConversation(channelId);
|
|
39153
|
+
for (const v of this.store.voiceChannelsOf(channelId)) await this.purgeOneConversation(v);
|
|
39154
|
+
}
|
|
39155
|
+
async purgeOneConversation(channelId) {
|
|
38594
39156
|
const scopes = this.unifiedOn ? this.store.ownerDmScopeIds().filter((s) => parseOwnerDmScopeId(s)?.channelId === channelId) : [];
|
|
38595
39157
|
await this.notes.purgeChannel(channelId);
|
|
38596
39158
|
for (const s of scopes) this.store.clearConversationAbsence(s);
|
|
@@ -39830,17 +40392,17 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39830
40392
|
async applyMemoryNote(raw, ctx) {
|
|
39831
40393
|
if (this.unifiedOn) return this.applyUnifiedNote(raw, ctx);
|
|
39832
40394
|
const ids = echoIds(raw);
|
|
39833
|
-
const
|
|
40395
|
+
const refuse2 = (reason, echo = ids) => {
|
|
39834
40396
|
writeServiceLog2("info", "memory.note_refused", {
|
|
39835
40397
|
reason_class: ServiceLogEnum.of(reason, ["mode", "invalid", "message", "audience", "provenance", "secret", "cap", "cleared", "store", "budget", "purged", "not_owner_words"])
|
|
39836
40398
|
});
|
|
39837
40399
|
return { type: "memory_note_result", ...echo, ok: false, reason };
|
|
39838
40400
|
};
|
|
39839
|
-
if (!this.notesProviderOn()) return
|
|
40401
|
+
if (!this.notesProviderOn()) return refuse2("mode");
|
|
39840
40402
|
const f = parseMemoryNoteFrame(raw);
|
|
39841
|
-
if (!f) return
|
|
39842
|
-
if (ctx.pendingRequests.has(f.request_id)) return
|
|
39843
|
-
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", {});
|
|
39844
40406
|
ctx.pendingRequests.add(f.request_id);
|
|
39845
40407
|
try {
|
|
39846
40408
|
const w2 = () => {
|
|
@@ -39852,13 +40414,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39852
40414
|
return cur.record;
|
|
39853
40415
|
};
|
|
39854
40416
|
const rec = w2();
|
|
39855
|
-
if (rec === void 0) return
|
|
40417
|
+
if (rec === void 0) return refuse2("message");
|
|
39856
40418
|
const channelId = rec.channel_id;
|
|
39857
|
-
if (rec.authorization?.memory_write !== true) return
|
|
39858
|
-
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");
|
|
39859
40421
|
const held = await this.heldPosition(channelId);
|
|
39860
40422
|
const now = this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false });
|
|
39861
|
-
if (now.memory_write !== true) return
|
|
40423
|
+
if (now.memory_write !== true) return refuse2("audience");
|
|
39862
40424
|
const spend = async () => {
|
|
39863
40425
|
const rw = await this.store.prepareRuntimeInboxRewrite(rec.message_id, bumpW9);
|
|
39864
40426
|
if (rw === "declined") return "budget";
|
|
@@ -39874,44 +40436,44 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39874
40436
|
};
|
|
39875
40437
|
if (f.op === "forget" && typeof f.id === "object") {
|
|
39876
40438
|
const shown = ctx.shownRecall(f.message_id, ctx.token);
|
|
39877
|
-
if (!shown || !shown.has(f.id.recall)) return
|
|
40439
|
+
if (!shown || !shown.has(f.id.recall)) return refuse2("invalid");
|
|
39878
40440
|
const rw = await spend();
|
|
39879
|
-
if (typeof rw === "string") return
|
|
39880
|
-
if (w2() !== rec) return
|
|
39881
|
-
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");
|
|
39882
40444
|
const r2 = await this.recall.forget(channelId, f.id.recall, rw, precommit(rw));
|
|
39883
|
-
if (r2 === "unknown_id") return
|
|
39884
|
-
if (r2 !== "ok") return
|
|
40445
|
+
if (r2 === "unknown_id") return refuse2("invalid");
|
|
40446
|
+
if (r2 !== "ok") return refuse2(r2);
|
|
39885
40447
|
writeServiceLog2("info", "memory.recall_forgotten");
|
|
39886
40448
|
return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true };
|
|
39887
40449
|
}
|
|
39888
40450
|
let mutation;
|
|
39889
40451
|
let rewrite;
|
|
39890
40452
|
if (f.op === "forget") {
|
|
39891
|
-
if (typeof f.id !== "number") return
|
|
40453
|
+
if (typeof f.id !== "number") return refuse2("invalid");
|
|
39892
40454
|
const rw = await spend();
|
|
39893
|
-
if (typeof rw === "string") return
|
|
40455
|
+
if (typeof rw === "string") return refuse2(rw);
|
|
39894
40456
|
rewrite = rw;
|
|
39895
40457
|
mutation = { op: "forget", id: f.id, rewrite: rw };
|
|
39896
40458
|
} else {
|
|
39897
40459
|
const p = prepareStoredNote(f.text, { text: rec.text, ...rec.transcribed === true ? { transcribed: true } : {} });
|
|
39898
|
-
if (!p.ok) return
|
|
40460
|
+
if (!p.ok) return refuse2(p.reason);
|
|
39899
40461
|
if (f.op === "replace") {
|
|
39900
|
-
if (typeof f.id !== "number") return
|
|
40462
|
+
if (typeof f.id !== "number") return refuse2("invalid");
|
|
39901
40463
|
const rw = await spend();
|
|
39902
|
-
if (typeof rw === "string") return
|
|
40464
|
+
if (typeof rw === "string") return refuse2(rw);
|
|
39903
40465
|
rewrite = rw;
|
|
39904
40466
|
mutation = { op: "replace", id: f.id, text: p.text, rewrite: rw };
|
|
39905
40467
|
} else {
|
|
39906
40468
|
mutation = { op: "add", text: p.text };
|
|
39907
40469
|
}
|
|
39908
40470
|
}
|
|
39909
|
-
if (w2() !== rec) return
|
|
39910
|
-
if (!this.notesProviderOn()) return
|
|
40471
|
+
if (w2() !== rec) return refuse2("message");
|
|
40472
|
+
if (!this.notesProviderOn()) return refuse2("mode");
|
|
39911
40473
|
const stamp = this.ingestGeneration.get(rec.message_id);
|
|
39912
|
-
if (stamp === void 0) return
|
|
40474
|
+
if (stamp === void 0) return refuse2("cleared");
|
|
39913
40475
|
const r = await this.notes.write(channelId, mutation, stamp, precommit(rewrite));
|
|
39914
|
-
if (!r.ok) return
|
|
40476
|
+
if (!r.ok) return refuse2(r.reason);
|
|
39915
40477
|
writeServiceLog2("info", "memory.note_written", {
|
|
39916
40478
|
op: ServiceLogEnum.of(f.op, ["add", "replace", "forget"]),
|
|
39917
40479
|
notes_count: r.notesCount,
|
|
@@ -39920,7 +40482,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39920
40482
|
return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true, id: r.id };
|
|
39921
40483
|
} catch (e) {
|
|
39922
40484
|
writeServiceLog2("warn", "memory.note_write_failed", {}, e);
|
|
39923
|
-
return
|
|
40485
|
+
return refuse2("store");
|
|
39924
40486
|
} finally {
|
|
39925
40487
|
ctx.pendingRequests.delete(f.request_id);
|
|
39926
40488
|
}
|
|
@@ -39934,17 +40496,17 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39934
40496
|
*/
|
|
39935
40497
|
async applyUnifiedNote(raw, ctx) {
|
|
39936
40498
|
const ids = echoIds(raw);
|
|
39937
|
-
const
|
|
40499
|
+
const refuse2 = (reason, echo = ids) => {
|
|
39938
40500
|
writeServiceLog2("info", "memory.note_refused", {
|
|
39939
40501
|
reason_class: ServiceLogEnum.of(reason, ["mode", "invalid", "message", "audience", "provenance", "secret", "cap", "cleared", "store", "budget", "purged", "not_owner_words"])
|
|
39940
40502
|
});
|
|
39941
40503
|
return { type: "memory_note_result", ...echo, ok: false, reason };
|
|
39942
40504
|
};
|
|
39943
|
-
if (!this.unifiedProviderOn()) return
|
|
40505
|
+
if (!this.unifiedProviderOn()) return refuse2("mode");
|
|
39944
40506
|
const f = parseMemoryNoteFrame(raw);
|
|
39945
|
-
if (!f) return
|
|
39946
|
-
if (ctx.pendingRequests.has(f.request_id)) return
|
|
39947
|
-
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", {});
|
|
39948
40510
|
ctx.pendingRequests.add(f.request_id);
|
|
39949
40511
|
try {
|
|
39950
40512
|
const w2 = () => {
|
|
@@ -39956,25 +40518,25 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39956
40518
|
return cur.record;
|
|
39957
40519
|
};
|
|
39958
40520
|
const rec = w2();
|
|
39959
|
-
if (rec === void 0) return
|
|
40521
|
+
if (rec === void 0) return refuse2("message");
|
|
39960
40522
|
const channelId = rec.channel_id;
|
|
39961
|
-
if (rec.authorization?.memory_write !== true) return
|
|
39962
|
-
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");
|
|
39963
40525
|
const held = await this.heldPosition(channelId);
|
|
39964
|
-
if (this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false }).memory_write !== true) return
|
|
40526
|
+
if (this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false }).memory_write !== true) return refuse2("audience");
|
|
39965
40527
|
const scopeId = this.ownerScopeNow(channelId);
|
|
39966
|
-
if (scopeId === null) return
|
|
40528
|
+
if (scopeId === null) return refuse2("audience");
|
|
39967
40529
|
const precommit = () => !this.unifiedProviderOn() ? "mode" : w2() !== rec ? "message" : null;
|
|
39968
|
-
if (f.op === "replace") return
|
|
39969
|
-
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);
|
|
39970
40532
|
const stripped = allowlistPin(f.text);
|
|
39971
|
-
if (stripped.trim().length === 0) return
|
|
40533
|
+
if (stripped.trim().length === 0) return refuse2("invalid");
|
|
39972
40534
|
const shown = ctx.shownUnified(f.message_id, ctx.token) ?? /* @__PURE__ */ new Set();
|
|
39973
|
-
if (!pinCoverage(stripped, rec.text, this.unifiedReads.coverageTexts(f.message_id, shown)).ok) return
|
|
39974
|
-
if (scanNote(stripped) !== null) return
|
|
39975
|
-
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");
|
|
39976
40538
|
const stamp = this.ingestGeneration.get(rec.message_id);
|
|
39977
|
-
if (stamp === void 0) return
|
|
40539
|
+
if (stamp === void 0) return refuse2("cleared");
|
|
39978
40540
|
const capture = this.store.itemCapture(channelId);
|
|
39979
40541
|
const r = await this.items.commit({
|
|
39980
40542
|
scopeKind: "owner_dm",
|
|
@@ -39992,7 +40554,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39992
40554
|
});
|
|
39993
40555
|
if (!r.ok) {
|
|
39994
40556
|
const map = { pin_cap: "cap", secret: "secret", cleared: "cleared", room_cleared: "cleared", disabled: "mode", mode: "mode", purged: "purged", message: "message", duplicate: "invalid", invalid: "invalid", empty: "invalid" };
|
|
39995
|
-
return
|
|
40557
|
+
return refuse2(map[r.reason] ?? "store");
|
|
39996
40558
|
}
|
|
39997
40559
|
await this.notes.exclusive(async () => this.unifiedReads.bumpGeneration());
|
|
39998
40560
|
void this.send(channelId, pinnedLine(stripped), void 0, { agentTurn: false }).catch((e) => writeServiceLog2("warn", "memory.pinned_line_failed", {}, e));
|
|
@@ -40000,7 +40562,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40000
40562
|
return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true, id: r.itemIds[0] };
|
|
40001
40563
|
} catch (e) {
|
|
40002
40564
|
writeServiceLog2("warn", "memory.note_write_failed", {}, e);
|
|
40003
|
-
return
|
|
40565
|
+
return refuse2("store");
|
|
40004
40566
|
} finally {
|
|
40005
40567
|
ctx.pendingRequests.delete(f.request_id);
|
|
40006
40568
|
}
|
|
@@ -40014,16 +40576,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40014
40576
|
* is invalidated in the same mutex hold, the item leaves the ranking hot set, then the bridge publishes `Forgot:`
|
|
40015
40577
|
* (OQ-P2: afterPublish keeps the line out of history and the items). Every reason is a key of UNIFIED_REFUSAL.
|
|
40016
40578
|
*/
|
|
40017
|
-
async forgetUnified(f, rec, channelId, ctx,
|
|
40018
|
-
if (typeof f.id !== "object") return
|
|
40579
|
+
async forgetUnified(f, rec, channelId, ctx, refuse2, precommit) {
|
|
40580
|
+
if (typeof f.id !== "object") return refuse2("invalid");
|
|
40019
40581
|
const itemId = f.id.recall;
|
|
40020
40582
|
const shown = ctx.shownUnified(f.message_id, ctx.token);
|
|
40021
|
-
if (!shown || !shown.has(itemId)) return
|
|
40583
|
+
if (!shown || !shown.has(itemId)) return refuse2("invalid");
|
|
40022
40584
|
const text = this.unifiedReads.shownText(f.message_id, itemId);
|
|
40023
|
-
if (text === void 0) return
|
|
40585
|
+
if (text === void 0) return refuse2("invalid");
|
|
40024
40586
|
const rw = await this.store.prepareRuntimeInboxRewrite(rec.message_id, bumpW9);
|
|
40025
|
-
if (rw === "declined") return
|
|
40026
|
-
if (rw === "no_row") return
|
|
40587
|
+
if (rw === "declined") return refuse2("budget");
|
|
40588
|
+
if (rw === "no_row") return refuse2("message");
|
|
40027
40589
|
const r = await this.notes.exclusive(async () => {
|
|
40028
40590
|
const veto = precommit();
|
|
40029
40591
|
if (veto !== null) return { ok: false, reason: veto };
|
|
@@ -40036,7 +40598,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40036
40598
|
}
|
|
40037
40599
|
return out;
|
|
40038
40600
|
});
|
|
40039
|
-
if (!r.ok) return
|
|
40601
|
+
if (!r.ok) return refuse2(r.reason === "unknown_id" ? "invalid" : r.reason);
|
|
40040
40602
|
this.rankingNote(() => this.ranking.removed(r.itemIds));
|
|
40041
40603
|
void this.send(channelId, forgotLine(text), void 0, { agentTurn: false }).catch((e) => writeServiceLog2("warn", "memory.forgot_line_failed", {}, e));
|
|
40042
40604
|
writeServiceLog2("info", "memory.item_forgotten");
|
|
@@ -40110,8 +40672,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40110
40672
|
this.fastReplyChains.set(channelId, next);
|
|
40111
40673
|
}
|
|
40112
40674
|
/** Count a fast reply against FAST_REPLY_MAX_JOBS and reserve its room send-floor token; returns the release. */
|
|
40113
|
-
reserveFastReply(
|
|
40675
|
+
reserveFastReply(replyChannel) {
|
|
40114
40676
|
this.fastReplyJobs++;
|
|
40677
|
+
const channelId = this.capChannelFor(replyChannel);
|
|
40115
40678
|
this.fastReplyReserved.set(channelId, (this.fastReplyReserved.get(channelId) ?? 0) + 1);
|
|
40116
40679
|
return () => {
|
|
40117
40680
|
this.fastReplyJobs--;
|
|
@@ -40335,6 +40898,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40335
40898
|
});
|
|
40336
40899
|
if (!res.ok) throw new Error(`list_channels failed: ${res.status}`);
|
|
40337
40900
|
const rows = await res.json();
|
|
40901
|
+
for (const r of rows) if (r.channel_kind === "voice") this.listedAsVoice.add(r.channel_id);
|
|
40338
40902
|
for (const r of rows) this.store.recordChannelEnclave(r.channel_id, r.enclave_id);
|
|
40339
40903
|
return rows.map((r) => ({
|
|
40340
40904
|
channel_id: r.channel_id,
|
|
@@ -40418,6 +40982,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40418
40982
|
*/
|
|
40419
40983
|
recordListedChannelMeta(channels, at) {
|
|
40420
40984
|
for (const c of channels) {
|
|
40985
|
+
if (this.listedAsVoice.has(c.channel_id)) continue;
|
|
40421
40986
|
this.channelMeta.set(c.channel_id, { is_dm: c.is_dm, name: c.name, at });
|
|
40422
40987
|
this.store.recordChannelIsDm(c.channel_id, c.is_dm);
|
|
40423
40988
|
}
|
|
@@ -40601,7 +41166,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40601
41166
|
}
|
|
40602
41167
|
const self = members.get(this.cfg.identityId);
|
|
40603
41168
|
if (self?.name) this.lastOwnDisplayName = self.name;
|
|
40604
|
-
|
|
41169
|
+
const unclassified = this.isUnlistedChannel(channelId) && !this.voiceChannels.isNotVoice(channelId);
|
|
41170
|
+
if (!this.voiceChannels.isVoice(channelId) && !unclassified) await this.resolveAndPinOwner(channelId, liveRows);
|
|
40605
41171
|
} catch {
|
|
40606
41172
|
}
|
|
40607
41173
|
}
|
|
@@ -40637,7 +41203,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40637
41203
|
* held group (never the backend's row label, M-2). NEVER throws (M-4): any
|
|
40638
41204
|
* exception is `failed verify_error`, and the row is acked like any other turn.
|
|
40639
41205
|
*/
|
|
40640
|
-
async verifyOwnerTurn(m) {
|
|
41206
|
+
async verifyOwnerTurn(m, keyChannel = m.channelId) {
|
|
40641
41207
|
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
40642
41208
|
if (!pin || m.envelope.sender_id !== pin.ownerIdentityId) return OWNER_TURN_UNCHECKED;
|
|
40643
41209
|
try {
|
|
@@ -40649,9 +41215,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40649
41215
|
}
|
|
40650
41216
|
if (v.sender_id !== pin.ownerIdentityId) return this.ownerTurnFailed(m, "malformed");
|
|
40651
41217
|
let leafClass = "ok";
|
|
40652
|
-
if (this.freshMeta(
|
|
41218
|
+
if (this.freshMeta(keyChannel)?.is_dm === true) {
|
|
40653
41219
|
const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
|
|
40654
|
-
if (signedDm !== void 0 && signedDm !==
|
|
41220
|
+
if (signedDm !== void 0 && signedDm !== keyChannel) return this.ownerTurnFailed(m, "wrong_dm_channel");
|
|
40655
41221
|
leafClass = classifyOwnerDmLeaves((await this.provider.leafTreeSummary(m.channelId)).leaves, {
|
|
40656
41222
|
ownerIdentityId: pin.ownerIdentityId,
|
|
40657
41223
|
agentIdentityId: this.cfg.identityId,
|
|
@@ -40815,9 +41381,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40815
41381
|
});
|
|
40816
41382
|
}
|
|
40817
41383
|
/** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
|
|
40818
|
-
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId, voiceScoped) {
|
|
41384
|
+
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId, voiceScoped, voiceParent) {
|
|
40819
41385
|
const held = await this.heldPosition(channelId);
|
|
40820
|
-
const
|
|
41386
|
+
const keyHeld = voiceParent === void 0 ? held : await this.heldPosition(voiceParent);
|
|
41387
|
+
const auth = this.authorizationFor(voiceParent ?? channelId, senderId, relaySpeakerId, keyHeld.epoch, keyHeld.groupId, {
|
|
41388
|
+
ownerVerdict,
|
|
41389
|
+
voiceScoped,
|
|
41390
|
+
...voiceParent === void 0 ? {} : { voice: { channelId, heldEpoch: held.epoch, heldGroupId: held.groupId } }
|
|
41391
|
+
});
|
|
40821
41392
|
if (!(auth.audience === "team_room" && auth.tool_grants.length > 0)) return auth;
|
|
40822
41393
|
if (this.roomGrantEpochs.size >= ROOM_GRANT_TRACK_MAX || held.epoch === null || held.groupId === null) {
|
|
40823
41394
|
writeServiceLog2("warn", "room_grant.untracked");
|
|
@@ -40870,7 +41441,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40870
41441
|
const meta = this.freshMeta(channelId);
|
|
40871
41442
|
const entry = this.freshRosterEntry(channelId);
|
|
40872
41443
|
const roster = entry?.members;
|
|
40873
|
-
const
|
|
41444
|
+
const vEntry = opts.voice === void 0 ? void 0 : this.freshRosterEntry(opts.voice.channelId);
|
|
41445
|
+
const vSender = vEntry?.members.get(senderId);
|
|
41446
|
+
const sender = opts.voice === void 0 ? roster?.get(senderId) : vSender === void 0 ? void 0 : roster?.get(senderId) ?? vSender;
|
|
41447
|
+
const senderIsRelay = opts.voice === void 0 ? sender?.is_relay === true : vSender?.is_relay === true;
|
|
40874
41448
|
let audience = "unknown";
|
|
40875
41449
|
if (meta && sender) {
|
|
40876
41450
|
if (sender.is_agent) audience = "agent_to_agent";
|
|
@@ -40882,15 +41456,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40882
41456
|
let tool_grants = NO_TOOL_GRANTS;
|
|
40883
41457
|
let relaySpeaker;
|
|
40884
41458
|
let memoryWrite = false;
|
|
40885
|
-
const epochFresh = rosterFreshAt(entry, heldEpoch, heldGroupId);
|
|
40886
|
-
const ownerDmFresh = epochFresh || this.ownerDmCycleFresh.has(channelId);
|
|
41459
|
+
const epochFresh = rosterFreshAt(entry, heldEpoch, heldGroupId) && (opts.voice === void 0 || rosterFreshAt(vEntry, opts.voice.heldEpoch, opts.voice.heldGroupId));
|
|
41460
|
+
const ownerDmFresh = epochFresh || opts.voice === void 0 && this.ownerDmCycleFresh.has(channelId);
|
|
40887
41461
|
if (ownerDmFresh && this.ownerDmDefaultGrants && meta?.is_dm === true && sender) {
|
|
40888
41462
|
const pinned = this.channelOwner.get(channelId);
|
|
40889
41463
|
const ownerId = pinned && pinned !== "anomalous" ? pinned.identityId : void 0;
|
|
40890
41464
|
if (ownerId !== void 0) {
|
|
40891
41465
|
const ownerDirect = audience === "owner_dm" && senderId === ownerId;
|
|
40892
41466
|
const owner = roster?.get(ownerId);
|
|
40893
|
-
const ownerRelayed = relaySpeakerId !== void 0 &&
|
|
41467
|
+
const ownerRelayed = relaySpeakerId !== void 0 && senderIsRelay && relaySpeakerId === ownerId && owner !== void 0 && !owner.is_agent && owner.is_agent_owner;
|
|
40894
41468
|
if (ownerDirect) tool_grants = opts.voiceScoped === true ? RELAYED_VOICE_GRANTS : OWNER_DM_DEFAULT_READ_GRANTS;
|
|
40895
41469
|
if (ownerRelayed) tool_grants = RELAYED_VOICE_GRANTS;
|
|
40896
41470
|
if (ownerRelayed) relaySpeaker = "owner";
|
|
@@ -41569,6 +42143,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41569
42143
|
async ingestCycleBody(receiptIds) {
|
|
41570
42144
|
if (this.closingDown) return;
|
|
41571
42145
|
const cycleStart = performance.now();
|
|
42146
|
+
let voiceListed = false;
|
|
42147
|
+
const cycleStartedAt = Date.now();
|
|
42148
|
+
const joinedInPhase0 = /* @__PURE__ */ new Set();
|
|
41572
42149
|
try {
|
|
41573
42150
|
const rows = await this.latency.time("recv.pull", () => this.backend.pullDelivery(this.cfg.deviceId));
|
|
41574
42151
|
this.recordRecvLag(rows);
|
|
@@ -41578,9 +42155,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41578
42155
|
}
|
|
41579
42156
|
for (const cid of channelIds) {
|
|
41580
42157
|
if (this.joinState.get(cid) !== "JOINED") {
|
|
42158
|
+
joinedInPhase0.add(cid);
|
|
41581
42159
|
await this.reconcileJoin(cid);
|
|
41582
42160
|
}
|
|
41583
42161
|
}
|
|
42162
|
+
if (await this.resolveUnlistedChannels(channelIds, false)) voiceListed = true;
|
|
41584
42163
|
for (const cid of channelIds) {
|
|
41585
42164
|
if (this.freshRoster(cid) === void 0) {
|
|
41586
42165
|
await this.refreshChannelRoster(cid);
|
|
@@ -41605,6 +42184,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41605
42184
|
}
|
|
41606
42185
|
} catch {
|
|
41607
42186
|
}
|
|
42187
|
+
const voiceStamp = await this.voicePositionSnapshot();
|
|
42188
|
+
const receiveStartedAt = Date.now();
|
|
42189
|
+
const stampsFor = (m) => {
|
|
42190
|
+
if (m.receivedAt >= receiveStartedAt) return true;
|
|
42191
|
+
const p = this.voiceChannels.parentOf(m.channelId);
|
|
42192
|
+
return m.receivedAt >= cycleStartedAt && p !== void 0 && !joinedInPhase0.has(p);
|
|
42193
|
+
};
|
|
41608
42194
|
let entries;
|
|
41609
42195
|
try {
|
|
41610
42196
|
entries = await this.latency.time("recv.decrypt", () => this.provider.receiveOnce(this.cfg.deviceId));
|
|
@@ -41614,7 +42200,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41614
42200
|
for (const cid of entries.supersededChannelIds ?? []) {
|
|
41615
42201
|
this.joinState.set(cid, "JOINED");
|
|
41616
42202
|
}
|
|
41617
|
-
if (!entries.length)
|
|
42203
|
+
if (!entries.length) {
|
|
42204
|
+
await this.reevaluateVoiceBindings();
|
|
42205
|
+
return;
|
|
42206
|
+
}
|
|
41618
42207
|
await this.maybePruneOwnerReplay();
|
|
41619
42208
|
const receivedAt = Date.now();
|
|
41620
42209
|
const purgeEpochAtReceipt = /* @__PURE__ */ new Map();
|
|
@@ -41627,16 +42216,31 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41627
42216
|
if (this.unifiedOn && !itemCaptureAtReceipt.has(m.channelId)) itemCaptureAtReceipt.set(m.channelId, this.store.itemCapture(m.channelId));
|
|
41628
42217
|
this.recallReceipt.set(m.messageId, { purgeEpoch: purgeEpochAtReceipt.get(m.channelId), modeEpoch: recallModeEpochAtReceipt });
|
|
41629
42218
|
receiptIds.push(m.messageId);
|
|
42219
|
+
const stamp = voiceStamp.get(m.channelId);
|
|
42220
|
+
if (stamp !== void 0) this.voiceChannels.noteReceipt(m.messageId, stampsFor(m) ? stamp : null);
|
|
42221
|
+
else if (this.voiceChannels.isVoice(m.channelId) || this.isUnlistedChannel(m.channelId) && !this.voiceChannels.isResolved(m.channelId)) {
|
|
42222
|
+
this.voiceChannels.noteReceipt(m.messageId, null);
|
|
42223
|
+
}
|
|
41630
42224
|
}
|
|
41631
|
-
|
|
41632
|
-
|
|
42225
|
+
if (await this.resolveUnlistedChannels(entries.map((m) => m.channelId), voiceListed)) voiceListed = true;
|
|
42226
|
+
await this.reevaluateVoiceBindings();
|
|
42227
|
+
entries = await this.gateVoiceEntries(entries);
|
|
42228
|
+
let metaRefreshed = voiceListed;
|
|
42229
|
+
if (!voiceListed && entries.some((m) => this.freshMeta(this.keyChannelForFrame(m)) === void 0)) {
|
|
41633
42230
|
await this.refreshChannelMeta();
|
|
41634
42231
|
metaRefreshed = true;
|
|
41635
42232
|
}
|
|
41636
|
-
await this.refreshRostersForCycle(entries.
|
|
42233
|
+
await this.refreshRostersForCycle(entries.flatMap((m) => {
|
|
42234
|
+
const key = this.keyChannelForFrame(m);
|
|
42235
|
+
return key === m.channelId ? [m.channelId] : [m.channelId, key];
|
|
42236
|
+
}));
|
|
41637
42237
|
const relayMissRefreshed = /* @__PURE__ */ new Set();
|
|
41638
42238
|
const acked = [];
|
|
41639
42239
|
for (const m of entries) {
|
|
42240
|
+
if (this.voiceChannels.isVoice(m.channelId) && !this.voiceFrameParent.has(m.messageId)) {
|
|
42241
|
+
acked.push(m.queueId);
|
|
42242
|
+
continue;
|
|
42243
|
+
}
|
|
41640
42244
|
try {
|
|
41641
42245
|
if (m.voiceMode === false) this.callBuffer.reset(m.channelId);
|
|
41642
42246
|
const encId = m.envelope.enclave_id;
|
|
@@ -41797,7 +42401,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41797
42401
|
}
|
|
41798
42402
|
if (typeof parsed.text === "string") {
|
|
41799
42403
|
if (parsed.text.trim() === VOICE_CALL_START_MARKER) this.callBuffer.reset(m.channelId);
|
|
41800
|
-
const
|
|
42404
|
+
const keyChannel = this.keyChannelForFrame(m);
|
|
42405
|
+
const voiceParent = keyChannel !== m.channelId ? keyChannel : void 0;
|
|
42406
|
+
const meta = this.freshMeta(keyChannel);
|
|
41801
42407
|
const senderType = m.envelope.sender_type;
|
|
41802
42408
|
const senderIsAgent = senderType === "agent" ? true : senderType === "human" ? false : void 0;
|
|
41803
42409
|
const claimsRelaySpeaker = parsed.relay === true && typeof parsed.speaker_identity_id === "string" && typeof parsed.speaker_display_name === "string";
|
|
@@ -41844,14 +42450,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41844
42450
|
senderIsRelay: this.freshRoster(m.channelId)?.get(m.envelope.sender_id)?.is_relay === true,
|
|
41845
42451
|
text: parsed.text
|
|
41846
42452
|
});
|
|
41847
|
-
const ownerVerdict = await this.verifyOwnerTurn(m);
|
|
42453
|
+
const ownerVerdict = await this.verifyOwnerTurn(m, keyChannel);
|
|
41848
42454
|
const turnAuthorization = await this.authorizeTurn(
|
|
41849
42455
|
m.channelId,
|
|
41850
42456
|
m.messageId,
|
|
41851
42457
|
m.envelope.sender_id,
|
|
41852
42458
|
ownerVerdict,
|
|
41853
42459
|
speaker?.identityId,
|
|
41854
|
-
voiceScoped
|
|
42460
|
+
voiceScoped,
|
|
42461
|
+
voiceParent
|
|
41855
42462
|
);
|
|
41856
42463
|
await this.prepareNotesForTurn(m.channelId, turnAuthorization);
|
|
41857
42464
|
const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
|
|
@@ -41878,11 +42485,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41878
42485
|
}
|
|
41879
42486
|
let fastPathRoomBlocked = false;
|
|
41880
42487
|
if (!coalescible && fastPathEligible && meta && meta.is_dm === false) {
|
|
41881
|
-
const
|
|
41882
|
-
|
|
42488
|
+
const floorKey = this.capChannelFor(m.channelId);
|
|
42489
|
+
const reserved = this.fastReplyReserved.get(floorKey) ?? 0;
|
|
42490
|
+
if (this.rateFloor.available(`room:${floorKey}`) - reserved < 2) fastPathRoomBlocked = true;
|
|
41883
42491
|
}
|
|
41884
42492
|
if (fastPathRoomBlocked) this.fastPathSkipped++;
|
|
41885
|
-
const fastPathFiring = fastPathEligible && !fastPathRoomBlocked && !this.
|
|
42493
|
+
const fastPathFiring = fastPathEligible && !fastPathRoomBlocked && !this.pausedFor(m.channelId);
|
|
41886
42494
|
const fastGreeting = isCallStart && fastPathFiring;
|
|
41887
42495
|
const runtimeVoice = (ackSent) => m.voiceMode ? {
|
|
41888
42496
|
voice: true,
|
|
@@ -41988,11 +42596,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41988
42596
|
void recordInboundMemory(this.memory, channelId, senderId, text, firstId, "voice", win.purgeEpoch);
|
|
41989
42597
|
let roomBlocked = false;
|
|
41990
42598
|
if (fastPathEligible && isRoom) {
|
|
41991
|
-
const
|
|
41992
|
-
|
|
42599
|
+
const floorKey = this.capChannelFor(channelId);
|
|
42600
|
+
const reserved = this.fastReplyReserved.get(floorKey) ?? 0;
|
|
42601
|
+
if (this.rateFloor.available(`room:${floorKey}`) - reserved < 2) roomBlocked = true;
|
|
41993
42602
|
}
|
|
41994
42603
|
if (roomBlocked) this.fastPathSkipped++;
|
|
41995
|
-
const plan = fastPathEligible && !roomBlocked && !this.
|
|
42604
|
+
const plan = fastPathEligible && !roomBlocked && !this.pausedFor(channelId) ? this.planFastPath(firstId, text, false) : void 0;
|
|
41996
42605
|
if (!plan || plan.kind === "dup" || plan.kind === "skip") {
|
|
41997
42606
|
if (plan?.kind === "skip" && plan.full) this.fastPathSkipped++;
|
|
41998
42607
|
await this.settleHeldRuntimeMessage(merged, true);
|
|
@@ -42096,6 +42705,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42096
42705
|
}
|
|
42097
42706
|
}
|
|
42098
42707
|
for (const m of entries) this.recallReceipt.delete(m.messageId);
|
|
42708
|
+
for (const m of entries) this.voiceFrameParent.delete(m.messageId);
|
|
42099
42709
|
if (acked.length) {
|
|
42100
42710
|
try {
|
|
42101
42711
|
await this.latency.time("recv.ack", () => this.provider.ackDelivery(this.cfg.deviceId, acked));
|
|
@@ -42376,8 +42986,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42376
42986
|
}
|
|
42377
42987
|
}
|
|
42378
42988
|
/** Spec §7.5 (T-14c, M-4): the runtime reports an executed-call cap for a room. Never throws. */
|
|
42379
|
-
noteRoomCapReached(
|
|
42380
|
-
if (typeof
|
|
42989
|
+
noteRoomCapReached(reported) {
|
|
42990
|
+
if (typeof reported !== "string" || this.closingDown) return;
|
|
42991
|
+
const channelId = this.capChannelFor(reported);
|
|
42381
42992
|
if (this.roomSearchDailyCap === 0) return;
|
|
42382
42993
|
if (this.freshMeta(channelId)?.is_dm !== false || this.channelRoster.get(channelId)?.crossOrg !== false) return;
|
|
42383
42994
|
const day = utcDayOf(Date.now());
|
|
@@ -42434,8 +43045,9 @@ ${channelId}`;
|
|
|
42434
43045
|
const a = entry.authorization;
|
|
42435
43046
|
if (!a || a.audience !== "team_room" || !a.tool_grants.some((g) => g.tool === "web_search")) return entry;
|
|
42436
43047
|
const day = utcDayOf(Date.now());
|
|
43048
|
+
const room = this.capChannelFor(entry.channel_id);
|
|
42437
43049
|
if (this.roomSearchDailyCap === 0) {
|
|
42438
|
-
this.logRoomCap(day,
|
|
43050
|
+
this.logRoomCap(day, room, "operator");
|
|
42439
43051
|
return this.withoutWebSearch(entry);
|
|
42440
43052
|
}
|
|
42441
43053
|
if (this.agentRoomSearchDailyCap === 0) {
|
|
@@ -42443,9 +43055,9 @@ ${channelId}`;
|
|
|
42443
43055
|
return this.withoutWebSearch(entry);
|
|
42444
43056
|
}
|
|
42445
43057
|
try {
|
|
42446
|
-
const n = this.store.countRoomSearchGrant(
|
|
42447
|
-
if (n === this.roomSearchDailyCap && this.store.markRoomSearchCapReached(
|
|
42448
|
-
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");
|
|
42449
43061
|
}
|
|
42450
43062
|
if (n > this.roomSearchDailyCap) return this.withoutWebSearch(entry);
|
|
42451
43063
|
const total = this.store.countRoomSearchGrant(AGENT_TOTAL_KEY, day);
|
|
@@ -42542,9 +43154,15 @@ ${channelId}`;
|
|
|
42542
43154
|
* PAUSE_RECHECK_MS window, so release does not depend on new traffic.
|
|
42543
43155
|
*/
|
|
42544
43156
|
heldForPause(channelId) {
|
|
42545
|
-
|
|
43157
|
+
let key = channelId;
|
|
43158
|
+
let at = this.pausedChannels.get(channelId);
|
|
43159
|
+
const parent3 = at === void 0 ? this.voiceChannels.parentOf(channelId) : void 0;
|
|
43160
|
+
if (parent3 !== void 0) {
|
|
43161
|
+
key = parent3;
|
|
43162
|
+
at = this.pausedChannels.get(parent3);
|
|
43163
|
+
}
|
|
42546
43164
|
if (at === void 0) return false;
|
|
42547
|
-
this.schedulePauseRecheck(
|
|
43165
|
+
this.schedulePauseRecheck(key, at);
|
|
42548
43166
|
return true;
|
|
42549
43167
|
}
|
|
42550
43168
|
schedulePauseRecheck(channelId, confirmedAt) {
|
|
@@ -42573,7 +43191,7 @@ ${channelId}`;
|
|
|
42573
43191
|
if (await this.probeOwnPause(channelId) === true) {
|
|
42574
43192
|
const now = Date.now();
|
|
42575
43193
|
this.pausedChannels.set(channelId, now);
|
|
42576
|
-
if (this.buffer.some((m) => m.channel_id === channelId)) this.schedulePauseRecheck(channelId, now);
|
|
43194
|
+
if (this.buffer.some((m) => m.channel_id === channelId || this.voiceChannels.parentOf(m.channel_id) === channelId)) this.schedulePauseRecheck(channelId, now);
|
|
42577
43195
|
return;
|
|
42578
43196
|
}
|
|
42579
43197
|
this.clearPause(channelId);
|
|
@@ -43052,6 +43670,10 @@ ${channelId}`;
|
|
|
43052
43670
|
custody_store_unavailable: this.provider.custodyHealth().storeUnavailable,
|
|
43053
43671
|
failed_attachments: this.failedAttachments,
|
|
43054
43672
|
refused_vault_refs: this.refusedVaultRefs,
|
|
43673
|
+
voice_frames_refused: this.voiceFramesRefused,
|
|
43674
|
+
voice_frames_filtered: this.voiceFramesFiltered,
|
|
43675
|
+
voice_receipt_cap_reached: this.voiceChannels.receiptCapReached(),
|
|
43676
|
+
voice_relay_in_parent: this.voiceChannels.relayInParentCount(),
|
|
43055
43677
|
fast_path_sent: this.fastPathSent,
|
|
43056
43678
|
fast_path_failed: this.fastPathFailed,
|
|
43057
43679
|
fast_path_skipped: this.fastPathSkipped,
|