@securecoms/agent-bridge 0.30.3 → 0.32.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 +59 -0
- package/dist/bin.js +352 -72
- package/dist/bridge-agent.d.ts +89 -15
- package/dist/index.js +352 -72
- package/dist/service/runtime-config-dir.d.ts +8 -2
- package/dist/version.d.ts +1 -1
- package/dist/voice/call-buffer.d.ts +118 -0
- package/dist/voice/persona.d.ts +13 -0
- package/dist/voice/voice-scope.d.ts +31 -0
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -7742,7 +7742,9 @@ function settingsToVoiceConfig(s, proxy) {
|
|
|
7742
7742
|
model: "securecoms-fast",
|
|
7743
7743
|
apiKey: proxy.apiKey,
|
|
7744
7744
|
timeoutMs: 2500,
|
|
7745
|
-
maxTokens: 80
|
|
7745
|
+
maxTokens: 80,
|
|
7746
|
+
// Spec 7.2: tells the proxy this bridge scopes voice turns. Sent to the SecureComs proxy only.
|
|
7747
|
+
headers: { "x-securecoms-bridge-caps": "voice-scope-v1" }
|
|
7746
7748
|
},
|
|
7747
7749
|
settings: s
|
|
7748
7750
|
};
|
|
@@ -7750,6 +7752,7 @@ function settingsToVoiceConfig(s, proxy) {
|
|
|
7750
7752
|
|
|
7751
7753
|
// src/voice/persona.ts
|
|
7752
7754
|
var CONTINUITY_CLAUSE = "The text messages in this chat and this voice call are one and the same ongoing conversation, and the earlier turns you can see ARE those messages. If asked whether you remember or received a message, answer from that history \u2014 never say you cannot access previous messages.";
|
|
7755
|
+
var VOICE_CALL_CLAUSE = "This is a live voice call. The earlier turns you can see are from this call only; during a call you cannot see the typed chat, saved notes or memory. If asked about a message, file or note you cannot see, say plainly that you can't see the chat while on a call and offer to pick it up in the chat afterwards. Never guess at what it said.";
|
|
7753
7756
|
var schema = z2.object({
|
|
7754
7757
|
persona: z2.object({
|
|
7755
7758
|
display_name: z2.string().min(1).optional(),
|
|
@@ -7762,6 +7765,8 @@ var schema = z2.object({
|
|
|
7762
7765
|
model: z2.string().min(1),
|
|
7763
7766
|
api_key: z2.string().min(1).optional(),
|
|
7764
7767
|
api_key_env: z2.string().min(1).optional(),
|
|
7768
|
+
// E.2: only the exact string "direct" opts in; any other value is ignored (today's proxy behavior), never an error.
|
|
7769
|
+
route: z2.unknown().optional(),
|
|
7765
7770
|
timeout_ms: z2.number().int().min(500).max(1e4).default(2500),
|
|
7766
7771
|
max_tokens: z2.number().int().min(16).max(400).default(80)
|
|
7767
7772
|
})
|
|
@@ -7786,7 +7791,8 @@ function parseVoiceConfig(raw, env) {
|
|
|
7786
7791
|
model: v.fast_path.model,
|
|
7787
7792
|
apiKey,
|
|
7788
7793
|
timeoutMs: v.fast_path.timeout_ms,
|
|
7789
|
-
maxTokens: v.fast_path.max_tokens
|
|
7794
|
+
maxTokens: v.fast_path.max_tokens,
|
|
7795
|
+
...v.fast_path.route === "direct" ? { route: "direct" } : {}
|
|
7790
7796
|
}
|
|
7791
7797
|
};
|
|
7792
7798
|
}
|
|
@@ -34056,7 +34062,8 @@ var FastLlmClient = class {
|
|
|
34056
34062
|
try {
|
|
34057
34063
|
res = await this.fetchImpl(`${this.cfg.baseUrl}${this.cfg.path ?? "/chat/completions"}`, {
|
|
34058
34064
|
method: "POST",
|
|
34059
|
-
|
|
34065
|
+
// Final fix T8: configured headers first, so they can never displace content-type or the bearer.
|
|
34066
|
+
headers: { ...this.cfg.headers, "content-type": "application/json", ...this.key ? { authorization: `Bearer ${this.key}` } : {} },
|
|
34060
34067
|
body: JSON.stringify({ model: this.cfg.model, messages, max_tokens: this.cfg.maxTokens, temperature: 0.4 }),
|
|
34061
34068
|
signal: ac.signal
|
|
34062
34069
|
});
|
|
@@ -34160,7 +34167,7 @@ function buildFastMessages(args2) {
|
|
|
34160
34167
|
const sp = systemPrompt(args2.persona, args2.settings, args2.agentName);
|
|
34161
34168
|
const head = sp.slice(0, sp.length - CONTINUITY_CLAUSE.length).trimEnd();
|
|
34162
34169
|
const notesLine = args2.notesDeferLine ? ` ${FAST_NOTES_DEFER_LINE}` : "";
|
|
34163
|
-
const system = `${head} ${args2.guidance.trim()}${notesLine} ${FAST_DEFER_RULE} ${
|
|
34170
|
+
const system = `${head} ${args2.guidance.trim()}${notesLine} ${FAST_DEFER_RULE} ${VOICE_CALL_CLAUSE}`;
|
|
34164
34171
|
const recent = args2.memory.recent.map((t) => ({
|
|
34165
34172
|
role: t.role === "agent" ? "assistant" : "user",
|
|
34166
34173
|
content: t.text
|
|
@@ -34197,6 +34204,160 @@ function normalizeFastReply(raw, maxChars = 200, maxSentences = 2) {
|
|
|
34197
34204
|
return sentences.slice(0, maxSentences).join(" ").slice(0, maxChars);
|
|
34198
34205
|
}
|
|
34199
34206
|
|
|
34207
|
+
// src/voice/voice-scope.ts
|
|
34208
|
+
function isVoiceScopedTurn(t) {
|
|
34209
|
+
return t.speaker !== void 0 || t.transcribed === true || t.senderIsRelay || t.text.trim() === VOICE_CALL_START_MARKER;
|
|
34210
|
+
}
|
|
34211
|
+
function admitsToCallBuffer(t) {
|
|
34212
|
+
return t.speaker !== void 0 || t.transcribed === true;
|
|
34213
|
+
}
|
|
34214
|
+
|
|
34215
|
+
// src/voice/call-buffer.ts
|
|
34216
|
+
var CALL_BUFFER_CAP = 50;
|
|
34217
|
+
var CALL_BUFFER_IDLE_MS = 30 * 6e4;
|
|
34218
|
+
var CALL_BUFFER_ID_CAP = 5e3;
|
|
34219
|
+
var CallBuffer = class {
|
|
34220
|
+
constructor(opts) {
|
|
34221
|
+
this.opts = opts;
|
|
34222
|
+
}
|
|
34223
|
+
channels = /* @__PURE__ */ new Map();
|
|
34224
|
+
/** messageId -> channelId, insertion ordered (oldest first) for the cap. */
|
|
34225
|
+
voiceIds = /* @__PURE__ */ new Map();
|
|
34226
|
+
/** messageId -> { channelId, the channel's bufSeq at the mark, the channel's generation then }. */
|
|
34227
|
+
seqNotes = /* @__PURE__ */ new Map();
|
|
34228
|
+
/**
|
|
34229
|
+
* Final fix I-1: messageId -> { channelId, the channel's bufSeq just before the turn's own peer entry }. A row the
|
|
34230
|
+
* backend redelivers (a failed ack, runtime_inbox.full backpressure) is ingested again; its id here means its text is
|
|
34231
|
+
* already in the call, so it is not appended again and its snapshot stops before its own entry (A-6).
|
|
34232
|
+
*/
|
|
34233
|
+
admitted = /* @__PURE__ */ new Map();
|
|
34234
|
+
/** The last bufSeq handed out per channel (kept across resets, so the sequence never runs backwards). */
|
|
34235
|
+
lastSeq = /* @__PURE__ */ new Map();
|
|
34236
|
+
nextSeq = 1;
|
|
34237
|
+
/** Per-channel reset generation (advanced by every reset, the idle rule included). */
|
|
34238
|
+
gens = /* @__PURE__ */ new Map();
|
|
34239
|
+
/**
|
|
34240
|
+
* Append a spoken turn's text, once per turn id (final fix I-1). Returns false (and appends nothing) when the id was
|
|
34241
|
+
* already admitted in this call, or when the buffer is disabled.
|
|
34242
|
+
*/
|
|
34243
|
+
appendPeer(channelId, text, messageId) {
|
|
34244
|
+
if (!this.opts.enabled) return false;
|
|
34245
|
+
if (this.admittedAt(messageId) !== void 0) return false;
|
|
34246
|
+
const before = this.seqAt(channelId);
|
|
34247
|
+
this.append(channelId, { role: "peer", text });
|
|
34248
|
+
this.admitted.set(messageId, { channelId, before });
|
|
34249
|
+
if (this.admitted.size > CALL_BUFFER_ID_CAP) this.admitted.delete(this.admitted.keys().next().value);
|
|
34250
|
+
return true;
|
|
34251
|
+
}
|
|
34252
|
+
/**
|
|
34253
|
+
* Final fix I-1: for a turn already admitted in this call, the channel's bufSeq just before its own entry (the end seq
|
|
34254
|
+
* of its first snapshot); undefined for a turn not admitted (or forgotten by a reset, the idle rule or the id cap).
|
|
34255
|
+
*/
|
|
34256
|
+
admittedAt(messageId) {
|
|
34257
|
+
const a = this.admitted.get(messageId);
|
|
34258
|
+
if (a === void 0) return void 0;
|
|
34259
|
+
this.expireIfIdle(a.channelId);
|
|
34260
|
+
return this.admitted.get(messageId)?.before;
|
|
34261
|
+
}
|
|
34262
|
+
appendAgent(channelId, text, origin) {
|
|
34263
|
+
this.append(channelId, { role: "agent", text, origin });
|
|
34264
|
+
}
|
|
34265
|
+
/**
|
|
34266
|
+
* Record a voice-scoped turn's id (for reply admission only, A-4) and note the channel's seq at this point. Final fix
|
|
34267
|
+
* I-1: a turn already admitted (a redelivered row) keeps the seq of its first admission, so the note never moves past
|
|
34268
|
+
* the turn's own entry.
|
|
34269
|
+
*/
|
|
34270
|
+
markVoiceScoped(channelId, messageId) {
|
|
34271
|
+
if (!this.opts.enabled) return;
|
|
34272
|
+
this.expireIfIdle(channelId);
|
|
34273
|
+
this.voiceIds.delete(messageId);
|
|
34274
|
+
this.voiceIds.set(messageId, channelId);
|
|
34275
|
+
if (this.voiceIds.size > CALL_BUFFER_ID_CAP) this.voiceIds.delete(this.voiceIds.keys().next().value);
|
|
34276
|
+
const seq = this.admittedAt(messageId) ?? this.seqAt(channelId);
|
|
34277
|
+
this.seqNotes.delete(messageId);
|
|
34278
|
+
this.seqNotes.set(messageId, { channelId, seq, generation: this.generation(channelId) });
|
|
34279
|
+
if (this.seqNotes.size > CALL_BUFFER_ID_CAP) this.seqNotes.delete(this.seqNotes.keys().next().value);
|
|
34280
|
+
}
|
|
34281
|
+
/** A-4: read ONLY by the bridge's `afterPublish` reply admission. */
|
|
34282
|
+
isVoiceScopedId(messageId) {
|
|
34283
|
+
const channelId = this.voiceIds.get(messageId);
|
|
34284
|
+
if (channelId === void 0) return false;
|
|
34285
|
+
this.expireIfIdle(channelId);
|
|
34286
|
+
return this.voiceIds.has(messageId);
|
|
34287
|
+
}
|
|
34288
|
+
/** The channel's mark noted when `messageId` was marked (the release re-read), or undefined (e.g. after a restart). */
|
|
34289
|
+
seqNoted(messageId) {
|
|
34290
|
+
const note = this.seqNotes.get(messageId);
|
|
34291
|
+
if (note === void 0) return void 0;
|
|
34292
|
+
this.expireIfIdle(note.channelId);
|
|
34293
|
+
const kept = this.seqNotes.get(messageId);
|
|
34294
|
+
return kept === void 0 ? void 0 : { seq: kept.seq, generation: kept.generation };
|
|
34295
|
+
}
|
|
34296
|
+
/** The bufSeq of the channel's newest entry (0 before any append). A snapshot's end seq. */
|
|
34297
|
+
seqAt(channelId) {
|
|
34298
|
+
this.expireIfIdle(channelId);
|
|
34299
|
+
return this.lastSeq.get(channelId) ?? 0;
|
|
34300
|
+
}
|
|
34301
|
+
/**
|
|
34302
|
+
* The channel's last `limit` entries, oldest first. With `upTo` (final fix I-1, a redelivered turn's re-snapshot),
|
|
34303
|
+
* only entries at or below that bufSeq.
|
|
34304
|
+
*/
|
|
34305
|
+
recent(channelId, limit, upTo) {
|
|
34306
|
+
if (limit <= 0) return [];
|
|
34307
|
+
this.expireIfIdle(channelId);
|
|
34308
|
+
const all = this.channels.get(channelId)?.entries ?? [];
|
|
34309
|
+
const entries = upTo === void 0 ? all : all.filter((e) => e.bufSeq <= upTo);
|
|
34310
|
+
return entries.slice(-limit).map(deliver);
|
|
34311
|
+
}
|
|
34312
|
+
/**
|
|
34313
|
+
* The channel's AGENT entries appended after `since.seq` (lines spoken or answered since a snapshot), oldest first.
|
|
34314
|
+
* Final fix M-4: nothing when the channel has been reset since the mark (a later call's lines).
|
|
34315
|
+
*/
|
|
34316
|
+
agentAfter(channelId, since) {
|
|
34317
|
+
if (this.generation(channelId) !== since.generation) return [];
|
|
34318
|
+
const entries = this.channels.get(channelId)?.entries ?? [];
|
|
34319
|
+
return entries.filter((e) => e.role === "agent" && e.bufSeq > since.seq).map(deliver);
|
|
34320
|
+
}
|
|
34321
|
+
/**
|
|
34322
|
+
* The channel's reset generation: it changes on every reset (explicit or idle) and on nothing else. A caller that
|
|
34323
|
+
* holds text across an await (the coalesce window) compares it to skip an append into a call that has since ended.
|
|
34324
|
+
*/
|
|
34325
|
+
generation(channelId) {
|
|
34326
|
+
this.expireIfIdle(channelId);
|
|
34327
|
+
return this.gens.get(channelId) ?? 0;
|
|
34328
|
+
}
|
|
34329
|
+
/** Clear the channel: its entries, its voice-scoped ids, its seq notes and its admitted ids. Only ever removes. */
|
|
34330
|
+
reset(channelId) {
|
|
34331
|
+
this.gens.set(channelId, (this.gens.get(channelId) ?? 0) + 1);
|
|
34332
|
+
this.channels.delete(channelId);
|
|
34333
|
+
for (const [id, c] of this.voiceIds) if (c === channelId) this.voiceIds.delete(id);
|
|
34334
|
+
for (const [id, n] of this.seqNotes) if (n.channelId === channelId) this.seqNotes.delete(id);
|
|
34335
|
+
for (const [id, a] of this.admitted) if (a.channelId === channelId) this.admitted.delete(id);
|
|
34336
|
+
}
|
|
34337
|
+
append(channelId, e) {
|
|
34338
|
+
if (!this.opts.enabled) return;
|
|
34339
|
+
this.expireIfIdle(channelId);
|
|
34340
|
+
let ch = this.channels.get(channelId);
|
|
34341
|
+
if (ch === void 0) {
|
|
34342
|
+
ch = { entries: [], lastAppend: 0 };
|
|
34343
|
+
this.channels.set(channelId, ch);
|
|
34344
|
+
}
|
|
34345
|
+
const bufSeq = this.nextSeq++;
|
|
34346
|
+
this.lastSeq.set(channelId, bufSeq);
|
|
34347
|
+
ch.entries.push({ ...e, bufSeq });
|
|
34348
|
+
if (ch.entries.length > this.opts.cap) ch.entries.splice(0, ch.entries.length - this.opts.cap);
|
|
34349
|
+
ch.lastAppend = this.opts.now();
|
|
34350
|
+
}
|
|
34351
|
+
/** Reset (c): more than `idleMs` since the channel's last append clears it, checked on every read and append. */
|
|
34352
|
+
expireIfIdle(channelId) {
|
|
34353
|
+
const ch = this.channels.get(channelId);
|
|
34354
|
+
if (ch !== void 0 && this.opts.now() - ch.lastAppend > this.opts.idleMs) this.reset(channelId);
|
|
34355
|
+
}
|
|
34356
|
+
};
|
|
34357
|
+
function deliver(e) {
|
|
34358
|
+
return { role: e.role, text: e.text, seq: 0, ...e.origin ? { origin: e.origin } : {} };
|
|
34359
|
+
}
|
|
34360
|
+
|
|
34200
34361
|
// src/vault.ts
|
|
34201
34362
|
var VaultIntegrityError = class extends Error {
|
|
34202
34363
|
constructor(message, cause) {
|
|
@@ -34504,6 +34665,7 @@ function lineHash(line) {
|
|
|
34504
34665
|
return createHash5("sha256").update(line.trim(), "utf8").digest("hex");
|
|
34505
34666
|
}
|
|
34506
34667
|
var LOCAL_REPORT_FILE = "local-config.json";
|
|
34668
|
+
var LOCAL_PERSONA_FILE_NAMES = ["IDENTITY.md", "SOUL.md", "AGENTS.md"];
|
|
34507
34669
|
var SECRET_PATH_COUNT_MAX = 1e4;
|
|
34508
34670
|
var isCount = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0 && v <= SECRET_PATH_COUNT_MAX;
|
|
34509
34671
|
function parseSecretPathsReport(raw) {
|
|
@@ -34585,18 +34747,23 @@ function readLocalReport(home, identityId) {
|
|
|
34585
34747
|
const hasSearchKey = doc.has_search_key === true;
|
|
34586
34748
|
const hasLlmKey = doc.has_llm_key === true;
|
|
34587
34749
|
const raw = doc.persona;
|
|
34588
|
-
const persona = raw && typeof raw === "object" && !Array.isArray(raw) ?
|
|
34589
|
-
|
|
34590
|
-
)
|
|
34750
|
+
const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
34751
|
+
const personaFiles = [];
|
|
34752
|
+
for (const name of LOCAL_PERSONA_FILE_NAMES) {
|
|
34753
|
+
const text = persona[name];
|
|
34754
|
+
if (typeof text === "string" && text.trim() !== "") {
|
|
34755
|
+
personaFiles.push({ name, bytes: Buffer.byteLength(text, "utf8") });
|
|
34756
|
+
}
|
|
34757
|
+
}
|
|
34591
34758
|
const capRaw = doc.owner_dm_search_cap_effective;
|
|
34592
34759
|
const ownerDmCap = typeof capRaw === "number" && Number.isInteger(capRaw) && capRaw >= 0 && capRaw <= OWNER_DM_SEARCH_DAILY_CAP_MAX ? capRaw : void 0;
|
|
34593
34760
|
return {
|
|
34594
|
-
|
|
34761
|
+
local_persona_files: personaFiles,
|
|
34595
34762
|
has_search_key: hasSearchKey,
|
|
34596
34763
|
has_llm_key: hasLlmKey,
|
|
34597
34764
|
has_extract_provider: doc.has_extract_provider === true,
|
|
34598
34765
|
...ownerDmCap !== void 0 ? { owner_dm_search_cap_effective: ownerDmCap } : {},
|
|
34599
|
-
legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source),
|
|
34766
|
+
legacy_memory: localLegacySources(doc).map(({ path: _path, content: _content, ...source }) => source),
|
|
34600
34767
|
secret_paths: parseSecretPathsReport(doc.secret_paths)
|
|
34601
34768
|
};
|
|
34602
34769
|
}
|
|
@@ -34681,16 +34848,6 @@ async function applyLegacyMemoryFactDecisions(home, identityId, rawDecisions, re
|
|
|
34681
34848
|
}
|
|
34682
34849
|
return { sourcesDeleted, factsAssigned };
|
|
34683
34850
|
}
|
|
34684
|
-
function readManagedMemory(home, identityId) {
|
|
34685
|
-
const p = join18(runtimeConfigDir(home, identityId), MEMORY_FILE);
|
|
34686
|
-
if (!existsSync4(p)) return null;
|
|
34687
|
-
try {
|
|
34688
|
-
const body = readFileSync10(p, "utf8").trim();
|
|
34689
|
-
return body || null;
|
|
34690
|
-
} catch {
|
|
34691
|
-
return null;
|
|
34692
|
-
}
|
|
34693
|
-
}
|
|
34694
34851
|
function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
34695
34852
|
const dir = runtimeConfigDir(home, identityId);
|
|
34696
34853
|
const path = join18(dir, MEMORY_FILE);
|
|
@@ -35397,6 +35554,10 @@ var OWNER_DM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
|
35397
35554
|
var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
35398
35555
|
Object.freeze({ tool: "current_time", mode: "read" })
|
|
35399
35556
|
]);
|
|
35557
|
+
var RELAYED_VOICE_GRANTS = Object.freeze([
|
|
35558
|
+
Object.freeze({ tool: "current_time", mode: "read" }),
|
|
35559
|
+
Object.freeze({ tool: "web_search", mode: "read" })
|
|
35560
|
+
]);
|
|
35400
35561
|
var NO_TOOL_GRANTS = Object.freeze([]);
|
|
35401
35562
|
var WS_CLOSE_SESSION_SUPERSEDED2 = 4409;
|
|
35402
35563
|
var OWNER_REPLAY_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
@@ -35756,6 +35917,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35756
35917
|
workspaceDir: this.cfg.memoryWorkspaceDir,
|
|
35757
35918
|
factsCharBudget: this.cfg.memoryFactsCharBudget
|
|
35758
35919
|
});
|
|
35920
|
+
this.callBuffer = new CallBuffer({
|
|
35921
|
+
cap: CALL_BUFFER_CAP,
|
|
35922
|
+
idleMs: CALL_BUFFER_IDLE_MS,
|
|
35923
|
+
now: () => Date.now(),
|
|
35924
|
+
enabled: (this.cfg.memoryProvider ?? "securecoms") !== "none"
|
|
35925
|
+
});
|
|
35759
35926
|
this.provider = new MlsGroupKeyProvider({
|
|
35760
35927
|
store: this.store,
|
|
35761
35928
|
backend: this.backend,
|
|
@@ -35889,6 +36056,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35889
36056
|
this.injectedFastLlm = !!deps.fastLlm;
|
|
35890
36057
|
this.fileVoiceConfig = cfg.voice;
|
|
35891
36058
|
this.fastLlm = deps.fastLlm ?? (cfg.voice?.fastPath.enabled ? new FastLlmClient(cfg.voice.fastPath, this.fetchImpl) : void 0);
|
|
36059
|
+
if (this.fileVoiceConfig?.fastPath.route === "direct" && !this.fileVoiceConfig.fastPath.apiKey) {
|
|
36060
|
+
writeServiceLog("warn", "voice.fast_path_direct_no_key");
|
|
36061
|
+
}
|
|
35892
36062
|
this.voiceSettingsSource = cfg.voice ? "file" : "none";
|
|
35893
36063
|
this.latency = deps.latency ?? new LatencyRecorder();
|
|
35894
36064
|
this.voiceCoalesceWindowMs = Math.max(0, deps.voiceCoalesceWindowMs ?? voiceCoalesceWindowFromEnv());
|
|
@@ -36579,6 +36749,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36579
36749
|
startedAt = 0;
|
|
36580
36750
|
store;
|
|
36581
36751
|
memory;
|
|
36752
|
+
/**
|
|
36753
|
+
* Voice fast-path privacy, Task 4b (spec §2.1, OQ-1 ruled): the current calls' spoken turns, in process only. The
|
|
36754
|
+
* ONLY history a voice-scoped turn sees; read through voiceScopedHistory, once per turn, at ingest.
|
|
36755
|
+
*/
|
|
36756
|
+
callBuffer;
|
|
36582
36757
|
/**
|
|
36583
36758
|
* Markdown memory 7a: the one owner of every note mutation and of the
|
|
36584
36759
|
* delivery cache (spec §6.3, §7.2). One per store, so one per identity.
|
|
@@ -36683,6 +36858,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36683
36858
|
const s = parseVoiceSettings(await res.json());
|
|
36684
36859
|
if (s.identityDisplayName) this.lastOwnDisplayName = s.identityDisplayName;
|
|
36685
36860
|
const proxy = settingsToVoiceConfig(s, { baseUrl: this.cfg.apiBase, apiKey: this.cfg.apiKey });
|
|
36861
|
+
const direct = this.fileVoiceConfig?.fastPath.route === "direct" ? this.fileVoiceConfig.fastPath : void 0;
|
|
36686
36862
|
if (s.version === 0 && this.fileVoiceConfig) {
|
|
36687
36863
|
const merged = {
|
|
36688
36864
|
...s,
|
|
@@ -36691,16 +36867,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36691
36867
|
quirks: this.fileVoiceConfig.persona.quirks
|
|
36692
36868
|
};
|
|
36693
36869
|
this.voiceSettings = merged;
|
|
36694
|
-
this.cfg.voice = { persona: this.fileVoiceConfig.persona, fastPath: proxy.fastPath, settings: merged };
|
|
36870
|
+
this.cfg.voice = { persona: this.fileVoiceConfig.persona, fastPath: direct ?? proxy.fastPath, settings: merged };
|
|
36695
36871
|
this.voiceSettingsVersion = 0;
|
|
36696
36872
|
this.voiceSettingsSource = "file";
|
|
36697
36873
|
} else {
|
|
36698
36874
|
this.voiceSettings = s;
|
|
36699
|
-
this.cfg.voice = proxy;
|
|
36875
|
+
this.cfg.voice = direct ? { ...proxy, fastPath: direct } : proxy;
|
|
36700
36876
|
this.voiceSettingsVersion = s.version;
|
|
36701
36877
|
this.voiceSettingsSource = "backend";
|
|
36702
36878
|
}
|
|
36703
|
-
if (!this.injectedFastLlm) {
|
|
36879
|
+
if (!this.injectedFastLlm && !direct) {
|
|
36704
36880
|
this.fastLlm = new FastLlmClient(this.cfg.voice.fastPath, this.fetchImpl);
|
|
36705
36881
|
}
|
|
36706
36882
|
writeServiceLog("info", "voice.settings_loaded", {
|
|
@@ -36929,12 +37105,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36929
37105
|
const report = {
|
|
36930
37106
|
applied_version: version,
|
|
36931
37107
|
paths: checkConfiguredPaths(skillsJson),
|
|
36932
|
-
//
|
|
36933
|
-
//
|
|
36934
|
-
|
|
36935
|
-
|
|
36936
|
-
// interpret any of it.
|
|
36937
|
-
local_persona: local?.persona ?? null,
|
|
37108
|
+
// F-0 (G-5): the report carries no content. No MEMORY.md text, no persona text, no legacy
|
|
37109
|
+
// text. E13 needs only to know which of the runtime's own persona files hold something, so
|
|
37110
|
+
// this is [{name, bytes}] for each non-blank file and nothing else.
|
|
37111
|
+
local_persona_files: local?.local_persona_files ?? [],
|
|
36938
37112
|
// Only the runtime knows whether a search key file is configured — it
|
|
36939
37113
|
// is an environment value on that process, invisible to the daemon and
|
|
36940
37114
|
// to the backend. The backend needs it to generate a truthful tool
|
|
@@ -36967,9 +37141,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36967
37141
|
...this.reportedClearGeneration(),
|
|
36968
37142
|
// 7b spec §9.2 (Task 19): capability versus state. Read-only: the report never starts the embedder.
|
|
36969
37143
|
...this.reportedRecall(),
|
|
36970
|
-
// Inert legacy files only. `readLocalReport` revalidates the current
|
|
36971
|
-
// regular-file bytes and strips every local path before this
|
|
36972
|
-
// machine.
|
|
37144
|
+
// Inert legacy files only, metadata only. `readLocalReport` revalidates the current
|
|
37145
|
+
// regular-file bytes and strips every local path and every `content` before this
|
|
37146
|
+
// leaves the machine.
|
|
36973
37147
|
legacy_memory: local?.legacy_memory ?? [],
|
|
36974
37148
|
// §B.4: counts of server-named secret paths inside / allowlisted /
|
|
36975
37149
|
// outside the secrets directory, and the runtime's mode. Numbers and
|
|
@@ -38226,6 +38400,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38226
38400
|
const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
|
|
38227
38401
|
const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(body.text);
|
|
38228
38402
|
if (opts.inReplyTo !== void 0 && this.isOwnerVerifyFailed(opts.inReplyTo)) return;
|
|
38403
|
+
if (body.kind === "text" && opts.callBufferReply === true && opts.inReplyTo !== void 0 && !this.spokenReplyIds.has(id) && !isBridgeMemoryLine(body.text) && this.callBuffer.isVoiceScopedId(opts.inReplyTo)) {
|
|
38404
|
+
this.callBuffer.appendAgent(channelId, body.text, "reply");
|
|
38405
|
+
}
|
|
38229
38406
|
const origin = this.spokenReplyIds.has(id) ? "fast_path" : !this.taintScope(channelId) ? "reply" : opts.inReplyTo !== void 0 && this.cleanTurns.has(opts.inReplyTo) && !this.taintedTurns.has(opts.inReplyTo) ? "reply" : "untrusted";
|
|
38230
38407
|
if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, body.text, id, modality, purgeEpoch, origin);
|
|
38231
38408
|
if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, body.text, id, replyCapture ?? null, origin);
|
|
@@ -38996,6 +39173,27 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38996
39173
|
mayReadHistoryForTurn(messageId, authorization) {
|
|
38997
39174
|
return !this.isOwnerVerifyFailed(messageId) && this.mayReadOpenClawMemoryForTurn(authorization);
|
|
38998
39175
|
}
|
|
39176
|
+
/**
|
|
39177
|
+
* Voice fast-path privacy (spec §2.1, OQ-1 ruled): the ONLY history source of a voice-scoped turn, the call buffer.
|
|
39178
|
+
* Never reads the memory provider, the store, the unified store or the 7b index. A-6: called once per turn, at ingest
|
|
39179
|
+
* (takeVoiceSnapshot), after the reset signals and before the turn's own append.
|
|
39180
|
+
*/
|
|
39181
|
+
voiceScopedHistory(channelId, limit, upTo) {
|
|
39182
|
+
return this.callBuffer.recent(channelId, limit, upTo);
|
|
39183
|
+
}
|
|
39184
|
+
/**
|
|
39185
|
+
* Task 4b (A-6): the turn's snapshot, taken BEFORE its own text is appended (audit finding 2). Final fix I-1: a turn
|
|
39186
|
+
* already admitted to this call (a redelivered row) is snapshotted up to just before its own entry, as it was the
|
|
39187
|
+
* first time, so its snapshot never contains itself.
|
|
39188
|
+
*/
|
|
39189
|
+
takeVoiceSnapshot(channelId, messageId) {
|
|
39190
|
+
const upTo = this.callBuffer.admittedAt(messageId);
|
|
39191
|
+
return {
|
|
39192
|
+
recent: this.voiceScopedHistory(channelId, _BridgeAgent.TURN_MEMORY_LIMIT, upTo),
|
|
39193
|
+
endSeq: upTo ?? this.callBuffer.seqAt(channelId),
|
|
39194
|
+
generation: this.callBuffer.generation(channelId)
|
|
39195
|
+
};
|
|
39196
|
+
}
|
|
38999
39197
|
/** An inbound turn may receive OpenClaw memory only from an active owner DM. */
|
|
39000
39198
|
mayReadOpenClawMemoryForTurn(authorization) {
|
|
39001
39199
|
return this.cfg.memoryProvider !== "openclaw" || authorization.sender_membership === "active" && authorization.audience === "owner_dm";
|
|
@@ -39005,16 +39203,26 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39005
39203
|
* stays owner-DM-only; every provider failure degrades to empty context and
|
|
39006
39204
|
* never falls back to the runtime's retired plaintext JSON files.
|
|
39007
39205
|
*/
|
|
39008
|
-
async memoryForTurn(channelId, messageId, authorization, limit = 50) {
|
|
39206
|
+
async memoryForTurn(channelId, messageId, authorization, limit = 50, voice) {
|
|
39009
39207
|
if (!this.mayReadHistoryForTurn(messageId, authorization)) {
|
|
39010
39208
|
return { recent: [], facts: [] };
|
|
39011
39209
|
}
|
|
39210
|
+
if (voice) return { recent: this.voiceFrameTurns(voice.snapshot?.recent ?? []), facts: [] };
|
|
39012
39211
|
try {
|
|
39013
39212
|
return this.fenceMemory(channelId, await this.memory.context(channelId, limit), holdsManifestGrant(authorization));
|
|
39014
39213
|
} catch {
|
|
39015
39214
|
return { recent: [], facts: [] };
|
|
39016
39215
|
}
|
|
39017
39216
|
}
|
|
39217
|
+
/**
|
|
39218
|
+
* Task 6 (spec §2.2 A-1, N1): call-buffer entries as runtime-frame turns. Under the voice fence (a pin is loaded) the
|
|
39219
|
+
* fast model's own lines (origin "fast_path") are dropped, as fenceMemory drops them from store history; the fast
|
|
39220
|
+
* prompt keeps them. The internal origin is not carried into the frame; `seq` stays 0 (A-5).
|
|
39221
|
+
*/
|
|
39222
|
+
voiceFrameTurns(entries) {
|
|
39223
|
+
const fenced = this.voiceFenceOn();
|
|
39224
|
+
return entries.filter((e) => !(fenced && e.origin === "fast_path")).map((e) => ({ seq: e.seq, role: e.role, text: e.text }));
|
|
39225
|
+
}
|
|
39018
39226
|
/**
|
|
39019
39227
|
* Owner grants plan 4 Task 16 (T-30, audit r2 H-1): the voice fence is on whenever a pin is loaded, on EVERY runtime
|
|
39020
39228
|
* turn (manifest, fallback, greeting, relayed, constants-only alike). It never depends on a turn's grants.
|
|
@@ -39049,7 +39257,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39049
39257
|
if (!strict && !ctx.recent.some((t) => t.role === "agent")) return ctx;
|
|
39050
39258
|
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
39051
39259
|
const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
|
|
39052
|
-
const meta = securecoms ? this.store.memoryLineMeta(channelId, ctx.recent.filter((t) => strict || t.role === "agent").map((t) => t.seq)) : /* @__PURE__ */ new Map();
|
|
39260
|
+
const meta = securecoms ? this.store.memoryLineMeta(channelId, ctx.recent.filter((t) => (strict || t.role === "agent") && t.seq !== 0).map((t) => t.seq)) : /* @__PURE__ */ new Map();
|
|
39053
39261
|
const fence = this.fenceContext(channelId);
|
|
39054
39262
|
const recent = ctx.recent.filter((t) => {
|
|
39055
39263
|
const m = meta.get(t.seq);
|
|
@@ -39068,7 +39276,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39068
39276
|
if (recent.length === 0) return false;
|
|
39069
39277
|
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
39070
39278
|
const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
|
|
39071
|
-
const meta = securecoms ? this.store.memoryLineMeta(m.channel_id, recent.filter((t) => t.role !== "agent").map((t) => t.seq)) : /* @__PURE__ */ new Map();
|
|
39279
|
+
const meta = securecoms ? this.store.memoryLineMeta(m.channel_id, recent.filter((t) => t.role !== "agent" && t.seq !== 0).map((t) => t.seq)) : /* @__PURE__ */ new Map();
|
|
39072
39280
|
return recent.some((t) => t.role !== "agent" && isRelayClassPeer({ peerId: t.peerId, modality: meta.get(t.seq)?.modality }, ownerId));
|
|
39073
39281
|
}
|
|
39074
39282
|
/**
|
|
@@ -39364,9 +39572,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39364
39572
|
writeServiceLog("warn", "memory.recall_prepare_failed", {}, e);
|
|
39365
39573
|
}
|
|
39366
39574
|
}
|
|
39575
|
+
/**
|
|
39576
|
+
* Voice fast-path privacy, Task 7 (spec §4): the frame-keyed delivery check. A voice-scoped frame gets no notes, no
|
|
39577
|
+
* recall and no unified items, whatever its authorization says. Keys on the frame's own marks, NEVER on the call
|
|
39578
|
+
* buffer's id set (a reset evicts ids; the staged frame must stay scoped).
|
|
39579
|
+
*/
|
|
39580
|
+
frameIsVoiceScoped(m) {
|
|
39581
|
+
return m.voice_scoped === true || m.transcribed === true || m.speaker !== void 0;
|
|
39582
|
+
}
|
|
39367
39583
|
/** ws flush (synchronous). Q1 to Q3 and Q6 again at delivery, on the ORIGINAL message; Q4 lives in withConsumerRecall. */
|
|
39368
39584
|
recallForDelivery(m) {
|
|
39369
39585
|
if (this.unifiedOn) return void 0;
|
|
39586
|
+
if (this.frameIsVoiceScoped(m)) return void 0;
|
|
39370
39587
|
if (!this.recallProviderOn() || !this.notesReadable(m.authorization)) return void 0;
|
|
39371
39588
|
return this.recall.deliverable(m.message_id, m.channel_id);
|
|
39372
39589
|
}
|
|
@@ -39599,6 +39816,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39599
39816
|
/** ws flush (synchronous). The predicate and the owner scope again at delivery, on the ORIGINAL message (H-4, Review Focus 3). */
|
|
39600
39817
|
unifiedForDelivery(m) {
|
|
39601
39818
|
if (!this.unifiedProviderOn()) return void 0;
|
|
39819
|
+
if (this.frameIsVoiceScoped(m)) return void 0;
|
|
39602
39820
|
const a = m.authorization;
|
|
39603
39821
|
if (!(a?.memory_write === true || a?.relay_speaker === "owner")) return void 0;
|
|
39604
39822
|
const scopeId = this.ownerScopeNow(m.channel_id);
|
|
@@ -39834,6 +40052,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39834
40052
|
notesForDelivery(m) {
|
|
39835
40053
|
if (this.unifiedOn) return { generation: this.notes.appliedGeneration() };
|
|
39836
40054
|
const generation = this.notes.appliedGeneration();
|
|
40055
|
+
if (this.frameIsVoiceScoped(m)) return { generation };
|
|
39837
40056
|
if (!this.notesReadable(m.authorization)) return { generation };
|
|
39838
40057
|
const d = this.notes.deliverable(m.channel_id);
|
|
39839
40058
|
if (d.miss) {
|
|
@@ -40193,7 +40412,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40193
40412
|
* deferring fast reply was actually published — F3).
|
|
40194
40413
|
*/
|
|
40195
40414
|
async speakAndSettle(turn, plan, held, heldAfterAck, fastText, guidance, fastSpeaker, greeting) {
|
|
40196
|
-
const outcome = plan.kind === "ack" ? await this.sendFastLine(turn
|
|
40415
|
+
const outcome = plan.kind === "ack" ? await this.sendFastLine(turn, plan.line) ? "deferred" : "failed" : await this.runFastReply(turn, fastText, guidance, fastSpeaker, greeting);
|
|
40197
40416
|
if (outcome === "answered") await this.settleHeldRuntimeMessage(held, false);
|
|
40198
40417
|
else await this.settleHeldRuntimeMessage(outcome === "deferred" ? heldAfterAck : held, true);
|
|
40199
40418
|
}
|
|
@@ -40212,19 +40431,29 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40212
40431
|
async settleFastReplies() {
|
|
40213
40432
|
while (this.fastReplyChains.size) await Promise.all([...this.fastReplyChains.values()]);
|
|
40214
40433
|
}
|
|
40215
|
-
/**
|
|
40216
|
-
|
|
40434
|
+
/**
|
|
40435
|
+
* Send a line the voice fast path speaks: its memory record is "voice" (H1).
|
|
40436
|
+
* Task 4b (spec §2.1 agent appends (1), A-1, A-2): once published, the line enters the call buffer as "fast_path",
|
|
40437
|
+
* unless the turn it answers failed owner verification (its id, or any fragment of a coalesced turn). A line answering
|
|
40438
|
+
* a turn the ingest did not find voice-scoped is not appended either (defense in depth: the fast path fires only on
|
|
40439
|
+
* voice-scoped turns, Task 5).
|
|
40440
|
+
*/
|
|
40441
|
+
async sendSpoken(channelId, text, messageId, turn) {
|
|
40217
40442
|
this.spokenReplyIds.add(messageId);
|
|
40218
40443
|
try {
|
|
40219
40444
|
await this.send(channelId, text, messageId);
|
|
40220
40445
|
} finally {
|
|
40221
40446
|
this.spokenReplyIds.delete(messageId);
|
|
40222
40447
|
}
|
|
40448
|
+
if (turn.voiceScoped === true && turn.anyFragmentVerifyFailed !== true && !this.isOwnerVerifyFailed(turn.messageId)) {
|
|
40449
|
+
this.callBuffer.appendAgent(channelId, text, "fast_path");
|
|
40450
|
+
}
|
|
40223
40451
|
}
|
|
40224
40452
|
/** Speak a fixed acknowledgement line; failures are counted, never thrown. */
|
|
40225
|
-
async sendFastLine(
|
|
40453
|
+
async sendFastLine(turn, line) {
|
|
40454
|
+
const { channelId, messageId } = turn;
|
|
40226
40455
|
try {
|
|
40227
|
-
await this.sendSpoken(channelId, line, fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, messageId));
|
|
40456
|
+
await this.sendSpoken(channelId, line, fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, messageId), turn);
|
|
40228
40457
|
this.fastPathSent++;
|
|
40229
40458
|
return true;
|
|
40230
40459
|
} catch (e) {
|
|
@@ -40238,8 +40467,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40238
40467
|
* the fast model's reply — or, in `ack_only`, the greeting template — is
|
|
40239
40468
|
* spoken, and the outcome decides whether the runtime answers next.
|
|
40240
40469
|
*
|
|
40241
|
-
* MUST NEVER throw: every failure path (
|
|
40242
|
-
*
|
|
40470
|
+
* MUST NEVER throw: every failure path (the LLM call, normalization,
|
|
40471
|
+
* or the send itself) is caught, counted in
|
|
40243
40472
|
* `fastPathFailed`, and logged WITHOUT the turn text or the model's reply.
|
|
40244
40473
|
* The `fastReplyId(...)` send id is deterministic, so a duplicate that
|
|
40245
40474
|
* slipped past the in-memory dedupe (e.g. a restart) resolves to the same
|
|
@@ -40253,20 +40482,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40253
40482
|
try {
|
|
40254
40483
|
if ((this.cfg.voiceFastPathMode ?? "full") === "ack_only") {
|
|
40255
40484
|
if (!isGreeting) return "failed";
|
|
40256
|
-
const ok = await this.sendFastLine(m
|
|
40485
|
+
const ok = await this.sendFastLine(m, ackOnlyGreeting(this.voiceAgentName(), speaker?.displayName));
|
|
40257
40486
|
return ok ? "answered" : "failed";
|
|
40258
40487
|
}
|
|
40259
40488
|
const fastLlm = this.fastLlm;
|
|
40260
40489
|
if (!fastLlm || !this.cfg.voice) return "failed";
|
|
40261
|
-
let memory;
|
|
40262
|
-
|
|
40263
|
-
|
|
40264
|
-
|
|
40265
|
-
|
|
40266
|
-
|
|
40267
|
-
}
|
|
40268
|
-
memory = { recent: [], facts: [] };
|
|
40269
|
-
}
|
|
40490
|
+
let memory = { recent: [], facts: [] };
|
|
40491
|
+
const snapshot = m.voiceSnapshot;
|
|
40492
|
+
if (m.voiceScoped === true && snapshot !== void 0 && m.anyFragmentVerifyFailed !== true && this.mayReadHistoryForTurn(m.messageId, m.authorization)) {
|
|
40493
|
+
memory = {
|
|
40494
|
+
recent: [...snapshot.recent, ...this.callBuffer.agentAfter(m.channelId, { seq: snapshot.endSeq, generation: snapshot.generation })].slice(-FAST_PATH_MEMORY_TURNS),
|
|
40495
|
+
facts: []
|
|
40496
|
+
};
|
|
40270
40497
|
}
|
|
40271
40498
|
const messages = buildFastMessages({
|
|
40272
40499
|
persona: this.cfg.voice.persona,
|
|
@@ -40289,7 +40516,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40289
40516
|
const refusal = !isGreeting && isRefusal(split3.reply);
|
|
40290
40517
|
const defer = split3.defer || refusal;
|
|
40291
40518
|
const reply = refusal ? deferAck(m.messageId) : split3.reply;
|
|
40292
|
-
await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId));
|
|
40519
|
+
await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId), m);
|
|
40293
40520
|
this.fastPathSent++;
|
|
40294
40521
|
if (defer) this.fastPathDeferred++;
|
|
40295
40522
|
if (refusal) this.fastPathRefusalDeferred++;
|
|
@@ -40353,6 +40580,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40353
40580
|
async memoryAtRelease(message) {
|
|
40354
40581
|
const prior = message.memory;
|
|
40355
40582
|
if (!prior || !this.mayReadHistoryForTurn(message.message_id, message.authorization)) return prior;
|
|
40583
|
+
if (this.frameIsVoiceScoped(message)) {
|
|
40584
|
+
const tails = this.voiceCoalescedTails.get(message.message_id) ?? [];
|
|
40585
|
+
if (tails.some((id) => this.isOwnerVerifyFailed(id))) return prior;
|
|
40586
|
+
const noted = this.callBuffer.seqNoted(message.message_id);
|
|
40587
|
+
if (noted === void 0) return prior;
|
|
40588
|
+
const spoken2 = this.voiceFrameTurns(this.callBuffer.agentAfter(message.channel_id, noted));
|
|
40589
|
+
if (spoken2.length === 0) return prior;
|
|
40590
|
+
return { recent: [...prior.recent, ...spoken2].slice(-_BridgeAgent.TURN_MEMORY_LIMIT), facts: [] };
|
|
40591
|
+
}
|
|
40356
40592
|
let fresh;
|
|
40357
40593
|
try {
|
|
40358
40594
|
fresh = await this.memory.context(message.channel_id, _BridgeAgent.TURN_MEMORY_LIMIT);
|
|
@@ -40864,9 +41100,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40864
41100
|
});
|
|
40865
41101
|
}
|
|
40866
41102
|
/** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
|
|
40867
|
-
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId) {
|
|
41103
|
+
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId, voiceScoped) {
|
|
40868
41104
|
const held = await this.heldPosition(channelId);
|
|
40869
|
-
const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict });
|
|
41105
|
+
const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict, voiceScoped });
|
|
40870
41106
|
if (!(auth.audience === "team_room" && auth.tool_grants.length > 0)) return auth;
|
|
40871
41107
|
if (this.roomGrantEpochs.size >= ROOM_GRANT_TRACK_MAX || held.epoch === null || held.groupId === null) {
|
|
40872
41108
|
writeServiceLog("warn", "room_grant.untracked");
|
|
@@ -40881,17 +41117,22 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40881
41117
|
* guess. The returned object and grant array are frozen before they cross the
|
|
40882
41118
|
* daemon boundary so one consumer cannot widen a later execution check.
|
|
40883
41119
|
*
|
|
40884
|
-
* Grants (architect ruling 2026-10-02):
|
|
40885
|
-
* the kill switch is on, the channel is a DM, and EITHER
|
|
41120
|
+
* Grants (architect ruling 2026-10-02; P-5 and G-1, spec §9): an owner-DM
|
|
41121
|
+
* constant only when the kill switch is on, the channel is a DM, and EITHER
|
|
40886
41122
|
* (a) audience is owner_dm AND the sender is the channel's pinned DM
|
|
40887
41123
|
* counterpart (`channelOwner`, never "anomalous"). A server-marked
|
|
40888
41124
|
* `is_agent_owner` on a member who is NOT the DM's single human
|
|
40889
41125
|
* counterpart gets nothing; on the counterpart it is trusted (see
|
|
40890
|
-
* LIMIT below);
|
|
41126
|
+
* LIMIT below). OWNER_DM_DEFAULT_READ_GRANTS on a typed turn;
|
|
41127
|
+
* RELAYED_VOICE_GRANTS and no memory_write when `opts.voiceScoped`
|
|
41128
|
+
* (G-1: the ingest predicate isVoiceScopedTurn, spec §1); OR
|
|
40891
41129
|
* (b) the sender is this channel's server-marked relay (`is_relay`) and the
|
|
40892
41130
|
* relayed `speaker_identity_id` (passed by the caller only for relay
|
|
40893
41131
|
* turns) is that same pinned owner, whose roster row is the agent's
|
|
40894
|
-
* owner. A speaker claim from any non-relay
|
|
41132
|
+
* owner: RELAYED_VOICE_GRANTS. A speaker claim from any non-relay
|
|
41133
|
+
* sender gets nothing.
|
|
41134
|
+
* RELAYED_VOICE_GRANTS is never manifest-eligible: effectiveToolGrants gives
|
|
41135
|
+
* the constant by reference, or the fallback when a good manifest is held.
|
|
40895
41136
|
* Both need the roster fetched at `heldEpoch` (the agent's current MLS
|
|
40896
41137
|
* epoch for the channel), or the DM marked epoch-fresh at this ingest
|
|
40897
41138
|
* cycle's refresh (audit M-B4).
|
|
@@ -40935,9 +41176,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40935
41176
|
const ownerDirect = audience === "owner_dm" && senderId === ownerId;
|
|
40936
41177
|
const owner = roster?.get(ownerId);
|
|
40937
41178
|
const ownerRelayed = relaySpeakerId !== void 0 && sender.is_relay && relaySpeakerId === ownerId && owner !== void 0 && !owner.is_agent && owner.is_agent_owner;
|
|
40938
|
-
if (ownerDirect
|
|
41179
|
+
if (ownerDirect) tool_grants = opts.voiceScoped === true ? RELAYED_VOICE_GRANTS : OWNER_DM_DEFAULT_READ_GRANTS;
|
|
41180
|
+
if (ownerRelayed) tool_grants = RELAYED_VOICE_GRANTS;
|
|
40939
41181
|
if (ownerRelayed) relaySpeaker = "owner";
|
|
40940
|
-
if (ownerDirect && relaySpeakerId === void 0 &&
|
|
41182
|
+
if (ownerDirect && relaySpeakerId === void 0 && // G-1: never on a voice-scoped turn.
|
|
41183
|
+
opts.voiceScoped !== true && (this.cfg.memoryProvider ?? "securecoms") === "securecoms" && // Final review M-4: the live payload flag too, so with notes off the
|
|
40941
41184
|
// runtime does not offer memory_note at all. Not memoryConfigConfirmed:
|
|
40942
41185
|
// a turn the boot drain pulls before the boot refetch keeps its mark,
|
|
40943
41186
|
// and the read and write gates (notesProviderOn) hold it until then.
|
|
@@ -40954,11 +41197,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40954
41197
|
const binding = this.manifestBinding;
|
|
40955
41198
|
const boundNow = binding !== null && binding.state === "bound" && binding.counter === this.toolGrantCounter;
|
|
40956
41199
|
const signedTargets = boundNow ? binding.signedTargets : { endpoints: 0, databases: 0 };
|
|
40957
|
-
if (tool_grants === OWNER_DM_DEFAULT_READ_GRANTS) {
|
|
40958
|
-
const eligible = relaySpeakerId === void 0 && opts.ownerVerdict?.kind === "verified" && manifest?.owner_dm_channel_id === channelId && boundNow && this.manifestConsumer !== null;
|
|
41200
|
+
if (tool_grants === OWNER_DM_DEFAULT_READ_GRANTS || tool_grants === RELAYED_VOICE_GRANTS) {
|
|
41201
|
+
const eligible = relaySpeakerId === void 0 && opts.ownerVerdict?.kind === "verified" && manifest?.owner_dm_channel_id === channelId && boundNow && this.manifestConsumer !== null && tool_grants !== RELAYED_VOICE_GRANTS;
|
|
40959
41202
|
const r = effectiveToolGrants({
|
|
40960
41203
|
audience: "owner_dm",
|
|
40961
|
-
base:
|
|
41204
|
+
base: tool_grants,
|
|
40962
41205
|
manifest,
|
|
40963
41206
|
eligible,
|
|
40964
41207
|
manifestSwitchOn: this.ownerDmManifestGrants,
|
|
@@ -41680,6 +41923,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41680
41923
|
const acked = [];
|
|
41681
41924
|
for (const m of entries) {
|
|
41682
41925
|
try {
|
|
41926
|
+
if (m.voiceMode === false) this.callBuffer.reset(m.channelId);
|
|
41683
41927
|
const encId = m.envelope.enclave_id;
|
|
41684
41928
|
if (encId && !this.channelEnclaveRecorded.has(m.channelId)) {
|
|
41685
41929
|
this.channelEnclaveRecorded.add(m.channelId);
|
|
@@ -41837,6 +42081,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41837
42081
|
continue;
|
|
41838
42082
|
}
|
|
41839
42083
|
if (typeof parsed.text === "string") {
|
|
42084
|
+
if (parsed.text.trim() === VOICE_CALL_START_MARKER) this.callBuffer.reset(m.channelId);
|
|
41840
42085
|
const meta = this.freshMeta(m.channelId);
|
|
41841
42086
|
const senderType = m.envelope.sender_type;
|
|
41842
42087
|
const senderIsAgent = senderType === "agent" ? true : senderType === "human" ? false : void 0;
|
|
@@ -41878,23 +42123,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41878
42123
|
const senderName = this.freshRoster(m.channelId)?.get(
|
|
41879
42124
|
m.envelope.sender_id
|
|
41880
42125
|
)?.name;
|
|
42126
|
+
const voiceScoped = isVoiceScopedTurn({
|
|
42127
|
+
speaker,
|
|
42128
|
+
transcribed: parsed.transcribed,
|
|
42129
|
+
senderIsRelay: this.freshRoster(m.channelId)?.get(m.envelope.sender_id)?.is_relay === true,
|
|
42130
|
+
text: parsed.text
|
|
42131
|
+
});
|
|
41881
42132
|
const ownerVerdict = await this.verifyOwnerTurn(m);
|
|
41882
42133
|
const turnAuthorization = await this.authorizeTurn(
|
|
41883
42134
|
m.channelId,
|
|
41884
42135
|
m.messageId,
|
|
41885
42136
|
m.envelope.sender_id,
|
|
41886
42137
|
ownerVerdict,
|
|
41887
|
-
speaker?.identityId
|
|
42138
|
+
speaker?.identityId,
|
|
42139
|
+
voiceScoped
|
|
41888
42140
|
);
|
|
41889
42141
|
await this.prepareNotesForTurn(m.channelId, turnAuthorization);
|
|
41890
|
-
const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization);
|
|
41891
42142
|
const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
|
|
41892
42143
|
const humanTurn = senderIsAgent !== true || speaker !== void 0;
|
|
41893
|
-
const fastPathEligible = Boolean(m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
42144
|
+
const fastPathEligible = Boolean(voiceScoped && m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
41894
42145
|
const turnText = parsed.text;
|
|
41895
42146
|
const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
|
|
41896
42147
|
const windowEligible = voiceWindowEligible({ voiceMode: m.voiceMode, speaker, transcribed: parsed.transcribed, voiceGuidance, isCallStart });
|
|
41897
42148
|
const coalescible = this.voiceCoalesceWindowMs > 0 && windowEligible;
|
|
42149
|
+
const callPeer = admitsToCallBuffer({ speaker, transcribed: parsed.transcribed });
|
|
42150
|
+
let voiceSnapshot;
|
|
42151
|
+
if (voiceScoped) {
|
|
42152
|
+
this.callBuffer.markVoiceScoped(m.channelId, m.messageId);
|
|
42153
|
+
if (!coalescible) {
|
|
42154
|
+
voiceSnapshot = this.takeVoiceSnapshot(m.channelId, m.messageId);
|
|
42155
|
+
if (callPeer && !this.isOwnerVerifyFailed(m.messageId)) this.callBuffer.appendPeer(m.channelId, parsed.text, m.messageId);
|
|
42156
|
+
}
|
|
42157
|
+
}
|
|
42158
|
+
const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization, void 0, voiceScoped ? { snapshot: voiceSnapshot } : void 0);
|
|
41898
42159
|
if (!coalescible) {
|
|
41899
42160
|
const kind2 = m.voiceMode === true || parsed.transcribed === true ? "voice" : "text";
|
|
41900
42161
|
if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId: m.channelId, messageId: m.messageId, text: parsed.text, authorization: turnAuthorization, kind: kind2 });
|
|
@@ -41910,7 +42171,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41910
42171
|
const fastGreeting = isCallStart && fastPathFiring;
|
|
41911
42172
|
const runtimeVoice = (ackSent) => m.voiceMode ? {
|
|
41912
42173
|
voice: true,
|
|
41913
|
-
|
|
42174
|
+
// Task 6 (OQ-3): a voice-scoped frame's guidance ends with VOICE_CALL_CLAUSE, on all three frames
|
|
42175
|
+
// (held, coalesced after-ack, after-ack); a typed turn during a call keeps its guidance.
|
|
42176
|
+
guidance: `${ackSent ? `${voiceGuidance} ${FAST_PATH_SPOKEN_GUIDANCE}` : voiceGuidance}${voiceScoped ? ` ${VOICE_CALL_CLAUSE}` : ""}`
|
|
41914
42177
|
} : {};
|
|
41915
42178
|
const runtimeEntry = (ackSent, memory) => ({
|
|
41916
42179
|
channel_id: m.channelId,
|
|
@@ -41931,6 +42194,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41931
42194
|
// identify: the envelope's own claim (relay or on-device
|
|
41932
42195
|
// transcript) or a relay-attributed turn.
|
|
41933
42196
|
...parsed.transcribed === true || speaker !== void 0 ? { transcribed: true } : {},
|
|
42197
|
+
...voiceScoped ? { voice_scoped: true } : {},
|
|
41934
42198
|
...runtimeVoice(ackSent)
|
|
41935
42199
|
});
|
|
41936
42200
|
if (coalescible) {
|
|
@@ -41960,6 +42224,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41960
42224
|
acked.push(m.queueId);
|
|
41961
42225
|
continue;
|
|
41962
42226
|
}
|
|
42227
|
+
const bufferGeneration = this.callBuffer.generation(m.channelId);
|
|
41963
42228
|
const held = runtimeEntry(false, await runtimeMemory);
|
|
41964
42229
|
if (!await this.stageHeldRuntimeMessage(held)) continue;
|
|
41965
42230
|
this.noteVoiceCoalesced(m.messageId);
|
|
@@ -41970,7 +42235,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41970
42235
|
closed: false,
|
|
41971
42236
|
pending: Promise.resolve(),
|
|
41972
42237
|
purgeEpoch: purgeEpochAtReceipt.get(m.channelId),
|
|
41973
|
-
itemCapture: itemCaptureAtReceipt.get(m.channelId)
|
|
42238
|
+
itemCapture: itemCaptureAtReceipt.get(m.channelId),
|
|
42239
|
+
bufferGeneration
|
|
41974
42240
|
};
|
|
41975
42241
|
this.voiceCoalescing.set(windowKey, win);
|
|
41976
42242
|
const openedAt = Date.now();
|
|
@@ -41986,9 +42252,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41986
42252
|
const [firstId, ...tailIds] = win.ids;
|
|
41987
42253
|
if (tailIds.length) this.voiceCoalescedTails.set(firstId, tailIds);
|
|
41988
42254
|
const text = win.parts.join(" ");
|
|
42255
|
+
const sameCall = this.callBuffer.generation(channelId) === win.bufferGeneration;
|
|
42256
|
+
const anyFragmentVerifyFailed = win.ids.some((id) => this.isOwnerVerifyFailed(id));
|
|
42257
|
+
let windowSnapshot;
|
|
42258
|
+
if (sameCall) {
|
|
42259
|
+
this.callBuffer.markVoiceScoped(channelId, firstId);
|
|
42260
|
+
windowSnapshot = this.takeVoiceSnapshot(channelId, firstId);
|
|
42261
|
+
}
|
|
42262
|
+
if (callPeer && sameCall && !anyFragmentVerifyFailed) this.callBuffer.appendPeer(channelId, text, firstId);
|
|
41989
42263
|
if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
|
|
41990
42264
|
else await this.prepareRecallForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
|
|
41991
|
-
const
|
|
42265
|
+
const windowMemory = await this.memoryForTurn(channelId, firstId, turnAuthorization, void 0, { snapshot: anyFragmentVerifyFailed ? void 0 : windowSnapshot });
|
|
42266
|
+
const merged = { ...held, text, memory: windowMemory };
|
|
41992
42267
|
if (this.unifiedOn) {
|
|
41993
42268
|
const snap = win.itemCapture ?? { ...this.store.itemCapture(channelId), purgeEpoch: -1 };
|
|
41994
42269
|
const work = this.tapUnifiedVoice(merged, { stamp: this.ingestGeneration.get(firstId), capture: { ...snap, purgeEpoch: win.purgeEpoch ?? -1 } });
|
|
@@ -42016,7 +42291,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42016
42291
|
const release = this.reserveFastReply(channelId);
|
|
42017
42292
|
try {
|
|
42018
42293
|
await this.speakAndSettle(
|
|
42019
|
-
{ channelId, messageId: firstId, senderId, authorization: turnAuthorization },
|
|
42294
|
+
{ channelId, messageId: firstId, senderId, authorization: turnAuthorization, voiceScoped: voiceScoped && sameCall, voiceSnapshot: windowSnapshot, anyFragmentVerifyFailed },
|
|
42020
42295
|
plan,
|
|
42021
42296
|
merged,
|
|
42022
42297
|
{ ...merged, ...runtimeVoice(true) },
|
|
@@ -42048,7 +42323,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42048
42323
|
channelId: m.channelId,
|
|
42049
42324
|
messageId: m.messageId,
|
|
42050
42325
|
senderId: m.envelope.sender_id,
|
|
42051
|
-
authorization: turnAuthorization
|
|
42326
|
+
authorization: turnAuthorization,
|
|
42327
|
+
voiceScoped,
|
|
42328
|
+
voiceSnapshot
|
|
42052
42329
|
};
|
|
42053
42330
|
const guidanceForFast = voiceGuidance;
|
|
42054
42331
|
const greeting = fastGreeting;
|
|
@@ -44171,10 +44448,13 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
44171
44448
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
44172
44449
|
return;
|
|
44173
44450
|
}
|
|
44451
|
+
const record = typeof m.in_reply_to === "string" ? inflight.get(m.in_reply_to) : void 0;
|
|
44452
|
+
const callBufferReply = session.consumer === "runtime" && session.perToolGrants === true && session.harnessClaimed !== true && typeof m.in_reply_to === "string" && record !== void 0 && session.deliveryToken.get(m.in_reply_to) !== void 0 && record.channel_id === m.channel_id && !agent.isRuntimeMessageRetired(m.in_reply_to);
|
|
44174
44453
|
const sendOpts = {
|
|
44175
44454
|
agentTurn: isAgentTurn(agent, m),
|
|
44176
44455
|
...typeof m.in_reply_to === "string" ? { inReplyTo: m.in_reply_to } : {},
|
|
44177
|
-
...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {}
|
|
44456
|
+
...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {},
|
|
44457
|
+
...callBufferReply ? { callBufferReply: true } : {}
|
|
44178
44458
|
};
|
|
44179
44459
|
if (m.a2a && typeof m.a2a === "object") {
|
|
44180
44460
|
const a = m.a2a;
|