@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/index.js
CHANGED
|
@@ -33431,7 +33431,8 @@ var FastLlmClient = class {
|
|
|
33431
33431
|
try {
|
|
33432
33432
|
res = await this.fetchImpl(`${this.cfg.baseUrl}${this.cfg.path ?? "/chat/completions"}`, {
|
|
33433
33433
|
method: "POST",
|
|
33434
|
-
|
|
33434
|
+
// Final fix T8: configured headers first, so they can never displace content-type or the bearer.
|
|
33435
|
+
headers: { ...this.cfg.headers, "content-type": "application/json", ...this.key ? { authorization: `Bearer ${this.key}` } : {} },
|
|
33435
33436
|
body: JSON.stringify({ model: this.cfg.model, messages, max_tokens: this.cfg.maxTokens, temperature: 0.4 }),
|
|
33436
33437
|
signal: ac.signal
|
|
33437
33438
|
});
|
|
@@ -33635,7 +33636,9 @@ function settingsToVoiceConfig(s, proxy) {
|
|
|
33635
33636
|
model: "securecoms-fast",
|
|
33636
33637
|
apiKey: proxy.apiKey,
|
|
33637
33638
|
timeoutMs: 2500,
|
|
33638
|
-
maxTokens: 80
|
|
33639
|
+
maxTokens: 80,
|
|
33640
|
+
// Spec 7.2: tells the proxy this bridge scopes voice turns. Sent to the SecureComs proxy only.
|
|
33641
|
+
headers: { "x-securecoms-bridge-caps": "voice-scope-v1" }
|
|
33639
33642
|
},
|
|
33640
33643
|
settings: s
|
|
33641
33644
|
};
|
|
@@ -33643,6 +33646,7 @@ function settingsToVoiceConfig(s, proxy) {
|
|
|
33643
33646
|
|
|
33644
33647
|
// src/voice/persona.ts
|
|
33645
33648
|
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.";
|
|
33649
|
+
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.";
|
|
33646
33650
|
var schema = z2.object({
|
|
33647
33651
|
persona: z2.object({
|
|
33648
33652
|
display_name: z2.string().min(1).optional(),
|
|
@@ -33655,6 +33659,8 @@ var schema = z2.object({
|
|
|
33655
33659
|
model: z2.string().min(1),
|
|
33656
33660
|
api_key: z2.string().min(1).optional(),
|
|
33657
33661
|
api_key_env: z2.string().min(1).optional(),
|
|
33662
|
+
// E.2: only the exact string "direct" opts in; any other value is ignored (today's proxy behavior), never an error.
|
|
33663
|
+
route: z2.unknown().optional(),
|
|
33658
33664
|
timeout_ms: z2.number().int().min(500).max(1e4).default(2500),
|
|
33659
33665
|
max_tokens: z2.number().int().min(16).max(400).default(80)
|
|
33660
33666
|
})
|
|
@@ -33679,7 +33685,8 @@ function parseVoiceConfig(raw, env) {
|
|
|
33679
33685
|
model: v.fast_path.model,
|
|
33680
33686
|
apiKey,
|
|
33681
33687
|
timeoutMs: v.fast_path.timeout_ms,
|
|
33682
|
-
maxTokens: v.fast_path.max_tokens
|
|
33688
|
+
maxTokens: v.fast_path.max_tokens,
|
|
33689
|
+
...v.fast_path.route === "direct" ? { route: "direct" } : {}
|
|
33683
33690
|
}
|
|
33684
33691
|
};
|
|
33685
33692
|
}
|
|
@@ -33723,7 +33730,7 @@ function buildFastMessages(args2) {
|
|
|
33723
33730
|
const sp = systemPrompt(args2.persona, args2.settings, args2.agentName);
|
|
33724
33731
|
const head = sp.slice(0, sp.length - CONTINUITY_CLAUSE.length).trimEnd();
|
|
33725
33732
|
const notesLine = args2.notesDeferLine ? ` ${FAST_NOTES_DEFER_LINE}` : "";
|
|
33726
|
-
const system = `${head} ${args2.guidance.trim()}${notesLine} ${FAST_DEFER_RULE} ${
|
|
33733
|
+
const system = `${head} ${args2.guidance.trim()}${notesLine} ${FAST_DEFER_RULE} ${VOICE_CALL_CLAUSE}`;
|
|
33727
33734
|
const recent = args2.memory.recent.map((t) => ({
|
|
33728
33735
|
role: t.role === "agent" ? "assistant" : "user",
|
|
33729
33736
|
content: t.text
|
|
@@ -33760,6 +33767,160 @@ function normalizeFastReply(raw, maxChars = 200, maxSentences = 2) {
|
|
|
33760
33767
|
return sentences.slice(0, maxSentences).join(" ").slice(0, maxChars);
|
|
33761
33768
|
}
|
|
33762
33769
|
|
|
33770
|
+
// src/voice/voice-scope.ts
|
|
33771
|
+
function isVoiceScopedTurn(t) {
|
|
33772
|
+
return t.speaker !== void 0 || t.transcribed === true || t.senderIsRelay || t.text.trim() === VOICE_CALL_START_MARKER;
|
|
33773
|
+
}
|
|
33774
|
+
function admitsToCallBuffer(t) {
|
|
33775
|
+
return t.speaker !== void 0 || t.transcribed === true;
|
|
33776
|
+
}
|
|
33777
|
+
|
|
33778
|
+
// src/voice/call-buffer.ts
|
|
33779
|
+
var CALL_BUFFER_CAP = 50;
|
|
33780
|
+
var CALL_BUFFER_IDLE_MS = 30 * 6e4;
|
|
33781
|
+
var CALL_BUFFER_ID_CAP = 5e3;
|
|
33782
|
+
var CallBuffer = class {
|
|
33783
|
+
constructor(opts) {
|
|
33784
|
+
this.opts = opts;
|
|
33785
|
+
}
|
|
33786
|
+
channels = /* @__PURE__ */ new Map();
|
|
33787
|
+
/** messageId -> channelId, insertion ordered (oldest first) for the cap. */
|
|
33788
|
+
voiceIds = /* @__PURE__ */ new Map();
|
|
33789
|
+
/** messageId -> { channelId, the channel's bufSeq at the mark, the channel's generation then }. */
|
|
33790
|
+
seqNotes = /* @__PURE__ */ new Map();
|
|
33791
|
+
/**
|
|
33792
|
+
* Final fix I-1: messageId -> { channelId, the channel's bufSeq just before the turn's own peer entry }. A row the
|
|
33793
|
+
* backend redelivers (a failed ack, runtime_inbox.full backpressure) is ingested again; its id here means its text is
|
|
33794
|
+
* already in the call, so it is not appended again and its snapshot stops before its own entry (A-6).
|
|
33795
|
+
*/
|
|
33796
|
+
admitted = /* @__PURE__ */ new Map();
|
|
33797
|
+
/** The last bufSeq handed out per channel (kept across resets, so the sequence never runs backwards). */
|
|
33798
|
+
lastSeq = /* @__PURE__ */ new Map();
|
|
33799
|
+
nextSeq = 1;
|
|
33800
|
+
/** Per-channel reset generation (advanced by every reset, the idle rule included). */
|
|
33801
|
+
gens = /* @__PURE__ */ new Map();
|
|
33802
|
+
/**
|
|
33803
|
+
* Append a spoken turn's text, once per turn id (final fix I-1). Returns false (and appends nothing) when the id was
|
|
33804
|
+
* already admitted in this call, or when the buffer is disabled.
|
|
33805
|
+
*/
|
|
33806
|
+
appendPeer(channelId, text, messageId) {
|
|
33807
|
+
if (!this.opts.enabled) return false;
|
|
33808
|
+
if (this.admittedAt(messageId) !== void 0) return false;
|
|
33809
|
+
const before = this.seqAt(channelId);
|
|
33810
|
+
this.append(channelId, { role: "peer", text });
|
|
33811
|
+
this.admitted.set(messageId, { channelId, before });
|
|
33812
|
+
if (this.admitted.size > CALL_BUFFER_ID_CAP) this.admitted.delete(this.admitted.keys().next().value);
|
|
33813
|
+
return true;
|
|
33814
|
+
}
|
|
33815
|
+
/**
|
|
33816
|
+
* Final fix I-1: for a turn already admitted in this call, the channel's bufSeq just before its own entry (the end seq
|
|
33817
|
+
* of its first snapshot); undefined for a turn not admitted (or forgotten by a reset, the idle rule or the id cap).
|
|
33818
|
+
*/
|
|
33819
|
+
admittedAt(messageId) {
|
|
33820
|
+
const a = this.admitted.get(messageId);
|
|
33821
|
+
if (a === void 0) return void 0;
|
|
33822
|
+
this.expireIfIdle(a.channelId);
|
|
33823
|
+
return this.admitted.get(messageId)?.before;
|
|
33824
|
+
}
|
|
33825
|
+
appendAgent(channelId, text, origin) {
|
|
33826
|
+
this.append(channelId, { role: "agent", text, origin });
|
|
33827
|
+
}
|
|
33828
|
+
/**
|
|
33829
|
+
* Record a voice-scoped turn's id (for reply admission only, A-4) and note the channel's seq at this point. Final fix
|
|
33830
|
+
* I-1: a turn already admitted (a redelivered row) keeps the seq of its first admission, so the note never moves past
|
|
33831
|
+
* the turn's own entry.
|
|
33832
|
+
*/
|
|
33833
|
+
markVoiceScoped(channelId, messageId) {
|
|
33834
|
+
if (!this.opts.enabled) return;
|
|
33835
|
+
this.expireIfIdle(channelId);
|
|
33836
|
+
this.voiceIds.delete(messageId);
|
|
33837
|
+
this.voiceIds.set(messageId, channelId);
|
|
33838
|
+
if (this.voiceIds.size > CALL_BUFFER_ID_CAP) this.voiceIds.delete(this.voiceIds.keys().next().value);
|
|
33839
|
+
const seq = this.admittedAt(messageId) ?? this.seqAt(channelId);
|
|
33840
|
+
this.seqNotes.delete(messageId);
|
|
33841
|
+
this.seqNotes.set(messageId, { channelId, seq, generation: this.generation(channelId) });
|
|
33842
|
+
if (this.seqNotes.size > CALL_BUFFER_ID_CAP) this.seqNotes.delete(this.seqNotes.keys().next().value);
|
|
33843
|
+
}
|
|
33844
|
+
/** A-4: read ONLY by the bridge's `afterPublish` reply admission. */
|
|
33845
|
+
isVoiceScopedId(messageId) {
|
|
33846
|
+
const channelId = this.voiceIds.get(messageId);
|
|
33847
|
+
if (channelId === void 0) return false;
|
|
33848
|
+
this.expireIfIdle(channelId);
|
|
33849
|
+
return this.voiceIds.has(messageId);
|
|
33850
|
+
}
|
|
33851
|
+
/** The channel's mark noted when `messageId` was marked (the release re-read), or undefined (e.g. after a restart). */
|
|
33852
|
+
seqNoted(messageId) {
|
|
33853
|
+
const note = this.seqNotes.get(messageId);
|
|
33854
|
+
if (note === void 0) return void 0;
|
|
33855
|
+
this.expireIfIdle(note.channelId);
|
|
33856
|
+
const kept = this.seqNotes.get(messageId);
|
|
33857
|
+
return kept === void 0 ? void 0 : { seq: kept.seq, generation: kept.generation };
|
|
33858
|
+
}
|
|
33859
|
+
/** The bufSeq of the channel's newest entry (0 before any append). A snapshot's end seq. */
|
|
33860
|
+
seqAt(channelId) {
|
|
33861
|
+
this.expireIfIdle(channelId);
|
|
33862
|
+
return this.lastSeq.get(channelId) ?? 0;
|
|
33863
|
+
}
|
|
33864
|
+
/**
|
|
33865
|
+
* The channel's last `limit` entries, oldest first. With `upTo` (final fix I-1, a redelivered turn's re-snapshot),
|
|
33866
|
+
* only entries at or below that bufSeq.
|
|
33867
|
+
*/
|
|
33868
|
+
recent(channelId, limit, upTo) {
|
|
33869
|
+
if (limit <= 0) return [];
|
|
33870
|
+
this.expireIfIdle(channelId);
|
|
33871
|
+
const all = this.channels.get(channelId)?.entries ?? [];
|
|
33872
|
+
const entries = upTo === void 0 ? all : all.filter((e) => e.bufSeq <= upTo);
|
|
33873
|
+
return entries.slice(-limit).map(deliver);
|
|
33874
|
+
}
|
|
33875
|
+
/**
|
|
33876
|
+
* The channel's AGENT entries appended after `since.seq` (lines spoken or answered since a snapshot), oldest first.
|
|
33877
|
+
* Final fix M-4: nothing when the channel has been reset since the mark (a later call's lines).
|
|
33878
|
+
*/
|
|
33879
|
+
agentAfter(channelId, since) {
|
|
33880
|
+
if (this.generation(channelId) !== since.generation) return [];
|
|
33881
|
+
const entries = this.channels.get(channelId)?.entries ?? [];
|
|
33882
|
+
return entries.filter((e) => e.role === "agent" && e.bufSeq > since.seq).map(deliver);
|
|
33883
|
+
}
|
|
33884
|
+
/**
|
|
33885
|
+
* The channel's reset generation: it changes on every reset (explicit or idle) and on nothing else. A caller that
|
|
33886
|
+
* holds text across an await (the coalesce window) compares it to skip an append into a call that has since ended.
|
|
33887
|
+
*/
|
|
33888
|
+
generation(channelId) {
|
|
33889
|
+
this.expireIfIdle(channelId);
|
|
33890
|
+
return this.gens.get(channelId) ?? 0;
|
|
33891
|
+
}
|
|
33892
|
+
/** Clear the channel: its entries, its voice-scoped ids, its seq notes and its admitted ids. Only ever removes. */
|
|
33893
|
+
reset(channelId) {
|
|
33894
|
+
this.gens.set(channelId, (this.gens.get(channelId) ?? 0) + 1);
|
|
33895
|
+
this.channels.delete(channelId);
|
|
33896
|
+
for (const [id, c] of this.voiceIds) if (c === channelId) this.voiceIds.delete(id);
|
|
33897
|
+
for (const [id, n] of this.seqNotes) if (n.channelId === channelId) this.seqNotes.delete(id);
|
|
33898
|
+
for (const [id, a] of this.admitted) if (a.channelId === channelId) this.admitted.delete(id);
|
|
33899
|
+
}
|
|
33900
|
+
append(channelId, e) {
|
|
33901
|
+
if (!this.opts.enabled) return;
|
|
33902
|
+
this.expireIfIdle(channelId);
|
|
33903
|
+
let ch = this.channels.get(channelId);
|
|
33904
|
+
if (ch === void 0) {
|
|
33905
|
+
ch = { entries: [], lastAppend: 0 };
|
|
33906
|
+
this.channels.set(channelId, ch);
|
|
33907
|
+
}
|
|
33908
|
+
const bufSeq = this.nextSeq++;
|
|
33909
|
+
this.lastSeq.set(channelId, bufSeq);
|
|
33910
|
+
ch.entries.push({ ...e, bufSeq });
|
|
33911
|
+
if (ch.entries.length > this.opts.cap) ch.entries.splice(0, ch.entries.length - this.opts.cap);
|
|
33912
|
+
ch.lastAppend = this.opts.now();
|
|
33913
|
+
}
|
|
33914
|
+
/** Reset (c): more than `idleMs` since the channel's last append clears it, checked on every read and append. */
|
|
33915
|
+
expireIfIdle(channelId) {
|
|
33916
|
+
const ch = this.channels.get(channelId);
|
|
33917
|
+
if (ch !== void 0 && this.opts.now() - ch.lastAppend > this.opts.idleMs) this.reset(channelId);
|
|
33918
|
+
}
|
|
33919
|
+
};
|
|
33920
|
+
function deliver(e) {
|
|
33921
|
+
return { role: e.role, text: e.text, seq: 0, ...e.origin ? { origin: e.origin } : {} };
|
|
33922
|
+
}
|
|
33923
|
+
|
|
33763
33924
|
// src/voice/defer-intent.ts
|
|
33764
33925
|
var NEEDS_RUNTIME = [
|
|
33765
33926
|
// Current time / date / day.
|
|
@@ -34139,6 +34300,7 @@ function lineHash(line) {
|
|
|
34139
34300
|
return createHash5("sha256").update(line.trim(), "utf8").digest("hex");
|
|
34140
34301
|
}
|
|
34141
34302
|
var LOCAL_REPORT_FILE = "local-config.json";
|
|
34303
|
+
var LOCAL_PERSONA_FILE_NAMES = ["IDENTITY.md", "SOUL.md", "AGENTS.md"];
|
|
34142
34304
|
var SECRET_PATH_COUNT_MAX = 1e4;
|
|
34143
34305
|
var isCount = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0 && v <= SECRET_PATH_COUNT_MAX;
|
|
34144
34306
|
function parseSecretPathsReport(raw) {
|
|
@@ -34220,18 +34382,23 @@ function readLocalReport(home, identityId) {
|
|
|
34220
34382
|
const hasSearchKey = doc.has_search_key === true;
|
|
34221
34383
|
const hasLlmKey = doc.has_llm_key === true;
|
|
34222
34384
|
const raw = doc.persona;
|
|
34223
|
-
const persona = raw && typeof raw === "object" && !Array.isArray(raw) ?
|
|
34224
|
-
|
|
34225
|
-
)
|
|
34385
|
+
const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
34386
|
+
const personaFiles = [];
|
|
34387
|
+
for (const name of LOCAL_PERSONA_FILE_NAMES) {
|
|
34388
|
+
const text = persona[name];
|
|
34389
|
+
if (typeof text === "string" && text.trim() !== "") {
|
|
34390
|
+
personaFiles.push({ name, bytes: Buffer.byteLength(text, "utf8") });
|
|
34391
|
+
}
|
|
34392
|
+
}
|
|
34226
34393
|
const capRaw = doc.owner_dm_search_cap_effective;
|
|
34227
34394
|
const ownerDmCap = typeof capRaw === "number" && Number.isInteger(capRaw) && capRaw >= 0 && capRaw <= OWNER_DM_SEARCH_DAILY_CAP_MAX ? capRaw : void 0;
|
|
34228
34395
|
return {
|
|
34229
|
-
|
|
34396
|
+
local_persona_files: personaFiles,
|
|
34230
34397
|
has_search_key: hasSearchKey,
|
|
34231
34398
|
has_llm_key: hasLlmKey,
|
|
34232
34399
|
has_extract_provider: doc.has_extract_provider === true,
|
|
34233
34400
|
...ownerDmCap !== void 0 ? { owner_dm_search_cap_effective: ownerDmCap } : {},
|
|
34234
|
-
legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source),
|
|
34401
|
+
legacy_memory: localLegacySources(doc).map(({ path: _path, content: _content, ...source }) => source),
|
|
34235
34402
|
secret_paths: parseSecretPathsReport(doc.secret_paths)
|
|
34236
34403
|
};
|
|
34237
34404
|
}
|
|
@@ -34316,16 +34483,6 @@ async function applyLegacyMemoryFactDecisions(home, identityId, rawDecisions, re
|
|
|
34316
34483
|
}
|
|
34317
34484
|
return { sourcesDeleted, factsAssigned };
|
|
34318
34485
|
}
|
|
34319
|
-
function readManagedMemory(home, identityId) {
|
|
34320
|
-
const p = join15(runtimeConfigDir(home, identityId), MEMORY_FILE);
|
|
34321
|
-
if (!existsSync4(p)) return null;
|
|
34322
|
-
try {
|
|
34323
|
-
const body = readFileSync8(p, "utf8").trim();
|
|
34324
|
-
return body || null;
|
|
34325
|
-
} catch {
|
|
34326
|
-
return null;
|
|
34327
|
-
}
|
|
34328
|
-
}
|
|
34329
34486
|
function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
34330
34487
|
const dir = runtimeConfigDir(home, identityId);
|
|
34331
34488
|
const path = join15(dir, MEMORY_FILE);
|
|
@@ -35113,6 +35270,10 @@ var OWNER_DM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
|
35113
35270
|
var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
35114
35271
|
Object.freeze({ tool: "current_time", mode: "read" })
|
|
35115
35272
|
]);
|
|
35273
|
+
var RELAYED_VOICE_GRANTS = Object.freeze([
|
|
35274
|
+
Object.freeze({ tool: "current_time", mode: "read" }),
|
|
35275
|
+
Object.freeze({ tool: "web_search", mode: "read" })
|
|
35276
|
+
]);
|
|
35116
35277
|
var NO_TOOL_GRANTS = Object.freeze([]);
|
|
35117
35278
|
var WS_CLOSE_SESSION_SUPERSEDED2 = 4409;
|
|
35118
35279
|
var OWNER_REPLAY_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
@@ -35472,6 +35633,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35472
35633
|
workspaceDir: this.cfg.memoryWorkspaceDir,
|
|
35473
35634
|
factsCharBudget: this.cfg.memoryFactsCharBudget
|
|
35474
35635
|
});
|
|
35636
|
+
this.callBuffer = new CallBuffer({
|
|
35637
|
+
cap: CALL_BUFFER_CAP,
|
|
35638
|
+
idleMs: CALL_BUFFER_IDLE_MS,
|
|
35639
|
+
now: () => Date.now(),
|
|
35640
|
+
enabled: (this.cfg.memoryProvider ?? "securecoms") !== "none"
|
|
35641
|
+
});
|
|
35475
35642
|
this.provider = new MlsGroupKeyProvider({
|
|
35476
35643
|
store: this.store,
|
|
35477
35644
|
backend: this.backend,
|
|
@@ -35605,6 +35772,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35605
35772
|
this.injectedFastLlm = !!deps.fastLlm;
|
|
35606
35773
|
this.fileVoiceConfig = cfg.voice;
|
|
35607
35774
|
this.fastLlm = deps.fastLlm ?? (cfg.voice?.fastPath.enabled ? new FastLlmClient(cfg.voice.fastPath, this.fetchImpl) : void 0);
|
|
35775
|
+
if (this.fileVoiceConfig?.fastPath.route === "direct" && !this.fileVoiceConfig.fastPath.apiKey) {
|
|
35776
|
+
writeServiceLog2("warn", "voice.fast_path_direct_no_key");
|
|
35777
|
+
}
|
|
35608
35778
|
this.voiceSettingsSource = cfg.voice ? "file" : "none";
|
|
35609
35779
|
this.latency = deps.latency ?? new LatencyRecorder();
|
|
35610
35780
|
this.voiceCoalesceWindowMs = Math.max(0, deps.voiceCoalesceWindowMs ?? voiceCoalesceWindowFromEnv());
|
|
@@ -36295,6 +36465,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36295
36465
|
startedAt = 0;
|
|
36296
36466
|
store;
|
|
36297
36467
|
memory;
|
|
36468
|
+
/**
|
|
36469
|
+
* Voice fast-path privacy, Task 4b (spec §2.1, OQ-1 ruled): the current calls' spoken turns, in process only. The
|
|
36470
|
+
* ONLY history a voice-scoped turn sees; read through voiceScopedHistory, once per turn, at ingest.
|
|
36471
|
+
*/
|
|
36472
|
+
callBuffer;
|
|
36298
36473
|
/**
|
|
36299
36474
|
* Markdown memory 7a: the one owner of every note mutation and of the
|
|
36300
36475
|
* delivery cache (spec §6.3, §7.2). One per store, so one per identity.
|
|
@@ -36399,6 +36574,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36399
36574
|
const s = parseVoiceSettings(await res.json());
|
|
36400
36575
|
if (s.identityDisplayName) this.lastOwnDisplayName = s.identityDisplayName;
|
|
36401
36576
|
const proxy = settingsToVoiceConfig(s, { baseUrl: this.cfg.apiBase, apiKey: this.cfg.apiKey });
|
|
36577
|
+
const direct = this.fileVoiceConfig?.fastPath.route === "direct" ? this.fileVoiceConfig.fastPath : void 0;
|
|
36402
36578
|
if (s.version === 0 && this.fileVoiceConfig) {
|
|
36403
36579
|
const merged = {
|
|
36404
36580
|
...s,
|
|
@@ -36407,16 +36583,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36407
36583
|
quirks: this.fileVoiceConfig.persona.quirks
|
|
36408
36584
|
};
|
|
36409
36585
|
this.voiceSettings = merged;
|
|
36410
|
-
this.cfg.voice = { persona: this.fileVoiceConfig.persona, fastPath: proxy.fastPath, settings: merged };
|
|
36586
|
+
this.cfg.voice = { persona: this.fileVoiceConfig.persona, fastPath: direct ?? proxy.fastPath, settings: merged };
|
|
36411
36587
|
this.voiceSettingsVersion = 0;
|
|
36412
36588
|
this.voiceSettingsSource = "file";
|
|
36413
36589
|
} else {
|
|
36414
36590
|
this.voiceSettings = s;
|
|
36415
|
-
this.cfg.voice = proxy;
|
|
36591
|
+
this.cfg.voice = direct ? { ...proxy, fastPath: direct } : proxy;
|
|
36416
36592
|
this.voiceSettingsVersion = s.version;
|
|
36417
36593
|
this.voiceSettingsSource = "backend";
|
|
36418
36594
|
}
|
|
36419
|
-
if (!this.injectedFastLlm) {
|
|
36595
|
+
if (!this.injectedFastLlm && !direct) {
|
|
36420
36596
|
this.fastLlm = new FastLlmClient(this.cfg.voice.fastPath, this.fetchImpl);
|
|
36421
36597
|
}
|
|
36422
36598
|
writeServiceLog2("info", "voice.settings_loaded", {
|
|
@@ -36645,12 +36821,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36645
36821
|
const report = {
|
|
36646
36822
|
applied_version: version,
|
|
36647
36823
|
paths: checkConfiguredPaths(skillsJson),
|
|
36648
|
-
//
|
|
36649
|
-
//
|
|
36650
|
-
|
|
36651
|
-
|
|
36652
|
-
// interpret any of it.
|
|
36653
|
-
local_persona: local?.persona ?? null,
|
|
36824
|
+
// F-0 (G-5): the report carries no content. No MEMORY.md text, no persona text, no legacy
|
|
36825
|
+
// text. E13 needs only to know which of the runtime's own persona files hold something, so
|
|
36826
|
+
// this is [{name, bytes}] for each non-blank file and nothing else.
|
|
36827
|
+
local_persona_files: local?.local_persona_files ?? [],
|
|
36654
36828
|
// Only the runtime knows whether a search key file is configured — it
|
|
36655
36829
|
// is an environment value on that process, invisible to the daemon and
|
|
36656
36830
|
// to the backend. The backend needs it to generate a truthful tool
|
|
@@ -36683,9 +36857,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36683
36857
|
...this.reportedClearGeneration(),
|
|
36684
36858
|
// 7b spec §9.2 (Task 19): capability versus state. Read-only: the report never starts the embedder.
|
|
36685
36859
|
...this.reportedRecall(),
|
|
36686
|
-
// Inert legacy files only. `readLocalReport` revalidates the current
|
|
36687
|
-
// regular-file bytes and strips every local path before this
|
|
36688
|
-
// machine.
|
|
36860
|
+
// Inert legacy files only, metadata only. `readLocalReport` revalidates the current
|
|
36861
|
+
// regular-file bytes and strips every local path and every `content` before this
|
|
36862
|
+
// leaves the machine.
|
|
36689
36863
|
legacy_memory: local?.legacy_memory ?? [],
|
|
36690
36864
|
// §B.4: counts of server-named secret paths inside / allowlisted /
|
|
36691
36865
|
// outside the secrets directory, and the runtime's mode. Numbers and
|
|
@@ -37942,6 +38116,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37942
38116
|
const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
|
|
37943
38117
|
const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(body.text);
|
|
37944
38118
|
if (opts.inReplyTo !== void 0 && this.isOwnerVerifyFailed(opts.inReplyTo)) return;
|
|
38119
|
+
if (body.kind === "text" && opts.callBufferReply === true && opts.inReplyTo !== void 0 && !this.spokenReplyIds.has(id) && !isBridgeMemoryLine(body.text) && this.callBuffer.isVoiceScopedId(opts.inReplyTo)) {
|
|
38120
|
+
this.callBuffer.appendAgent(channelId, body.text, "reply");
|
|
38121
|
+
}
|
|
37945
38122
|
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";
|
|
37946
38123
|
if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, body.text, id, modality, purgeEpoch, origin);
|
|
37947
38124
|
if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, body.text, id, replyCapture ?? null, origin);
|
|
@@ -38712,6 +38889,27 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38712
38889
|
mayReadHistoryForTurn(messageId, authorization) {
|
|
38713
38890
|
return !this.isOwnerVerifyFailed(messageId) && this.mayReadOpenClawMemoryForTurn(authorization);
|
|
38714
38891
|
}
|
|
38892
|
+
/**
|
|
38893
|
+
* Voice fast-path privacy (spec §2.1, OQ-1 ruled): the ONLY history source of a voice-scoped turn, the call buffer.
|
|
38894
|
+
* Never reads the memory provider, the store, the unified store or the 7b index. A-6: called once per turn, at ingest
|
|
38895
|
+
* (takeVoiceSnapshot), after the reset signals and before the turn's own append.
|
|
38896
|
+
*/
|
|
38897
|
+
voiceScopedHistory(channelId, limit, upTo) {
|
|
38898
|
+
return this.callBuffer.recent(channelId, limit, upTo);
|
|
38899
|
+
}
|
|
38900
|
+
/**
|
|
38901
|
+
* Task 4b (A-6): the turn's snapshot, taken BEFORE its own text is appended (audit finding 2). Final fix I-1: a turn
|
|
38902
|
+
* already admitted to this call (a redelivered row) is snapshotted up to just before its own entry, as it was the
|
|
38903
|
+
* first time, so its snapshot never contains itself.
|
|
38904
|
+
*/
|
|
38905
|
+
takeVoiceSnapshot(channelId, messageId) {
|
|
38906
|
+
const upTo = this.callBuffer.admittedAt(messageId);
|
|
38907
|
+
return {
|
|
38908
|
+
recent: this.voiceScopedHistory(channelId, _BridgeAgent.TURN_MEMORY_LIMIT, upTo),
|
|
38909
|
+
endSeq: upTo ?? this.callBuffer.seqAt(channelId),
|
|
38910
|
+
generation: this.callBuffer.generation(channelId)
|
|
38911
|
+
};
|
|
38912
|
+
}
|
|
38715
38913
|
/** An inbound turn may receive OpenClaw memory only from an active owner DM. */
|
|
38716
38914
|
mayReadOpenClawMemoryForTurn(authorization) {
|
|
38717
38915
|
return this.cfg.memoryProvider !== "openclaw" || authorization.sender_membership === "active" && authorization.audience === "owner_dm";
|
|
@@ -38721,16 +38919,26 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38721
38919
|
* stays owner-DM-only; every provider failure degrades to empty context and
|
|
38722
38920
|
* never falls back to the runtime's retired plaintext JSON files.
|
|
38723
38921
|
*/
|
|
38724
|
-
async memoryForTurn(channelId, messageId, authorization, limit = 50) {
|
|
38922
|
+
async memoryForTurn(channelId, messageId, authorization, limit = 50, voice) {
|
|
38725
38923
|
if (!this.mayReadHistoryForTurn(messageId, authorization)) {
|
|
38726
38924
|
return { recent: [], facts: [] };
|
|
38727
38925
|
}
|
|
38926
|
+
if (voice) return { recent: this.voiceFrameTurns(voice.snapshot?.recent ?? []), facts: [] };
|
|
38728
38927
|
try {
|
|
38729
38928
|
return this.fenceMemory(channelId, await this.memory.context(channelId, limit), holdsManifestGrant(authorization));
|
|
38730
38929
|
} catch {
|
|
38731
38930
|
return { recent: [], facts: [] };
|
|
38732
38931
|
}
|
|
38733
38932
|
}
|
|
38933
|
+
/**
|
|
38934
|
+
* Task 6 (spec §2.2 A-1, N1): call-buffer entries as runtime-frame turns. Under the voice fence (a pin is loaded) the
|
|
38935
|
+
* fast model's own lines (origin "fast_path") are dropped, as fenceMemory drops them from store history; the fast
|
|
38936
|
+
* prompt keeps them. The internal origin is not carried into the frame; `seq` stays 0 (A-5).
|
|
38937
|
+
*/
|
|
38938
|
+
voiceFrameTurns(entries) {
|
|
38939
|
+
const fenced = this.voiceFenceOn();
|
|
38940
|
+
return entries.filter((e) => !(fenced && e.origin === "fast_path")).map((e) => ({ seq: e.seq, role: e.role, text: e.text }));
|
|
38941
|
+
}
|
|
38734
38942
|
/**
|
|
38735
38943
|
* 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
|
|
38736
38944
|
* turn (manifest, fallback, greeting, relayed, constants-only alike). It never depends on a turn's grants.
|
|
@@ -38765,7 +38973,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38765
38973
|
if (!strict && !ctx.recent.some((t) => t.role === "agent")) return ctx;
|
|
38766
38974
|
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
38767
38975
|
const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
|
|
38768
|
-
const meta = securecoms ? this.store.memoryLineMeta(channelId, ctx.recent.filter((t) => strict || t.role === "agent").map((t) => t.seq)) : /* @__PURE__ */ new Map();
|
|
38976
|
+
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();
|
|
38769
38977
|
const fence = this.fenceContext(channelId);
|
|
38770
38978
|
const recent = ctx.recent.filter((t) => {
|
|
38771
38979
|
const m = meta.get(t.seq);
|
|
@@ -38784,7 +38992,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38784
38992
|
if (recent.length === 0) return false;
|
|
38785
38993
|
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
38786
38994
|
const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
|
|
38787
|
-
const meta = securecoms ? this.store.memoryLineMeta(m.channel_id, recent.filter((t) => t.role !== "agent").map((t) => t.seq)) : /* @__PURE__ */ new Map();
|
|
38995
|
+
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();
|
|
38788
38996
|
return recent.some((t) => t.role !== "agent" && isRelayClassPeer({ peerId: t.peerId, modality: meta.get(t.seq)?.modality }, ownerId));
|
|
38789
38997
|
}
|
|
38790
38998
|
/**
|
|
@@ -39080,9 +39288,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39080
39288
|
writeServiceLog2("warn", "memory.recall_prepare_failed", {}, e);
|
|
39081
39289
|
}
|
|
39082
39290
|
}
|
|
39291
|
+
/**
|
|
39292
|
+
* Voice fast-path privacy, Task 7 (spec §4): the frame-keyed delivery check. A voice-scoped frame gets no notes, no
|
|
39293
|
+
* recall and no unified items, whatever its authorization says. Keys on the frame's own marks, NEVER on the call
|
|
39294
|
+
* buffer's id set (a reset evicts ids; the staged frame must stay scoped).
|
|
39295
|
+
*/
|
|
39296
|
+
frameIsVoiceScoped(m) {
|
|
39297
|
+
return m.voice_scoped === true || m.transcribed === true || m.speaker !== void 0;
|
|
39298
|
+
}
|
|
39083
39299
|
/** ws flush (synchronous). Q1 to Q3 and Q6 again at delivery, on the ORIGINAL message; Q4 lives in withConsumerRecall. */
|
|
39084
39300
|
recallForDelivery(m) {
|
|
39085
39301
|
if (this.unifiedOn) return void 0;
|
|
39302
|
+
if (this.frameIsVoiceScoped(m)) return void 0;
|
|
39086
39303
|
if (!this.recallProviderOn() || !this.notesReadable(m.authorization)) return void 0;
|
|
39087
39304
|
return this.recall.deliverable(m.message_id, m.channel_id);
|
|
39088
39305
|
}
|
|
@@ -39315,6 +39532,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39315
39532
|
/** ws flush (synchronous). The predicate and the owner scope again at delivery, on the ORIGINAL message (H-4, Review Focus 3). */
|
|
39316
39533
|
unifiedForDelivery(m) {
|
|
39317
39534
|
if (!this.unifiedProviderOn()) return void 0;
|
|
39535
|
+
if (this.frameIsVoiceScoped(m)) return void 0;
|
|
39318
39536
|
const a = m.authorization;
|
|
39319
39537
|
if (!(a?.memory_write === true || a?.relay_speaker === "owner")) return void 0;
|
|
39320
39538
|
const scopeId = this.ownerScopeNow(m.channel_id);
|
|
@@ -39550,6 +39768,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39550
39768
|
notesForDelivery(m) {
|
|
39551
39769
|
if (this.unifiedOn) return { generation: this.notes.appliedGeneration() };
|
|
39552
39770
|
const generation = this.notes.appliedGeneration();
|
|
39771
|
+
if (this.frameIsVoiceScoped(m)) return { generation };
|
|
39553
39772
|
if (!this.notesReadable(m.authorization)) return { generation };
|
|
39554
39773
|
const d = this.notes.deliverable(m.channel_id);
|
|
39555
39774
|
if (d.miss) {
|
|
@@ -39909,7 +40128,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39909
40128
|
* deferring fast reply was actually published — F3).
|
|
39910
40129
|
*/
|
|
39911
40130
|
async speakAndSettle(turn, plan, held, heldAfterAck, fastText, guidance, fastSpeaker, greeting) {
|
|
39912
|
-
const outcome = plan.kind === "ack" ? await this.sendFastLine(turn
|
|
40131
|
+
const outcome = plan.kind === "ack" ? await this.sendFastLine(turn, plan.line) ? "deferred" : "failed" : await this.runFastReply(turn, fastText, guidance, fastSpeaker, greeting);
|
|
39913
40132
|
if (outcome === "answered") await this.settleHeldRuntimeMessage(held, false);
|
|
39914
40133
|
else await this.settleHeldRuntimeMessage(outcome === "deferred" ? heldAfterAck : held, true);
|
|
39915
40134
|
}
|
|
@@ -39928,19 +40147,29 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39928
40147
|
async settleFastReplies() {
|
|
39929
40148
|
while (this.fastReplyChains.size) await Promise.all([...this.fastReplyChains.values()]);
|
|
39930
40149
|
}
|
|
39931
|
-
/**
|
|
39932
|
-
|
|
40150
|
+
/**
|
|
40151
|
+
* Send a line the voice fast path speaks: its memory record is "voice" (H1).
|
|
40152
|
+
* Task 4b (spec §2.1 agent appends (1), A-1, A-2): once published, the line enters the call buffer as "fast_path",
|
|
40153
|
+
* unless the turn it answers failed owner verification (its id, or any fragment of a coalesced turn). A line answering
|
|
40154
|
+
* a turn the ingest did not find voice-scoped is not appended either (defense in depth: the fast path fires only on
|
|
40155
|
+
* voice-scoped turns, Task 5).
|
|
40156
|
+
*/
|
|
40157
|
+
async sendSpoken(channelId, text, messageId, turn) {
|
|
39933
40158
|
this.spokenReplyIds.add(messageId);
|
|
39934
40159
|
try {
|
|
39935
40160
|
await this.send(channelId, text, messageId);
|
|
39936
40161
|
} finally {
|
|
39937
40162
|
this.spokenReplyIds.delete(messageId);
|
|
39938
40163
|
}
|
|
40164
|
+
if (turn.voiceScoped === true && turn.anyFragmentVerifyFailed !== true && !this.isOwnerVerifyFailed(turn.messageId)) {
|
|
40165
|
+
this.callBuffer.appendAgent(channelId, text, "fast_path");
|
|
40166
|
+
}
|
|
39939
40167
|
}
|
|
39940
40168
|
/** Speak a fixed acknowledgement line; failures are counted, never thrown. */
|
|
39941
|
-
async sendFastLine(
|
|
40169
|
+
async sendFastLine(turn, line) {
|
|
40170
|
+
const { channelId, messageId } = turn;
|
|
39942
40171
|
try {
|
|
39943
|
-
await this.sendSpoken(channelId, line, fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, messageId));
|
|
40172
|
+
await this.sendSpoken(channelId, line, fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, messageId), turn);
|
|
39944
40173
|
this.fastPathSent++;
|
|
39945
40174
|
return true;
|
|
39946
40175
|
} catch (e) {
|
|
@@ -39954,8 +40183,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39954
40183
|
* the fast model's reply — or, in `ack_only`, the greeting template — is
|
|
39955
40184
|
* spoken, and the outcome decides whether the runtime answers next.
|
|
39956
40185
|
*
|
|
39957
|
-
* MUST NEVER throw: every failure path (
|
|
39958
|
-
*
|
|
40186
|
+
* MUST NEVER throw: every failure path (the LLM call, normalization,
|
|
40187
|
+
* or the send itself) is caught, counted in
|
|
39959
40188
|
* `fastPathFailed`, and logged WITHOUT the turn text or the model's reply.
|
|
39960
40189
|
* The `fastReplyId(...)` send id is deterministic, so a duplicate that
|
|
39961
40190
|
* slipped past the in-memory dedupe (e.g. a restart) resolves to the same
|
|
@@ -39969,20 +40198,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39969
40198
|
try {
|
|
39970
40199
|
if ((this.cfg.voiceFastPathMode ?? "full") === "ack_only") {
|
|
39971
40200
|
if (!isGreeting) return "failed";
|
|
39972
|
-
const ok = await this.sendFastLine(m
|
|
40201
|
+
const ok = await this.sendFastLine(m, ackOnlyGreeting(this.voiceAgentName(), speaker?.displayName));
|
|
39973
40202
|
return ok ? "answered" : "failed";
|
|
39974
40203
|
}
|
|
39975
40204
|
const fastLlm = this.fastLlm;
|
|
39976
40205
|
if (!fastLlm || !this.cfg.voice) return "failed";
|
|
39977
|
-
let memory;
|
|
39978
|
-
|
|
39979
|
-
|
|
39980
|
-
|
|
39981
|
-
|
|
39982
|
-
|
|
39983
|
-
}
|
|
39984
|
-
memory = { recent: [], facts: [] };
|
|
39985
|
-
}
|
|
40206
|
+
let memory = { recent: [], facts: [] };
|
|
40207
|
+
const snapshot = m.voiceSnapshot;
|
|
40208
|
+
if (m.voiceScoped === true && snapshot !== void 0 && m.anyFragmentVerifyFailed !== true && this.mayReadHistoryForTurn(m.messageId, m.authorization)) {
|
|
40209
|
+
memory = {
|
|
40210
|
+
recent: [...snapshot.recent, ...this.callBuffer.agentAfter(m.channelId, { seq: snapshot.endSeq, generation: snapshot.generation })].slice(-FAST_PATH_MEMORY_TURNS),
|
|
40211
|
+
facts: []
|
|
40212
|
+
};
|
|
39986
40213
|
}
|
|
39987
40214
|
const messages = buildFastMessages({
|
|
39988
40215
|
persona: this.cfg.voice.persona,
|
|
@@ -40005,7 +40232,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40005
40232
|
const refusal = !isGreeting && isRefusal(split3.reply);
|
|
40006
40233
|
const defer = split3.defer || refusal;
|
|
40007
40234
|
const reply = refusal ? deferAck(m.messageId) : split3.reply;
|
|
40008
|
-
await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId));
|
|
40235
|
+
await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId), m);
|
|
40009
40236
|
this.fastPathSent++;
|
|
40010
40237
|
if (defer) this.fastPathDeferred++;
|
|
40011
40238
|
if (refusal) this.fastPathRefusalDeferred++;
|
|
@@ -40069,6 +40296,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40069
40296
|
async memoryAtRelease(message) {
|
|
40070
40297
|
const prior = message.memory;
|
|
40071
40298
|
if (!prior || !this.mayReadHistoryForTurn(message.message_id, message.authorization)) return prior;
|
|
40299
|
+
if (this.frameIsVoiceScoped(message)) {
|
|
40300
|
+
const tails = this.voiceCoalescedTails.get(message.message_id) ?? [];
|
|
40301
|
+
if (tails.some((id) => this.isOwnerVerifyFailed(id))) return prior;
|
|
40302
|
+
const noted = this.callBuffer.seqNoted(message.message_id);
|
|
40303
|
+
if (noted === void 0) return prior;
|
|
40304
|
+
const spoken2 = this.voiceFrameTurns(this.callBuffer.agentAfter(message.channel_id, noted));
|
|
40305
|
+
if (spoken2.length === 0) return prior;
|
|
40306
|
+
return { recent: [...prior.recent, ...spoken2].slice(-_BridgeAgent.TURN_MEMORY_LIMIT), facts: [] };
|
|
40307
|
+
}
|
|
40072
40308
|
let fresh;
|
|
40073
40309
|
try {
|
|
40074
40310
|
fresh = await this.memory.context(message.channel_id, _BridgeAgent.TURN_MEMORY_LIMIT);
|
|
@@ -40580,9 +40816,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40580
40816
|
});
|
|
40581
40817
|
}
|
|
40582
40818
|
/** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
|
|
40583
|
-
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId) {
|
|
40819
|
+
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId, voiceScoped) {
|
|
40584
40820
|
const held = await this.heldPosition(channelId);
|
|
40585
|
-
const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict });
|
|
40821
|
+
const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict, voiceScoped });
|
|
40586
40822
|
if (!(auth.audience === "team_room" && auth.tool_grants.length > 0)) return auth;
|
|
40587
40823
|
if (this.roomGrantEpochs.size >= ROOM_GRANT_TRACK_MAX || held.epoch === null || held.groupId === null) {
|
|
40588
40824
|
writeServiceLog2("warn", "room_grant.untracked");
|
|
@@ -40597,17 +40833,22 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40597
40833
|
* guess. The returned object and grant array are frozen before they cross the
|
|
40598
40834
|
* daemon boundary so one consumer cannot widen a later execution check.
|
|
40599
40835
|
*
|
|
40600
|
-
* Grants (architect ruling 2026-10-02):
|
|
40601
|
-
* the kill switch is on, the channel is a DM, and EITHER
|
|
40836
|
+
* Grants (architect ruling 2026-10-02; P-5 and G-1, spec §9): an owner-DM
|
|
40837
|
+
* constant only when the kill switch is on, the channel is a DM, and EITHER
|
|
40602
40838
|
* (a) audience is owner_dm AND the sender is the channel's pinned DM
|
|
40603
40839
|
* counterpart (`channelOwner`, never "anomalous"). A server-marked
|
|
40604
40840
|
* `is_agent_owner` on a member who is NOT the DM's single human
|
|
40605
40841
|
* counterpart gets nothing; on the counterpart it is trusted (see
|
|
40606
|
-
* LIMIT below);
|
|
40842
|
+
* LIMIT below). OWNER_DM_DEFAULT_READ_GRANTS on a typed turn;
|
|
40843
|
+
* RELAYED_VOICE_GRANTS and no memory_write when `opts.voiceScoped`
|
|
40844
|
+
* (G-1: the ingest predicate isVoiceScopedTurn, spec §1); OR
|
|
40607
40845
|
* (b) the sender is this channel's server-marked relay (`is_relay`) and the
|
|
40608
40846
|
* relayed `speaker_identity_id` (passed by the caller only for relay
|
|
40609
40847
|
* turns) is that same pinned owner, whose roster row is the agent's
|
|
40610
|
-
* owner. A speaker claim from any non-relay
|
|
40848
|
+
* owner: RELAYED_VOICE_GRANTS. A speaker claim from any non-relay
|
|
40849
|
+
* sender gets nothing.
|
|
40850
|
+
* RELAYED_VOICE_GRANTS is never manifest-eligible: effectiveToolGrants gives
|
|
40851
|
+
* the constant by reference, or the fallback when a good manifest is held.
|
|
40611
40852
|
* Both need the roster fetched at `heldEpoch` (the agent's current MLS
|
|
40612
40853
|
* epoch for the channel), or the DM marked epoch-fresh at this ingest
|
|
40613
40854
|
* cycle's refresh (audit M-B4).
|
|
@@ -40651,9 +40892,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40651
40892
|
const ownerDirect = audience === "owner_dm" && senderId === ownerId;
|
|
40652
40893
|
const owner = roster?.get(ownerId);
|
|
40653
40894
|
const ownerRelayed = relaySpeakerId !== void 0 && sender.is_relay && relaySpeakerId === ownerId && owner !== void 0 && !owner.is_agent && owner.is_agent_owner;
|
|
40654
|
-
if (ownerDirect
|
|
40895
|
+
if (ownerDirect) tool_grants = opts.voiceScoped === true ? RELAYED_VOICE_GRANTS : OWNER_DM_DEFAULT_READ_GRANTS;
|
|
40896
|
+
if (ownerRelayed) tool_grants = RELAYED_VOICE_GRANTS;
|
|
40655
40897
|
if (ownerRelayed) relaySpeaker = "owner";
|
|
40656
|
-
if (ownerDirect && relaySpeakerId === void 0 &&
|
|
40898
|
+
if (ownerDirect && relaySpeakerId === void 0 && // G-1: never on a voice-scoped turn.
|
|
40899
|
+
opts.voiceScoped !== true && (this.cfg.memoryProvider ?? "securecoms") === "securecoms" && // Final review M-4: the live payload flag too, so with notes off the
|
|
40657
40900
|
// runtime does not offer memory_note at all. Not memoryConfigConfirmed:
|
|
40658
40901
|
// a turn the boot drain pulls before the boot refetch keeps its mark,
|
|
40659
40902
|
// and the read and write gates (notesProviderOn) hold it until then.
|
|
@@ -40670,11 +40913,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40670
40913
|
const binding = this.manifestBinding;
|
|
40671
40914
|
const boundNow = binding !== null && binding.state === "bound" && binding.counter === this.toolGrantCounter;
|
|
40672
40915
|
const signedTargets = boundNow ? binding.signedTargets : { endpoints: 0, databases: 0 };
|
|
40673
|
-
if (tool_grants === OWNER_DM_DEFAULT_READ_GRANTS) {
|
|
40674
|
-
const eligible = relaySpeakerId === void 0 && opts.ownerVerdict?.kind === "verified" && manifest?.owner_dm_channel_id === channelId && boundNow && this.manifestConsumer !== null;
|
|
40916
|
+
if (tool_grants === OWNER_DM_DEFAULT_READ_GRANTS || tool_grants === RELAYED_VOICE_GRANTS) {
|
|
40917
|
+
const eligible = relaySpeakerId === void 0 && opts.ownerVerdict?.kind === "verified" && manifest?.owner_dm_channel_id === channelId && boundNow && this.manifestConsumer !== null && tool_grants !== RELAYED_VOICE_GRANTS;
|
|
40675
40918
|
const r = effectiveToolGrants({
|
|
40676
40919
|
audience: "owner_dm",
|
|
40677
|
-
base:
|
|
40920
|
+
base: tool_grants,
|
|
40678
40921
|
manifest,
|
|
40679
40922
|
eligible,
|
|
40680
40923
|
manifestSwitchOn: this.ownerDmManifestGrants,
|
|
@@ -41396,6 +41639,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41396
41639
|
const acked = [];
|
|
41397
41640
|
for (const m of entries) {
|
|
41398
41641
|
try {
|
|
41642
|
+
if (m.voiceMode === false) this.callBuffer.reset(m.channelId);
|
|
41399
41643
|
const encId = m.envelope.enclave_id;
|
|
41400
41644
|
if (encId && !this.channelEnclaveRecorded.has(m.channelId)) {
|
|
41401
41645
|
this.channelEnclaveRecorded.add(m.channelId);
|
|
@@ -41553,6 +41797,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41553
41797
|
continue;
|
|
41554
41798
|
}
|
|
41555
41799
|
if (typeof parsed.text === "string") {
|
|
41800
|
+
if (parsed.text.trim() === VOICE_CALL_START_MARKER) this.callBuffer.reset(m.channelId);
|
|
41556
41801
|
const meta = this.freshMeta(m.channelId);
|
|
41557
41802
|
const senderType = m.envelope.sender_type;
|
|
41558
41803
|
const senderIsAgent = senderType === "agent" ? true : senderType === "human" ? false : void 0;
|
|
@@ -41594,23 +41839,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41594
41839
|
const senderName = this.freshRoster(m.channelId)?.get(
|
|
41595
41840
|
m.envelope.sender_id
|
|
41596
41841
|
)?.name;
|
|
41842
|
+
const voiceScoped = isVoiceScopedTurn({
|
|
41843
|
+
speaker,
|
|
41844
|
+
transcribed: parsed.transcribed,
|
|
41845
|
+
senderIsRelay: this.freshRoster(m.channelId)?.get(m.envelope.sender_id)?.is_relay === true,
|
|
41846
|
+
text: parsed.text
|
|
41847
|
+
});
|
|
41597
41848
|
const ownerVerdict = await this.verifyOwnerTurn(m);
|
|
41598
41849
|
const turnAuthorization = await this.authorizeTurn(
|
|
41599
41850
|
m.channelId,
|
|
41600
41851
|
m.messageId,
|
|
41601
41852
|
m.envelope.sender_id,
|
|
41602
41853
|
ownerVerdict,
|
|
41603
|
-
speaker?.identityId
|
|
41854
|
+
speaker?.identityId,
|
|
41855
|
+
voiceScoped
|
|
41604
41856
|
);
|
|
41605
41857
|
await this.prepareNotesForTurn(m.channelId, turnAuthorization);
|
|
41606
|
-
const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization);
|
|
41607
41858
|
const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
|
|
41608
41859
|
const humanTurn = senderIsAgent !== true || speaker !== void 0;
|
|
41609
|
-
const fastPathEligible = Boolean(m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
41860
|
+
const fastPathEligible = Boolean(voiceScoped && m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
41610
41861
|
const turnText = parsed.text;
|
|
41611
41862
|
const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
|
|
41612
41863
|
const windowEligible = voiceWindowEligible({ voiceMode: m.voiceMode, speaker, transcribed: parsed.transcribed, voiceGuidance, isCallStart });
|
|
41613
41864
|
const coalescible = this.voiceCoalesceWindowMs > 0 && windowEligible;
|
|
41865
|
+
const callPeer = admitsToCallBuffer({ speaker, transcribed: parsed.transcribed });
|
|
41866
|
+
let voiceSnapshot;
|
|
41867
|
+
if (voiceScoped) {
|
|
41868
|
+
this.callBuffer.markVoiceScoped(m.channelId, m.messageId);
|
|
41869
|
+
if (!coalescible) {
|
|
41870
|
+
voiceSnapshot = this.takeVoiceSnapshot(m.channelId, m.messageId);
|
|
41871
|
+
if (callPeer && !this.isOwnerVerifyFailed(m.messageId)) this.callBuffer.appendPeer(m.channelId, parsed.text, m.messageId);
|
|
41872
|
+
}
|
|
41873
|
+
}
|
|
41874
|
+
const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization, void 0, voiceScoped ? { snapshot: voiceSnapshot } : void 0);
|
|
41614
41875
|
if (!coalescible) {
|
|
41615
41876
|
const kind2 = m.voiceMode === true || parsed.transcribed === true ? "voice" : "text";
|
|
41616
41877
|
if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId: m.channelId, messageId: m.messageId, text: parsed.text, authorization: turnAuthorization, kind: kind2 });
|
|
@@ -41626,7 +41887,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41626
41887
|
const fastGreeting = isCallStart && fastPathFiring;
|
|
41627
41888
|
const runtimeVoice = (ackSent) => m.voiceMode ? {
|
|
41628
41889
|
voice: true,
|
|
41629
|
-
|
|
41890
|
+
// Task 6 (OQ-3): a voice-scoped frame's guidance ends with VOICE_CALL_CLAUSE, on all three frames
|
|
41891
|
+
// (held, coalesced after-ack, after-ack); a typed turn during a call keeps its guidance.
|
|
41892
|
+
guidance: `${ackSent ? `${voiceGuidance} ${FAST_PATH_SPOKEN_GUIDANCE}` : voiceGuidance}${voiceScoped ? ` ${VOICE_CALL_CLAUSE}` : ""}`
|
|
41630
41893
|
} : {};
|
|
41631
41894
|
const runtimeEntry = (ackSent, memory) => ({
|
|
41632
41895
|
channel_id: m.channelId,
|
|
@@ -41647,6 +41910,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41647
41910
|
// identify: the envelope's own claim (relay or on-device
|
|
41648
41911
|
// transcript) or a relay-attributed turn.
|
|
41649
41912
|
...parsed.transcribed === true || speaker !== void 0 ? { transcribed: true } : {},
|
|
41913
|
+
...voiceScoped ? { voice_scoped: true } : {},
|
|
41650
41914
|
...runtimeVoice(ackSent)
|
|
41651
41915
|
});
|
|
41652
41916
|
if (coalescible) {
|
|
@@ -41676,6 +41940,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41676
41940
|
acked.push(m.queueId);
|
|
41677
41941
|
continue;
|
|
41678
41942
|
}
|
|
41943
|
+
const bufferGeneration = this.callBuffer.generation(m.channelId);
|
|
41679
41944
|
const held = runtimeEntry(false, await runtimeMemory);
|
|
41680
41945
|
if (!await this.stageHeldRuntimeMessage(held)) continue;
|
|
41681
41946
|
this.noteVoiceCoalesced(m.messageId);
|
|
@@ -41686,7 +41951,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41686
41951
|
closed: false,
|
|
41687
41952
|
pending: Promise.resolve(),
|
|
41688
41953
|
purgeEpoch: purgeEpochAtReceipt.get(m.channelId),
|
|
41689
|
-
itemCapture: itemCaptureAtReceipt.get(m.channelId)
|
|
41954
|
+
itemCapture: itemCaptureAtReceipt.get(m.channelId),
|
|
41955
|
+
bufferGeneration
|
|
41690
41956
|
};
|
|
41691
41957
|
this.voiceCoalescing.set(windowKey, win);
|
|
41692
41958
|
const openedAt = Date.now();
|
|
@@ -41702,9 +41968,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41702
41968
|
const [firstId, ...tailIds] = win.ids;
|
|
41703
41969
|
if (tailIds.length) this.voiceCoalescedTails.set(firstId, tailIds);
|
|
41704
41970
|
const text = win.parts.join(" ");
|
|
41971
|
+
const sameCall = this.callBuffer.generation(channelId) === win.bufferGeneration;
|
|
41972
|
+
const anyFragmentVerifyFailed = win.ids.some((id) => this.isOwnerVerifyFailed(id));
|
|
41973
|
+
let windowSnapshot;
|
|
41974
|
+
if (sameCall) {
|
|
41975
|
+
this.callBuffer.markVoiceScoped(channelId, firstId);
|
|
41976
|
+
windowSnapshot = this.takeVoiceSnapshot(channelId, firstId);
|
|
41977
|
+
}
|
|
41978
|
+
if (callPeer && sameCall && !anyFragmentVerifyFailed) this.callBuffer.appendPeer(channelId, text, firstId);
|
|
41705
41979
|
if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
|
|
41706
41980
|
else await this.prepareRecallForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
|
|
41707
|
-
const
|
|
41981
|
+
const windowMemory = await this.memoryForTurn(channelId, firstId, turnAuthorization, void 0, { snapshot: anyFragmentVerifyFailed ? void 0 : windowSnapshot });
|
|
41982
|
+
const merged = { ...held, text, memory: windowMemory };
|
|
41708
41983
|
if (this.unifiedOn) {
|
|
41709
41984
|
const snap = win.itemCapture ?? { ...this.store.itemCapture(channelId), purgeEpoch: -1 };
|
|
41710
41985
|
const work = this.tapUnifiedVoice(merged, { stamp: this.ingestGeneration.get(firstId), capture: { ...snap, purgeEpoch: win.purgeEpoch ?? -1 } });
|
|
@@ -41732,7 +42007,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41732
42007
|
const release = this.reserveFastReply(channelId);
|
|
41733
42008
|
try {
|
|
41734
42009
|
await this.speakAndSettle(
|
|
41735
|
-
{ channelId, messageId: firstId, senderId, authorization: turnAuthorization },
|
|
42010
|
+
{ channelId, messageId: firstId, senderId, authorization: turnAuthorization, voiceScoped: voiceScoped && sameCall, voiceSnapshot: windowSnapshot, anyFragmentVerifyFailed },
|
|
41736
42011
|
plan,
|
|
41737
42012
|
merged,
|
|
41738
42013
|
{ ...merged, ...runtimeVoice(true) },
|
|
@@ -41764,7 +42039,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41764
42039
|
channelId: m.channelId,
|
|
41765
42040
|
messageId: m.messageId,
|
|
41766
42041
|
senderId: m.envelope.sender_id,
|
|
41767
|
-
authorization: turnAuthorization
|
|
42042
|
+
authorization: turnAuthorization,
|
|
42043
|
+
voiceScoped,
|
|
42044
|
+
voiceSnapshot
|
|
41768
42045
|
};
|
|
41769
42046
|
const guidanceForFast = voiceGuidance;
|
|
41770
42047
|
const greeting = fastGreeting;
|
|
@@ -44033,10 +44310,13 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
44033
44310
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
44034
44311
|
return;
|
|
44035
44312
|
}
|
|
44313
|
+
const record = typeof m.in_reply_to === "string" ? inflight.get(m.in_reply_to) : void 0;
|
|
44314
|
+
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);
|
|
44036
44315
|
const sendOpts = {
|
|
44037
44316
|
agentTurn: isAgentTurn(agent, m),
|
|
44038
44317
|
...typeof m.in_reply_to === "string" ? { inReplyTo: m.in_reply_to } : {},
|
|
44039
|
-
...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {}
|
|
44318
|
+
...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {},
|
|
44319
|
+
...callBufferReply ? { callBufferReply: true } : {}
|
|
44040
44320
|
};
|
|
44041
44321
|
if (m.a2a && typeof m.a2a === "object") {
|
|
44042
44322
|
const a = m.a2a;
|