@securecoms/agent-bridge 0.30.3 → 0.31.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 +16 -0
- package/dist/bin.js +323 -44
- package/dist/bridge-agent.d.ts +89 -15
- package/dist/index.js +323 -44
- package/dist/version.d.ts +1 -1
- package/dist/voice/call-buffer.d.ts +118 -0
- package/dist/voice/persona.d.ts +9 -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(),
|
|
@@ -34056,7 +34059,8 @@ var FastLlmClient = class {
|
|
|
34056
34059
|
try {
|
|
34057
34060
|
res = await this.fetchImpl(`${this.cfg.baseUrl}${this.cfg.path ?? "/chat/completions"}`, {
|
|
34058
34061
|
method: "POST",
|
|
34059
|
-
|
|
34062
|
+
// Final fix T8: configured headers first, so they can never displace content-type or the bearer.
|
|
34063
|
+
headers: { ...this.cfg.headers, "content-type": "application/json", ...this.key ? { authorization: `Bearer ${this.key}` } : {} },
|
|
34060
34064
|
body: JSON.stringify({ model: this.cfg.model, messages, max_tokens: this.cfg.maxTokens, temperature: 0.4 }),
|
|
34061
34065
|
signal: ac.signal
|
|
34062
34066
|
});
|
|
@@ -34160,7 +34164,7 @@ function buildFastMessages(args2) {
|
|
|
34160
34164
|
const sp = systemPrompt(args2.persona, args2.settings, args2.agentName);
|
|
34161
34165
|
const head = sp.slice(0, sp.length - CONTINUITY_CLAUSE.length).trimEnd();
|
|
34162
34166
|
const notesLine = args2.notesDeferLine ? ` ${FAST_NOTES_DEFER_LINE}` : "";
|
|
34163
|
-
const system = `${head} ${args2.guidance.trim()}${notesLine} ${FAST_DEFER_RULE} ${
|
|
34167
|
+
const system = `${head} ${args2.guidance.trim()}${notesLine} ${FAST_DEFER_RULE} ${VOICE_CALL_CLAUSE}`;
|
|
34164
34168
|
const recent = args2.memory.recent.map((t) => ({
|
|
34165
34169
|
role: t.role === "agent" ? "assistant" : "user",
|
|
34166
34170
|
content: t.text
|
|
@@ -34197,6 +34201,160 @@ function normalizeFastReply(raw, maxChars = 200, maxSentences = 2) {
|
|
|
34197
34201
|
return sentences.slice(0, maxSentences).join(" ").slice(0, maxChars);
|
|
34198
34202
|
}
|
|
34199
34203
|
|
|
34204
|
+
// src/voice/voice-scope.ts
|
|
34205
|
+
function isVoiceScopedTurn(t) {
|
|
34206
|
+
return t.speaker !== void 0 || t.transcribed === true || t.senderIsRelay || t.text.trim() === VOICE_CALL_START_MARKER;
|
|
34207
|
+
}
|
|
34208
|
+
function admitsToCallBuffer(t) {
|
|
34209
|
+
return t.speaker !== void 0 || t.transcribed === true;
|
|
34210
|
+
}
|
|
34211
|
+
|
|
34212
|
+
// src/voice/call-buffer.ts
|
|
34213
|
+
var CALL_BUFFER_CAP = 50;
|
|
34214
|
+
var CALL_BUFFER_IDLE_MS = 30 * 6e4;
|
|
34215
|
+
var CALL_BUFFER_ID_CAP = 5e3;
|
|
34216
|
+
var CallBuffer = class {
|
|
34217
|
+
constructor(opts) {
|
|
34218
|
+
this.opts = opts;
|
|
34219
|
+
}
|
|
34220
|
+
channels = /* @__PURE__ */ new Map();
|
|
34221
|
+
/** messageId -> channelId, insertion ordered (oldest first) for the cap. */
|
|
34222
|
+
voiceIds = /* @__PURE__ */ new Map();
|
|
34223
|
+
/** messageId -> { channelId, the channel's bufSeq at the mark, the channel's generation then }. */
|
|
34224
|
+
seqNotes = /* @__PURE__ */ new Map();
|
|
34225
|
+
/**
|
|
34226
|
+
* Final fix I-1: messageId -> { channelId, the channel's bufSeq just before the turn's own peer entry }. A row the
|
|
34227
|
+
* backend redelivers (a failed ack, runtime_inbox.full backpressure) is ingested again; its id here means its text is
|
|
34228
|
+
* already in the call, so it is not appended again and its snapshot stops before its own entry (A-6).
|
|
34229
|
+
*/
|
|
34230
|
+
admitted = /* @__PURE__ */ new Map();
|
|
34231
|
+
/** The last bufSeq handed out per channel (kept across resets, so the sequence never runs backwards). */
|
|
34232
|
+
lastSeq = /* @__PURE__ */ new Map();
|
|
34233
|
+
nextSeq = 1;
|
|
34234
|
+
/** Per-channel reset generation (advanced by every reset, the idle rule included). */
|
|
34235
|
+
gens = /* @__PURE__ */ new Map();
|
|
34236
|
+
/**
|
|
34237
|
+
* Append a spoken turn's text, once per turn id (final fix I-1). Returns false (and appends nothing) when the id was
|
|
34238
|
+
* already admitted in this call, or when the buffer is disabled.
|
|
34239
|
+
*/
|
|
34240
|
+
appendPeer(channelId, text, messageId) {
|
|
34241
|
+
if (!this.opts.enabled) return false;
|
|
34242
|
+
if (this.admittedAt(messageId) !== void 0) return false;
|
|
34243
|
+
const before = this.seqAt(channelId);
|
|
34244
|
+
this.append(channelId, { role: "peer", text });
|
|
34245
|
+
this.admitted.set(messageId, { channelId, before });
|
|
34246
|
+
if (this.admitted.size > CALL_BUFFER_ID_CAP) this.admitted.delete(this.admitted.keys().next().value);
|
|
34247
|
+
return true;
|
|
34248
|
+
}
|
|
34249
|
+
/**
|
|
34250
|
+
* Final fix I-1: for a turn already admitted in this call, the channel's bufSeq just before its own entry (the end seq
|
|
34251
|
+
* of its first snapshot); undefined for a turn not admitted (or forgotten by a reset, the idle rule or the id cap).
|
|
34252
|
+
*/
|
|
34253
|
+
admittedAt(messageId) {
|
|
34254
|
+
const a = this.admitted.get(messageId);
|
|
34255
|
+
if (a === void 0) return void 0;
|
|
34256
|
+
this.expireIfIdle(a.channelId);
|
|
34257
|
+
return this.admitted.get(messageId)?.before;
|
|
34258
|
+
}
|
|
34259
|
+
appendAgent(channelId, text, origin) {
|
|
34260
|
+
this.append(channelId, { role: "agent", text, origin });
|
|
34261
|
+
}
|
|
34262
|
+
/**
|
|
34263
|
+
* Record a voice-scoped turn's id (for reply admission only, A-4) and note the channel's seq at this point. Final fix
|
|
34264
|
+
* I-1: a turn already admitted (a redelivered row) keeps the seq of its first admission, so the note never moves past
|
|
34265
|
+
* the turn's own entry.
|
|
34266
|
+
*/
|
|
34267
|
+
markVoiceScoped(channelId, messageId) {
|
|
34268
|
+
if (!this.opts.enabled) return;
|
|
34269
|
+
this.expireIfIdle(channelId);
|
|
34270
|
+
this.voiceIds.delete(messageId);
|
|
34271
|
+
this.voiceIds.set(messageId, channelId);
|
|
34272
|
+
if (this.voiceIds.size > CALL_BUFFER_ID_CAP) this.voiceIds.delete(this.voiceIds.keys().next().value);
|
|
34273
|
+
const seq = this.admittedAt(messageId) ?? this.seqAt(channelId);
|
|
34274
|
+
this.seqNotes.delete(messageId);
|
|
34275
|
+
this.seqNotes.set(messageId, { channelId, seq, generation: this.generation(channelId) });
|
|
34276
|
+
if (this.seqNotes.size > CALL_BUFFER_ID_CAP) this.seqNotes.delete(this.seqNotes.keys().next().value);
|
|
34277
|
+
}
|
|
34278
|
+
/** A-4: read ONLY by the bridge's `afterPublish` reply admission. */
|
|
34279
|
+
isVoiceScopedId(messageId) {
|
|
34280
|
+
const channelId = this.voiceIds.get(messageId);
|
|
34281
|
+
if (channelId === void 0) return false;
|
|
34282
|
+
this.expireIfIdle(channelId);
|
|
34283
|
+
return this.voiceIds.has(messageId);
|
|
34284
|
+
}
|
|
34285
|
+
/** The channel's mark noted when `messageId` was marked (the release re-read), or undefined (e.g. after a restart). */
|
|
34286
|
+
seqNoted(messageId) {
|
|
34287
|
+
const note = this.seqNotes.get(messageId);
|
|
34288
|
+
if (note === void 0) return void 0;
|
|
34289
|
+
this.expireIfIdle(note.channelId);
|
|
34290
|
+
const kept = this.seqNotes.get(messageId);
|
|
34291
|
+
return kept === void 0 ? void 0 : { seq: kept.seq, generation: kept.generation };
|
|
34292
|
+
}
|
|
34293
|
+
/** The bufSeq of the channel's newest entry (0 before any append). A snapshot's end seq. */
|
|
34294
|
+
seqAt(channelId) {
|
|
34295
|
+
this.expireIfIdle(channelId);
|
|
34296
|
+
return this.lastSeq.get(channelId) ?? 0;
|
|
34297
|
+
}
|
|
34298
|
+
/**
|
|
34299
|
+
* The channel's last `limit` entries, oldest first. With `upTo` (final fix I-1, a redelivered turn's re-snapshot),
|
|
34300
|
+
* only entries at or below that bufSeq.
|
|
34301
|
+
*/
|
|
34302
|
+
recent(channelId, limit, upTo) {
|
|
34303
|
+
if (limit <= 0) return [];
|
|
34304
|
+
this.expireIfIdle(channelId);
|
|
34305
|
+
const all = this.channels.get(channelId)?.entries ?? [];
|
|
34306
|
+
const entries = upTo === void 0 ? all : all.filter((e) => e.bufSeq <= upTo);
|
|
34307
|
+
return entries.slice(-limit).map(deliver);
|
|
34308
|
+
}
|
|
34309
|
+
/**
|
|
34310
|
+
* The channel's AGENT entries appended after `since.seq` (lines spoken or answered since a snapshot), oldest first.
|
|
34311
|
+
* Final fix M-4: nothing when the channel has been reset since the mark (a later call's lines).
|
|
34312
|
+
*/
|
|
34313
|
+
agentAfter(channelId, since) {
|
|
34314
|
+
if (this.generation(channelId) !== since.generation) return [];
|
|
34315
|
+
const entries = this.channels.get(channelId)?.entries ?? [];
|
|
34316
|
+
return entries.filter((e) => e.role === "agent" && e.bufSeq > since.seq).map(deliver);
|
|
34317
|
+
}
|
|
34318
|
+
/**
|
|
34319
|
+
* The channel's reset generation: it changes on every reset (explicit or idle) and on nothing else. A caller that
|
|
34320
|
+
* holds text across an await (the coalesce window) compares it to skip an append into a call that has since ended.
|
|
34321
|
+
*/
|
|
34322
|
+
generation(channelId) {
|
|
34323
|
+
this.expireIfIdle(channelId);
|
|
34324
|
+
return this.gens.get(channelId) ?? 0;
|
|
34325
|
+
}
|
|
34326
|
+
/** Clear the channel: its entries, its voice-scoped ids, its seq notes and its admitted ids. Only ever removes. */
|
|
34327
|
+
reset(channelId) {
|
|
34328
|
+
this.gens.set(channelId, (this.gens.get(channelId) ?? 0) + 1);
|
|
34329
|
+
this.channels.delete(channelId);
|
|
34330
|
+
for (const [id, c] of this.voiceIds) if (c === channelId) this.voiceIds.delete(id);
|
|
34331
|
+
for (const [id, n] of this.seqNotes) if (n.channelId === channelId) this.seqNotes.delete(id);
|
|
34332
|
+
for (const [id, a] of this.admitted) if (a.channelId === channelId) this.admitted.delete(id);
|
|
34333
|
+
}
|
|
34334
|
+
append(channelId, e) {
|
|
34335
|
+
if (!this.opts.enabled) return;
|
|
34336
|
+
this.expireIfIdle(channelId);
|
|
34337
|
+
let ch = this.channels.get(channelId);
|
|
34338
|
+
if (ch === void 0) {
|
|
34339
|
+
ch = { entries: [], lastAppend: 0 };
|
|
34340
|
+
this.channels.set(channelId, ch);
|
|
34341
|
+
}
|
|
34342
|
+
const bufSeq = this.nextSeq++;
|
|
34343
|
+
this.lastSeq.set(channelId, bufSeq);
|
|
34344
|
+
ch.entries.push({ ...e, bufSeq });
|
|
34345
|
+
if (ch.entries.length > this.opts.cap) ch.entries.splice(0, ch.entries.length - this.opts.cap);
|
|
34346
|
+
ch.lastAppend = this.opts.now();
|
|
34347
|
+
}
|
|
34348
|
+
/** Reset (c): more than `idleMs` since the channel's last append clears it, checked on every read and append. */
|
|
34349
|
+
expireIfIdle(channelId) {
|
|
34350
|
+
const ch = this.channels.get(channelId);
|
|
34351
|
+
if (ch !== void 0 && this.opts.now() - ch.lastAppend > this.opts.idleMs) this.reset(channelId);
|
|
34352
|
+
}
|
|
34353
|
+
};
|
|
34354
|
+
function deliver(e) {
|
|
34355
|
+
return { role: e.role, text: e.text, seq: 0, ...e.origin ? { origin: e.origin } : {} };
|
|
34356
|
+
}
|
|
34357
|
+
|
|
34200
34358
|
// src/vault.ts
|
|
34201
34359
|
var VaultIntegrityError = class extends Error {
|
|
34202
34360
|
constructor(message, cause) {
|
|
@@ -35397,6 +35555,10 @@ var OWNER_DM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
|
35397
35555
|
var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
35398
35556
|
Object.freeze({ tool: "current_time", mode: "read" })
|
|
35399
35557
|
]);
|
|
35558
|
+
var RELAYED_VOICE_GRANTS = Object.freeze([
|
|
35559
|
+
Object.freeze({ tool: "current_time", mode: "read" }),
|
|
35560
|
+
Object.freeze({ tool: "web_search", mode: "read" })
|
|
35561
|
+
]);
|
|
35400
35562
|
var NO_TOOL_GRANTS = Object.freeze([]);
|
|
35401
35563
|
var WS_CLOSE_SESSION_SUPERSEDED2 = 4409;
|
|
35402
35564
|
var OWNER_REPLAY_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
@@ -35756,6 +35918,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35756
35918
|
workspaceDir: this.cfg.memoryWorkspaceDir,
|
|
35757
35919
|
factsCharBudget: this.cfg.memoryFactsCharBudget
|
|
35758
35920
|
});
|
|
35921
|
+
this.callBuffer = new CallBuffer({
|
|
35922
|
+
cap: CALL_BUFFER_CAP,
|
|
35923
|
+
idleMs: CALL_BUFFER_IDLE_MS,
|
|
35924
|
+
now: () => Date.now(),
|
|
35925
|
+
enabled: (this.cfg.memoryProvider ?? "securecoms") !== "none"
|
|
35926
|
+
});
|
|
35759
35927
|
this.provider = new MlsGroupKeyProvider({
|
|
35760
35928
|
store: this.store,
|
|
35761
35929
|
backend: this.backend,
|
|
@@ -36579,6 +36747,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36579
36747
|
startedAt = 0;
|
|
36580
36748
|
store;
|
|
36581
36749
|
memory;
|
|
36750
|
+
/**
|
|
36751
|
+
* Voice fast-path privacy, Task 4b (spec §2.1, OQ-1 ruled): the current calls' spoken turns, in process only. The
|
|
36752
|
+
* ONLY history a voice-scoped turn sees; read through voiceScopedHistory, once per turn, at ingest.
|
|
36753
|
+
*/
|
|
36754
|
+
callBuffer;
|
|
36582
36755
|
/**
|
|
36583
36756
|
* Markdown memory 7a: the one owner of every note mutation and of the
|
|
36584
36757
|
* delivery cache (spec §6.3, §7.2). One per store, so one per identity.
|
|
@@ -38226,6 +38399,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38226
38399
|
const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
|
|
38227
38400
|
const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(body.text);
|
|
38228
38401
|
if (opts.inReplyTo !== void 0 && this.isOwnerVerifyFailed(opts.inReplyTo)) return;
|
|
38402
|
+
if (body.kind === "text" && opts.callBufferReply === true && opts.inReplyTo !== void 0 && !this.spokenReplyIds.has(id) && !isBridgeMemoryLine(body.text) && this.callBuffer.isVoiceScopedId(opts.inReplyTo)) {
|
|
38403
|
+
this.callBuffer.appendAgent(channelId, body.text, "reply");
|
|
38404
|
+
}
|
|
38229
38405
|
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
38406
|
if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, body.text, id, modality, purgeEpoch, origin);
|
|
38231
38407
|
if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, body.text, id, replyCapture ?? null, origin);
|
|
@@ -38996,6 +39172,27 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38996
39172
|
mayReadHistoryForTurn(messageId, authorization) {
|
|
38997
39173
|
return !this.isOwnerVerifyFailed(messageId) && this.mayReadOpenClawMemoryForTurn(authorization);
|
|
38998
39174
|
}
|
|
39175
|
+
/**
|
|
39176
|
+
* Voice fast-path privacy (spec §2.1, OQ-1 ruled): the ONLY history source of a voice-scoped turn, the call buffer.
|
|
39177
|
+
* Never reads the memory provider, the store, the unified store or the 7b index. A-6: called once per turn, at ingest
|
|
39178
|
+
* (takeVoiceSnapshot), after the reset signals and before the turn's own append.
|
|
39179
|
+
*/
|
|
39180
|
+
voiceScopedHistory(channelId, limit, upTo) {
|
|
39181
|
+
return this.callBuffer.recent(channelId, limit, upTo);
|
|
39182
|
+
}
|
|
39183
|
+
/**
|
|
39184
|
+
* Task 4b (A-6): the turn's snapshot, taken BEFORE its own text is appended (audit finding 2). Final fix I-1: a turn
|
|
39185
|
+
* already admitted to this call (a redelivered row) is snapshotted up to just before its own entry, as it was the
|
|
39186
|
+
* first time, so its snapshot never contains itself.
|
|
39187
|
+
*/
|
|
39188
|
+
takeVoiceSnapshot(channelId, messageId) {
|
|
39189
|
+
const upTo = this.callBuffer.admittedAt(messageId);
|
|
39190
|
+
return {
|
|
39191
|
+
recent: this.voiceScopedHistory(channelId, _BridgeAgent.TURN_MEMORY_LIMIT, upTo),
|
|
39192
|
+
endSeq: upTo ?? this.callBuffer.seqAt(channelId),
|
|
39193
|
+
generation: this.callBuffer.generation(channelId)
|
|
39194
|
+
};
|
|
39195
|
+
}
|
|
38999
39196
|
/** An inbound turn may receive OpenClaw memory only from an active owner DM. */
|
|
39000
39197
|
mayReadOpenClawMemoryForTurn(authorization) {
|
|
39001
39198
|
return this.cfg.memoryProvider !== "openclaw" || authorization.sender_membership === "active" && authorization.audience === "owner_dm";
|
|
@@ -39005,16 +39202,26 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39005
39202
|
* stays owner-DM-only; every provider failure degrades to empty context and
|
|
39006
39203
|
* never falls back to the runtime's retired plaintext JSON files.
|
|
39007
39204
|
*/
|
|
39008
|
-
async memoryForTurn(channelId, messageId, authorization, limit = 50) {
|
|
39205
|
+
async memoryForTurn(channelId, messageId, authorization, limit = 50, voice) {
|
|
39009
39206
|
if (!this.mayReadHistoryForTurn(messageId, authorization)) {
|
|
39010
39207
|
return { recent: [], facts: [] };
|
|
39011
39208
|
}
|
|
39209
|
+
if (voice) return { recent: this.voiceFrameTurns(voice.snapshot?.recent ?? []), facts: [] };
|
|
39012
39210
|
try {
|
|
39013
39211
|
return this.fenceMemory(channelId, await this.memory.context(channelId, limit), holdsManifestGrant(authorization));
|
|
39014
39212
|
} catch {
|
|
39015
39213
|
return { recent: [], facts: [] };
|
|
39016
39214
|
}
|
|
39017
39215
|
}
|
|
39216
|
+
/**
|
|
39217
|
+
* Task 6 (spec §2.2 A-1, N1): call-buffer entries as runtime-frame turns. Under the voice fence (a pin is loaded) the
|
|
39218
|
+
* fast model's own lines (origin "fast_path") are dropped, as fenceMemory drops them from store history; the fast
|
|
39219
|
+
* prompt keeps them. The internal origin is not carried into the frame; `seq` stays 0 (A-5).
|
|
39220
|
+
*/
|
|
39221
|
+
voiceFrameTurns(entries) {
|
|
39222
|
+
const fenced = this.voiceFenceOn();
|
|
39223
|
+
return entries.filter((e) => !(fenced && e.origin === "fast_path")).map((e) => ({ seq: e.seq, role: e.role, text: e.text }));
|
|
39224
|
+
}
|
|
39018
39225
|
/**
|
|
39019
39226
|
* 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
39227
|
* turn (manifest, fallback, greeting, relayed, constants-only alike). It never depends on a turn's grants.
|
|
@@ -39049,7 +39256,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39049
39256
|
if (!strict && !ctx.recent.some((t) => t.role === "agent")) return ctx;
|
|
39050
39257
|
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
39051
39258
|
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();
|
|
39259
|
+
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
39260
|
const fence = this.fenceContext(channelId);
|
|
39054
39261
|
const recent = ctx.recent.filter((t) => {
|
|
39055
39262
|
const m = meta.get(t.seq);
|
|
@@ -39068,7 +39275,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39068
39275
|
if (recent.length === 0) return false;
|
|
39069
39276
|
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
39070
39277
|
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();
|
|
39278
|
+
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
39279
|
return recent.some((t) => t.role !== "agent" && isRelayClassPeer({ peerId: t.peerId, modality: meta.get(t.seq)?.modality }, ownerId));
|
|
39073
39280
|
}
|
|
39074
39281
|
/**
|
|
@@ -39364,9 +39571,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39364
39571
|
writeServiceLog("warn", "memory.recall_prepare_failed", {}, e);
|
|
39365
39572
|
}
|
|
39366
39573
|
}
|
|
39574
|
+
/**
|
|
39575
|
+
* Voice fast-path privacy, Task 7 (spec §4): the frame-keyed delivery check. A voice-scoped frame gets no notes, no
|
|
39576
|
+
* recall and no unified items, whatever its authorization says. Keys on the frame's own marks, NEVER on the call
|
|
39577
|
+
* buffer's id set (a reset evicts ids; the staged frame must stay scoped).
|
|
39578
|
+
*/
|
|
39579
|
+
frameIsVoiceScoped(m) {
|
|
39580
|
+
return m.voice_scoped === true || m.transcribed === true || m.speaker !== void 0;
|
|
39581
|
+
}
|
|
39367
39582
|
/** ws flush (synchronous). Q1 to Q3 and Q6 again at delivery, on the ORIGINAL message; Q4 lives in withConsumerRecall. */
|
|
39368
39583
|
recallForDelivery(m) {
|
|
39369
39584
|
if (this.unifiedOn) return void 0;
|
|
39585
|
+
if (this.frameIsVoiceScoped(m)) return void 0;
|
|
39370
39586
|
if (!this.recallProviderOn() || !this.notesReadable(m.authorization)) return void 0;
|
|
39371
39587
|
return this.recall.deliverable(m.message_id, m.channel_id);
|
|
39372
39588
|
}
|
|
@@ -39599,6 +39815,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39599
39815
|
/** ws flush (synchronous). The predicate and the owner scope again at delivery, on the ORIGINAL message (H-4, Review Focus 3). */
|
|
39600
39816
|
unifiedForDelivery(m) {
|
|
39601
39817
|
if (!this.unifiedProviderOn()) return void 0;
|
|
39818
|
+
if (this.frameIsVoiceScoped(m)) return void 0;
|
|
39602
39819
|
const a = m.authorization;
|
|
39603
39820
|
if (!(a?.memory_write === true || a?.relay_speaker === "owner")) return void 0;
|
|
39604
39821
|
const scopeId = this.ownerScopeNow(m.channel_id);
|
|
@@ -39834,6 +40051,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39834
40051
|
notesForDelivery(m) {
|
|
39835
40052
|
if (this.unifiedOn) return { generation: this.notes.appliedGeneration() };
|
|
39836
40053
|
const generation = this.notes.appliedGeneration();
|
|
40054
|
+
if (this.frameIsVoiceScoped(m)) return { generation };
|
|
39837
40055
|
if (!this.notesReadable(m.authorization)) return { generation };
|
|
39838
40056
|
const d = this.notes.deliverable(m.channel_id);
|
|
39839
40057
|
if (d.miss) {
|
|
@@ -40193,7 +40411,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40193
40411
|
* deferring fast reply was actually published — F3).
|
|
40194
40412
|
*/
|
|
40195
40413
|
async speakAndSettle(turn, plan, held, heldAfterAck, fastText, guidance, fastSpeaker, greeting) {
|
|
40196
|
-
const outcome = plan.kind === "ack" ? await this.sendFastLine(turn
|
|
40414
|
+
const outcome = plan.kind === "ack" ? await this.sendFastLine(turn, plan.line) ? "deferred" : "failed" : await this.runFastReply(turn, fastText, guidance, fastSpeaker, greeting);
|
|
40197
40415
|
if (outcome === "answered") await this.settleHeldRuntimeMessage(held, false);
|
|
40198
40416
|
else await this.settleHeldRuntimeMessage(outcome === "deferred" ? heldAfterAck : held, true);
|
|
40199
40417
|
}
|
|
@@ -40212,19 +40430,29 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40212
40430
|
async settleFastReplies() {
|
|
40213
40431
|
while (this.fastReplyChains.size) await Promise.all([...this.fastReplyChains.values()]);
|
|
40214
40432
|
}
|
|
40215
|
-
/**
|
|
40216
|
-
|
|
40433
|
+
/**
|
|
40434
|
+
* Send a line the voice fast path speaks: its memory record is "voice" (H1).
|
|
40435
|
+
* Task 4b (spec §2.1 agent appends (1), A-1, A-2): once published, the line enters the call buffer as "fast_path",
|
|
40436
|
+
* unless the turn it answers failed owner verification (its id, or any fragment of a coalesced turn). A line answering
|
|
40437
|
+
* a turn the ingest did not find voice-scoped is not appended either (defense in depth: the fast path fires only on
|
|
40438
|
+
* voice-scoped turns, Task 5).
|
|
40439
|
+
*/
|
|
40440
|
+
async sendSpoken(channelId, text, messageId, turn) {
|
|
40217
40441
|
this.spokenReplyIds.add(messageId);
|
|
40218
40442
|
try {
|
|
40219
40443
|
await this.send(channelId, text, messageId);
|
|
40220
40444
|
} finally {
|
|
40221
40445
|
this.spokenReplyIds.delete(messageId);
|
|
40222
40446
|
}
|
|
40447
|
+
if (turn.voiceScoped === true && turn.anyFragmentVerifyFailed !== true && !this.isOwnerVerifyFailed(turn.messageId)) {
|
|
40448
|
+
this.callBuffer.appendAgent(channelId, text, "fast_path");
|
|
40449
|
+
}
|
|
40223
40450
|
}
|
|
40224
40451
|
/** Speak a fixed acknowledgement line; failures are counted, never thrown. */
|
|
40225
|
-
async sendFastLine(
|
|
40452
|
+
async sendFastLine(turn, line) {
|
|
40453
|
+
const { channelId, messageId } = turn;
|
|
40226
40454
|
try {
|
|
40227
|
-
await this.sendSpoken(channelId, line, fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, messageId));
|
|
40455
|
+
await this.sendSpoken(channelId, line, fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, messageId), turn);
|
|
40228
40456
|
this.fastPathSent++;
|
|
40229
40457
|
return true;
|
|
40230
40458
|
} catch (e) {
|
|
@@ -40238,8 +40466,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40238
40466
|
* the fast model's reply — or, in `ack_only`, the greeting template — is
|
|
40239
40467
|
* spoken, and the outcome decides whether the runtime answers next.
|
|
40240
40468
|
*
|
|
40241
|
-
* MUST NEVER throw: every failure path (
|
|
40242
|
-
*
|
|
40469
|
+
* MUST NEVER throw: every failure path (the LLM call, normalization,
|
|
40470
|
+
* or the send itself) is caught, counted in
|
|
40243
40471
|
* `fastPathFailed`, and logged WITHOUT the turn text or the model's reply.
|
|
40244
40472
|
* The `fastReplyId(...)` send id is deterministic, so a duplicate that
|
|
40245
40473
|
* slipped past the in-memory dedupe (e.g. a restart) resolves to the same
|
|
@@ -40253,20 +40481,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40253
40481
|
try {
|
|
40254
40482
|
if ((this.cfg.voiceFastPathMode ?? "full") === "ack_only") {
|
|
40255
40483
|
if (!isGreeting) return "failed";
|
|
40256
|
-
const ok = await this.sendFastLine(m
|
|
40484
|
+
const ok = await this.sendFastLine(m, ackOnlyGreeting(this.voiceAgentName(), speaker?.displayName));
|
|
40257
40485
|
return ok ? "answered" : "failed";
|
|
40258
40486
|
}
|
|
40259
40487
|
const fastLlm = this.fastLlm;
|
|
40260
40488
|
if (!fastLlm || !this.cfg.voice) return "failed";
|
|
40261
|
-
let memory;
|
|
40262
|
-
|
|
40263
|
-
|
|
40264
|
-
|
|
40265
|
-
|
|
40266
|
-
|
|
40267
|
-
}
|
|
40268
|
-
memory = { recent: [], facts: [] };
|
|
40269
|
-
}
|
|
40489
|
+
let memory = { recent: [], facts: [] };
|
|
40490
|
+
const snapshot = m.voiceSnapshot;
|
|
40491
|
+
if (m.voiceScoped === true && snapshot !== void 0 && m.anyFragmentVerifyFailed !== true && this.mayReadHistoryForTurn(m.messageId, m.authorization)) {
|
|
40492
|
+
memory = {
|
|
40493
|
+
recent: [...snapshot.recent, ...this.callBuffer.agentAfter(m.channelId, { seq: snapshot.endSeq, generation: snapshot.generation })].slice(-FAST_PATH_MEMORY_TURNS),
|
|
40494
|
+
facts: []
|
|
40495
|
+
};
|
|
40270
40496
|
}
|
|
40271
40497
|
const messages = buildFastMessages({
|
|
40272
40498
|
persona: this.cfg.voice.persona,
|
|
@@ -40289,7 +40515,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40289
40515
|
const refusal = !isGreeting && isRefusal(split3.reply);
|
|
40290
40516
|
const defer = split3.defer || refusal;
|
|
40291
40517
|
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));
|
|
40518
|
+
await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId), m);
|
|
40293
40519
|
this.fastPathSent++;
|
|
40294
40520
|
if (defer) this.fastPathDeferred++;
|
|
40295
40521
|
if (refusal) this.fastPathRefusalDeferred++;
|
|
@@ -40353,6 +40579,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40353
40579
|
async memoryAtRelease(message) {
|
|
40354
40580
|
const prior = message.memory;
|
|
40355
40581
|
if (!prior || !this.mayReadHistoryForTurn(message.message_id, message.authorization)) return prior;
|
|
40582
|
+
if (this.frameIsVoiceScoped(message)) {
|
|
40583
|
+
const tails = this.voiceCoalescedTails.get(message.message_id) ?? [];
|
|
40584
|
+
if (tails.some((id) => this.isOwnerVerifyFailed(id))) return prior;
|
|
40585
|
+
const noted = this.callBuffer.seqNoted(message.message_id);
|
|
40586
|
+
if (noted === void 0) return prior;
|
|
40587
|
+
const spoken2 = this.voiceFrameTurns(this.callBuffer.agentAfter(message.channel_id, noted));
|
|
40588
|
+
if (spoken2.length === 0) return prior;
|
|
40589
|
+
return { recent: [...prior.recent, ...spoken2].slice(-_BridgeAgent.TURN_MEMORY_LIMIT), facts: [] };
|
|
40590
|
+
}
|
|
40356
40591
|
let fresh;
|
|
40357
40592
|
try {
|
|
40358
40593
|
fresh = await this.memory.context(message.channel_id, _BridgeAgent.TURN_MEMORY_LIMIT);
|
|
@@ -40864,9 +41099,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40864
41099
|
});
|
|
40865
41100
|
}
|
|
40866
41101
|
/** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
|
|
40867
|
-
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId) {
|
|
41102
|
+
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId, voiceScoped) {
|
|
40868
41103
|
const held = await this.heldPosition(channelId);
|
|
40869
|
-
const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict });
|
|
41104
|
+
const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict, voiceScoped });
|
|
40870
41105
|
if (!(auth.audience === "team_room" && auth.tool_grants.length > 0)) return auth;
|
|
40871
41106
|
if (this.roomGrantEpochs.size >= ROOM_GRANT_TRACK_MAX || held.epoch === null || held.groupId === null) {
|
|
40872
41107
|
writeServiceLog("warn", "room_grant.untracked");
|
|
@@ -40881,17 +41116,22 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40881
41116
|
* guess. The returned object and grant array are frozen before they cross the
|
|
40882
41117
|
* daemon boundary so one consumer cannot widen a later execution check.
|
|
40883
41118
|
*
|
|
40884
|
-
* Grants (architect ruling 2026-10-02):
|
|
40885
|
-
* the kill switch is on, the channel is a DM, and EITHER
|
|
41119
|
+
* Grants (architect ruling 2026-10-02; P-5 and G-1, spec §9): an owner-DM
|
|
41120
|
+
* constant only when the kill switch is on, the channel is a DM, and EITHER
|
|
40886
41121
|
* (a) audience is owner_dm AND the sender is the channel's pinned DM
|
|
40887
41122
|
* counterpart (`channelOwner`, never "anomalous"). A server-marked
|
|
40888
41123
|
* `is_agent_owner` on a member who is NOT the DM's single human
|
|
40889
41124
|
* counterpart gets nothing; on the counterpart it is trusted (see
|
|
40890
|
-
* LIMIT below);
|
|
41125
|
+
* LIMIT below). OWNER_DM_DEFAULT_READ_GRANTS on a typed turn;
|
|
41126
|
+
* RELAYED_VOICE_GRANTS and no memory_write when `opts.voiceScoped`
|
|
41127
|
+
* (G-1: the ingest predicate isVoiceScopedTurn, spec §1); OR
|
|
40891
41128
|
* (b) the sender is this channel's server-marked relay (`is_relay`) and the
|
|
40892
41129
|
* relayed `speaker_identity_id` (passed by the caller only for relay
|
|
40893
41130
|
* turns) is that same pinned owner, whose roster row is the agent's
|
|
40894
|
-
* owner. A speaker claim from any non-relay
|
|
41131
|
+
* owner: RELAYED_VOICE_GRANTS. A speaker claim from any non-relay
|
|
41132
|
+
* sender gets nothing.
|
|
41133
|
+
* RELAYED_VOICE_GRANTS is never manifest-eligible: effectiveToolGrants gives
|
|
41134
|
+
* the constant by reference, or the fallback when a good manifest is held.
|
|
40895
41135
|
* Both need the roster fetched at `heldEpoch` (the agent's current MLS
|
|
40896
41136
|
* epoch for the channel), or the DM marked epoch-fresh at this ingest
|
|
40897
41137
|
* cycle's refresh (audit M-B4).
|
|
@@ -40935,9 +41175,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40935
41175
|
const ownerDirect = audience === "owner_dm" && senderId === ownerId;
|
|
40936
41176
|
const owner = roster?.get(ownerId);
|
|
40937
41177
|
const ownerRelayed = relaySpeakerId !== void 0 && sender.is_relay && relaySpeakerId === ownerId && owner !== void 0 && !owner.is_agent && owner.is_agent_owner;
|
|
40938
|
-
if (ownerDirect
|
|
41178
|
+
if (ownerDirect) tool_grants = opts.voiceScoped === true ? RELAYED_VOICE_GRANTS : OWNER_DM_DEFAULT_READ_GRANTS;
|
|
41179
|
+
if (ownerRelayed) tool_grants = RELAYED_VOICE_GRANTS;
|
|
40939
41180
|
if (ownerRelayed) relaySpeaker = "owner";
|
|
40940
|
-
if (ownerDirect && relaySpeakerId === void 0 &&
|
|
41181
|
+
if (ownerDirect && relaySpeakerId === void 0 && // G-1: never on a voice-scoped turn.
|
|
41182
|
+
opts.voiceScoped !== true && (this.cfg.memoryProvider ?? "securecoms") === "securecoms" && // Final review M-4: the live payload flag too, so with notes off the
|
|
40941
41183
|
// runtime does not offer memory_note at all. Not memoryConfigConfirmed:
|
|
40942
41184
|
// a turn the boot drain pulls before the boot refetch keeps its mark,
|
|
40943
41185
|
// and the read and write gates (notesProviderOn) hold it until then.
|
|
@@ -40954,11 +41196,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40954
41196
|
const binding = this.manifestBinding;
|
|
40955
41197
|
const boundNow = binding !== null && binding.state === "bound" && binding.counter === this.toolGrantCounter;
|
|
40956
41198
|
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;
|
|
41199
|
+
if (tool_grants === OWNER_DM_DEFAULT_READ_GRANTS || tool_grants === RELAYED_VOICE_GRANTS) {
|
|
41200
|
+
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
41201
|
const r = effectiveToolGrants({
|
|
40960
41202
|
audience: "owner_dm",
|
|
40961
|
-
base:
|
|
41203
|
+
base: tool_grants,
|
|
40962
41204
|
manifest,
|
|
40963
41205
|
eligible,
|
|
40964
41206
|
manifestSwitchOn: this.ownerDmManifestGrants,
|
|
@@ -41680,6 +41922,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41680
41922
|
const acked = [];
|
|
41681
41923
|
for (const m of entries) {
|
|
41682
41924
|
try {
|
|
41925
|
+
if (m.voiceMode === false) this.callBuffer.reset(m.channelId);
|
|
41683
41926
|
const encId = m.envelope.enclave_id;
|
|
41684
41927
|
if (encId && !this.channelEnclaveRecorded.has(m.channelId)) {
|
|
41685
41928
|
this.channelEnclaveRecorded.add(m.channelId);
|
|
@@ -41837,6 +42080,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41837
42080
|
continue;
|
|
41838
42081
|
}
|
|
41839
42082
|
if (typeof parsed.text === "string") {
|
|
42083
|
+
if (parsed.text.trim() === VOICE_CALL_START_MARKER) this.callBuffer.reset(m.channelId);
|
|
41840
42084
|
const meta = this.freshMeta(m.channelId);
|
|
41841
42085
|
const senderType = m.envelope.sender_type;
|
|
41842
42086
|
const senderIsAgent = senderType === "agent" ? true : senderType === "human" ? false : void 0;
|
|
@@ -41878,23 +42122,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41878
42122
|
const senderName = this.freshRoster(m.channelId)?.get(
|
|
41879
42123
|
m.envelope.sender_id
|
|
41880
42124
|
)?.name;
|
|
42125
|
+
const voiceScoped = isVoiceScopedTurn({
|
|
42126
|
+
speaker,
|
|
42127
|
+
transcribed: parsed.transcribed,
|
|
42128
|
+
senderIsRelay: this.freshRoster(m.channelId)?.get(m.envelope.sender_id)?.is_relay === true,
|
|
42129
|
+
text: parsed.text
|
|
42130
|
+
});
|
|
41881
42131
|
const ownerVerdict = await this.verifyOwnerTurn(m);
|
|
41882
42132
|
const turnAuthorization = await this.authorizeTurn(
|
|
41883
42133
|
m.channelId,
|
|
41884
42134
|
m.messageId,
|
|
41885
42135
|
m.envelope.sender_id,
|
|
41886
42136
|
ownerVerdict,
|
|
41887
|
-
speaker?.identityId
|
|
42137
|
+
speaker?.identityId,
|
|
42138
|
+
voiceScoped
|
|
41888
42139
|
);
|
|
41889
42140
|
await this.prepareNotesForTurn(m.channelId, turnAuthorization);
|
|
41890
|
-
const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization);
|
|
41891
42141
|
const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
|
|
41892
42142
|
const humanTurn = senderIsAgent !== true || speaker !== void 0;
|
|
41893
|
-
const fastPathEligible = Boolean(m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
42143
|
+
const fastPathEligible = Boolean(voiceScoped && m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
41894
42144
|
const turnText = parsed.text;
|
|
41895
42145
|
const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
|
|
41896
42146
|
const windowEligible = voiceWindowEligible({ voiceMode: m.voiceMode, speaker, transcribed: parsed.transcribed, voiceGuidance, isCallStart });
|
|
41897
42147
|
const coalescible = this.voiceCoalesceWindowMs > 0 && windowEligible;
|
|
42148
|
+
const callPeer = admitsToCallBuffer({ speaker, transcribed: parsed.transcribed });
|
|
42149
|
+
let voiceSnapshot;
|
|
42150
|
+
if (voiceScoped) {
|
|
42151
|
+
this.callBuffer.markVoiceScoped(m.channelId, m.messageId);
|
|
42152
|
+
if (!coalescible) {
|
|
42153
|
+
voiceSnapshot = this.takeVoiceSnapshot(m.channelId, m.messageId);
|
|
42154
|
+
if (callPeer && !this.isOwnerVerifyFailed(m.messageId)) this.callBuffer.appendPeer(m.channelId, parsed.text, m.messageId);
|
|
42155
|
+
}
|
|
42156
|
+
}
|
|
42157
|
+
const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization, void 0, voiceScoped ? { snapshot: voiceSnapshot } : void 0);
|
|
41898
42158
|
if (!coalescible) {
|
|
41899
42159
|
const kind2 = m.voiceMode === true || parsed.transcribed === true ? "voice" : "text";
|
|
41900
42160
|
if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId: m.channelId, messageId: m.messageId, text: parsed.text, authorization: turnAuthorization, kind: kind2 });
|
|
@@ -41910,7 +42170,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41910
42170
|
const fastGreeting = isCallStart && fastPathFiring;
|
|
41911
42171
|
const runtimeVoice = (ackSent) => m.voiceMode ? {
|
|
41912
42172
|
voice: true,
|
|
41913
|
-
|
|
42173
|
+
// Task 6 (OQ-3): a voice-scoped frame's guidance ends with VOICE_CALL_CLAUSE, on all three frames
|
|
42174
|
+
// (held, coalesced after-ack, after-ack); a typed turn during a call keeps its guidance.
|
|
42175
|
+
guidance: `${ackSent ? `${voiceGuidance} ${FAST_PATH_SPOKEN_GUIDANCE}` : voiceGuidance}${voiceScoped ? ` ${VOICE_CALL_CLAUSE}` : ""}`
|
|
41914
42176
|
} : {};
|
|
41915
42177
|
const runtimeEntry = (ackSent, memory) => ({
|
|
41916
42178
|
channel_id: m.channelId,
|
|
@@ -41931,6 +42193,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41931
42193
|
// identify: the envelope's own claim (relay or on-device
|
|
41932
42194
|
// transcript) or a relay-attributed turn.
|
|
41933
42195
|
...parsed.transcribed === true || speaker !== void 0 ? { transcribed: true } : {},
|
|
42196
|
+
...voiceScoped ? { voice_scoped: true } : {},
|
|
41934
42197
|
...runtimeVoice(ackSent)
|
|
41935
42198
|
});
|
|
41936
42199
|
if (coalescible) {
|
|
@@ -41960,6 +42223,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41960
42223
|
acked.push(m.queueId);
|
|
41961
42224
|
continue;
|
|
41962
42225
|
}
|
|
42226
|
+
const bufferGeneration = this.callBuffer.generation(m.channelId);
|
|
41963
42227
|
const held = runtimeEntry(false, await runtimeMemory);
|
|
41964
42228
|
if (!await this.stageHeldRuntimeMessage(held)) continue;
|
|
41965
42229
|
this.noteVoiceCoalesced(m.messageId);
|
|
@@ -41970,7 +42234,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41970
42234
|
closed: false,
|
|
41971
42235
|
pending: Promise.resolve(),
|
|
41972
42236
|
purgeEpoch: purgeEpochAtReceipt.get(m.channelId),
|
|
41973
|
-
itemCapture: itemCaptureAtReceipt.get(m.channelId)
|
|
42237
|
+
itemCapture: itemCaptureAtReceipt.get(m.channelId),
|
|
42238
|
+
bufferGeneration
|
|
41974
42239
|
};
|
|
41975
42240
|
this.voiceCoalescing.set(windowKey, win);
|
|
41976
42241
|
const openedAt = Date.now();
|
|
@@ -41986,9 +42251,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41986
42251
|
const [firstId, ...tailIds] = win.ids;
|
|
41987
42252
|
if (tailIds.length) this.voiceCoalescedTails.set(firstId, tailIds);
|
|
41988
42253
|
const text = win.parts.join(" ");
|
|
42254
|
+
const sameCall = this.callBuffer.generation(channelId) === win.bufferGeneration;
|
|
42255
|
+
const anyFragmentVerifyFailed = win.ids.some((id) => this.isOwnerVerifyFailed(id));
|
|
42256
|
+
let windowSnapshot;
|
|
42257
|
+
if (sameCall) {
|
|
42258
|
+
this.callBuffer.markVoiceScoped(channelId, firstId);
|
|
42259
|
+
windowSnapshot = this.takeVoiceSnapshot(channelId, firstId);
|
|
42260
|
+
}
|
|
42261
|
+
if (callPeer && sameCall && !anyFragmentVerifyFailed) this.callBuffer.appendPeer(channelId, text, firstId);
|
|
41989
42262
|
if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
|
|
41990
42263
|
else await this.prepareRecallForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
|
|
41991
|
-
const
|
|
42264
|
+
const windowMemory = await this.memoryForTurn(channelId, firstId, turnAuthorization, void 0, { snapshot: anyFragmentVerifyFailed ? void 0 : windowSnapshot });
|
|
42265
|
+
const merged = { ...held, text, memory: windowMemory };
|
|
41992
42266
|
if (this.unifiedOn) {
|
|
41993
42267
|
const snap = win.itemCapture ?? { ...this.store.itemCapture(channelId), purgeEpoch: -1 };
|
|
41994
42268
|
const work = this.tapUnifiedVoice(merged, { stamp: this.ingestGeneration.get(firstId), capture: { ...snap, purgeEpoch: win.purgeEpoch ?? -1 } });
|
|
@@ -42016,7 +42290,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42016
42290
|
const release = this.reserveFastReply(channelId);
|
|
42017
42291
|
try {
|
|
42018
42292
|
await this.speakAndSettle(
|
|
42019
|
-
{ channelId, messageId: firstId, senderId, authorization: turnAuthorization },
|
|
42293
|
+
{ channelId, messageId: firstId, senderId, authorization: turnAuthorization, voiceScoped: voiceScoped && sameCall, voiceSnapshot: windowSnapshot, anyFragmentVerifyFailed },
|
|
42020
42294
|
plan,
|
|
42021
42295
|
merged,
|
|
42022
42296
|
{ ...merged, ...runtimeVoice(true) },
|
|
@@ -42048,7 +42322,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42048
42322
|
channelId: m.channelId,
|
|
42049
42323
|
messageId: m.messageId,
|
|
42050
42324
|
senderId: m.envelope.sender_id,
|
|
42051
|
-
authorization: turnAuthorization
|
|
42325
|
+
authorization: turnAuthorization,
|
|
42326
|
+
voiceScoped,
|
|
42327
|
+
voiceSnapshot
|
|
42052
42328
|
};
|
|
42053
42329
|
const guidanceForFast = voiceGuidance;
|
|
42054
42330
|
const greeting = fastGreeting;
|
|
@@ -44171,10 +44447,13 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
44171
44447
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
44172
44448
|
return;
|
|
44173
44449
|
}
|
|
44450
|
+
const record = typeof m.in_reply_to === "string" ? inflight.get(m.in_reply_to) : void 0;
|
|
44451
|
+
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
44452
|
const sendOpts = {
|
|
44175
44453
|
agentTurn: isAgentTurn(agent, m),
|
|
44176
44454
|
...typeof m.in_reply_to === "string" ? { inReplyTo: m.in_reply_to } : {},
|
|
44177
|
-
...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {}
|
|
44455
|
+
...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {},
|
|
44456
|
+
...callBufferReply ? { callBufferReply: true } : {}
|
|
44178
44457
|
};
|
|
44179
44458
|
if (m.a2a && typeof m.a2a === "object") {
|
|
44180
44459
|
const a = m.a2a;
|