@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/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(),
|
|
@@ -33723,7 +33727,7 @@ function buildFastMessages(args2) {
|
|
|
33723
33727
|
const sp = systemPrompt(args2.persona, args2.settings, args2.agentName);
|
|
33724
33728
|
const head = sp.slice(0, sp.length - CONTINUITY_CLAUSE.length).trimEnd();
|
|
33725
33729
|
const notesLine = args2.notesDeferLine ? ` ${FAST_NOTES_DEFER_LINE}` : "";
|
|
33726
|
-
const system = `${head} ${args2.guidance.trim()}${notesLine} ${FAST_DEFER_RULE} ${
|
|
33730
|
+
const system = `${head} ${args2.guidance.trim()}${notesLine} ${FAST_DEFER_RULE} ${VOICE_CALL_CLAUSE}`;
|
|
33727
33731
|
const recent = args2.memory.recent.map((t) => ({
|
|
33728
33732
|
role: t.role === "agent" ? "assistant" : "user",
|
|
33729
33733
|
content: t.text
|
|
@@ -33760,6 +33764,160 @@ function normalizeFastReply(raw, maxChars = 200, maxSentences = 2) {
|
|
|
33760
33764
|
return sentences.slice(0, maxSentences).join(" ").slice(0, maxChars);
|
|
33761
33765
|
}
|
|
33762
33766
|
|
|
33767
|
+
// src/voice/voice-scope.ts
|
|
33768
|
+
function isVoiceScopedTurn(t) {
|
|
33769
|
+
return t.speaker !== void 0 || t.transcribed === true || t.senderIsRelay || t.text.trim() === VOICE_CALL_START_MARKER;
|
|
33770
|
+
}
|
|
33771
|
+
function admitsToCallBuffer(t) {
|
|
33772
|
+
return t.speaker !== void 0 || t.transcribed === true;
|
|
33773
|
+
}
|
|
33774
|
+
|
|
33775
|
+
// src/voice/call-buffer.ts
|
|
33776
|
+
var CALL_BUFFER_CAP = 50;
|
|
33777
|
+
var CALL_BUFFER_IDLE_MS = 30 * 6e4;
|
|
33778
|
+
var CALL_BUFFER_ID_CAP = 5e3;
|
|
33779
|
+
var CallBuffer = class {
|
|
33780
|
+
constructor(opts) {
|
|
33781
|
+
this.opts = opts;
|
|
33782
|
+
}
|
|
33783
|
+
channels = /* @__PURE__ */ new Map();
|
|
33784
|
+
/** messageId -> channelId, insertion ordered (oldest first) for the cap. */
|
|
33785
|
+
voiceIds = /* @__PURE__ */ new Map();
|
|
33786
|
+
/** messageId -> { channelId, the channel's bufSeq at the mark, the channel's generation then }. */
|
|
33787
|
+
seqNotes = /* @__PURE__ */ new Map();
|
|
33788
|
+
/**
|
|
33789
|
+
* Final fix I-1: messageId -> { channelId, the channel's bufSeq just before the turn's own peer entry }. A row the
|
|
33790
|
+
* backend redelivers (a failed ack, runtime_inbox.full backpressure) is ingested again; its id here means its text is
|
|
33791
|
+
* already in the call, so it is not appended again and its snapshot stops before its own entry (A-6).
|
|
33792
|
+
*/
|
|
33793
|
+
admitted = /* @__PURE__ */ new Map();
|
|
33794
|
+
/** The last bufSeq handed out per channel (kept across resets, so the sequence never runs backwards). */
|
|
33795
|
+
lastSeq = /* @__PURE__ */ new Map();
|
|
33796
|
+
nextSeq = 1;
|
|
33797
|
+
/** Per-channel reset generation (advanced by every reset, the idle rule included). */
|
|
33798
|
+
gens = /* @__PURE__ */ new Map();
|
|
33799
|
+
/**
|
|
33800
|
+
* Append a spoken turn's text, once per turn id (final fix I-1). Returns false (and appends nothing) when the id was
|
|
33801
|
+
* already admitted in this call, or when the buffer is disabled.
|
|
33802
|
+
*/
|
|
33803
|
+
appendPeer(channelId, text, messageId) {
|
|
33804
|
+
if (!this.opts.enabled) return false;
|
|
33805
|
+
if (this.admittedAt(messageId) !== void 0) return false;
|
|
33806
|
+
const before = this.seqAt(channelId);
|
|
33807
|
+
this.append(channelId, { role: "peer", text });
|
|
33808
|
+
this.admitted.set(messageId, { channelId, before });
|
|
33809
|
+
if (this.admitted.size > CALL_BUFFER_ID_CAP) this.admitted.delete(this.admitted.keys().next().value);
|
|
33810
|
+
return true;
|
|
33811
|
+
}
|
|
33812
|
+
/**
|
|
33813
|
+
* Final fix I-1: for a turn already admitted in this call, the channel's bufSeq just before its own entry (the end seq
|
|
33814
|
+
* of its first snapshot); undefined for a turn not admitted (or forgotten by a reset, the idle rule or the id cap).
|
|
33815
|
+
*/
|
|
33816
|
+
admittedAt(messageId) {
|
|
33817
|
+
const a = this.admitted.get(messageId);
|
|
33818
|
+
if (a === void 0) return void 0;
|
|
33819
|
+
this.expireIfIdle(a.channelId);
|
|
33820
|
+
return this.admitted.get(messageId)?.before;
|
|
33821
|
+
}
|
|
33822
|
+
appendAgent(channelId, text, origin) {
|
|
33823
|
+
this.append(channelId, { role: "agent", text, origin });
|
|
33824
|
+
}
|
|
33825
|
+
/**
|
|
33826
|
+
* Record a voice-scoped turn's id (for reply admission only, A-4) and note the channel's seq at this point. Final fix
|
|
33827
|
+
* I-1: a turn already admitted (a redelivered row) keeps the seq of its first admission, so the note never moves past
|
|
33828
|
+
* the turn's own entry.
|
|
33829
|
+
*/
|
|
33830
|
+
markVoiceScoped(channelId, messageId) {
|
|
33831
|
+
if (!this.opts.enabled) return;
|
|
33832
|
+
this.expireIfIdle(channelId);
|
|
33833
|
+
this.voiceIds.delete(messageId);
|
|
33834
|
+
this.voiceIds.set(messageId, channelId);
|
|
33835
|
+
if (this.voiceIds.size > CALL_BUFFER_ID_CAP) this.voiceIds.delete(this.voiceIds.keys().next().value);
|
|
33836
|
+
const seq = this.admittedAt(messageId) ?? this.seqAt(channelId);
|
|
33837
|
+
this.seqNotes.delete(messageId);
|
|
33838
|
+
this.seqNotes.set(messageId, { channelId, seq, generation: this.generation(channelId) });
|
|
33839
|
+
if (this.seqNotes.size > CALL_BUFFER_ID_CAP) this.seqNotes.delete(this.seqNotes.keys().next().value);
|
|
33840
|
+
}
|
|
33841
|
+
/** A-4: read ONLY by the bridge's `afterPublish` reply admission. */
|
|
33842
|
+
isVoiceScopedId(messageId) {
|
|
33843
|
+
const channelId = this.voiceIds.get(messageId);
|
|
33844
|
+
if (channelId === void 0) return false;
|
|
33845
|
+
this.expireIfIdle(channelId);
|
|
33846
|
+
return this.voiceIds.has(messageId);
|
|
33847
|
+
}
|
|
33848
|
+
/** The channel's mark noted when `messageId` was marked (the release re-read), or undefined (e.g. after a restart). */
|
|
33849
|
+
seqNoted(messageId) {
|
|
33850
|
+
const note = this.seqNotes.get(messageId);
|
|
33851
|
+
if (note === void 0) return void 0;
|
|
33852
|
+
this.expireIfIdle(note.channelId);
|
|
33853
|
+
const kept = this.seqNotes.get(messageId);
|
|
33854
|
+
return kept === void 0 ? void 0 : { seq: kept.seq, generation: kept.generation };
|
|
33855
|
+
}
|
|
33856
|
+
/** The bufSeq of the channel's newest entry (0 before any append). A snapshot's end seq. */
|
|
33857
|
+
seqAt(channelId) {
|
|
33858
|
+
this.expireIfIdle(channelId);
|
|
33859
|
+
return this.lastSeq.get(channelId) ?? 0;
|
|
33860
|
+
}
|
|
33861
|
+
/**
|
|
33862
|
+
* The channel's last `limit` entries, oldest first. With `upTo` (final fix I-1, a redelivered turn's re-snapshot),
|
|
33863
|
+
* only entries at or below that bufSeq.
|
|
33864
|
+
*/
|
|
33865
|
+
recent(channelId, limit, upTo) {
|
|
33866
|
+
if (limit <= 0) return [];
|
|
33867
|
+
this.expireIfIdle(channelId);
|
|
33868
|
+
const all = this.channels.get(channelId)?.entries ?? [];
|
|
33869
|
+
const entries = upTo === void 0 ? all : all.filter((e) => e.bufSeq <= upTo);
|
|
33870
|
+
return entries.slice(-limit).map(deliver);
|
|
33871
|
+
}
|
|
33872
|
+
/**
|
|
33873
|
+
* The channel's AGENT entries appended after `since.seq` (lines spoken or answered since a snapshot), oldest first.
|
|
33874
|
+
* Final fix M-4: nothing when the channel has been reset since the mark (a later call's lines).
|
|
33875
|
+
*/
|
|
33876
|
+
agentAfter(channelId, since) {
|
|
33877
|
+
if (this.generation(channelId) !== since.generation) return [];
|
|
33878
|
+
const entries = this.channels.get(channelId)?.entries ?? [];
|
|
33879
|
+
return entries.filter((e) => e.role === "agent" && e.bufSeq > since.seq).map(deliver);
|
|
33880
|
+
}
|
|
33881
|
+
/**
|
|
33882
|
+
* The channel's reset generation: it changes on every reset (explicit or idle) and on nothing else. A caller that
|
|
33883
|
+
* holds text across an await (the coalesce window) compares it to skip an append into a call that has since ended.
|
|
33884
|
+
*/
|
|
33885
|
+
generation(channelId) {
|
|
33886
|
+
this.expireIfIdle(channelId);
|
|
33887
|
+
return this.gens.get(channelId) ?? 0;
|
|
33888
|
+
}
|
|
33889
|
+
/** Clear the channel: its entries, its voice-scoped ids, its seq notes and its admitted ids. Only ever removes. */
|
|
33890
|
+
reset(channelId) {
|
|
33891
|
+
this.gens.set(channelId, (this.gens.get(channelId) ?? 0) + 1);
|
|
33892
|
+
this.channels.delete(channelId);
|
|
33893
|
+
for (const [id, c] of this.voiceIds) if (c === channelId) this.voiceIds.delete(id);
|
|
33894
|
+
for (const [id, n] of this.seqNotes) if (n.channelId === channelId) this.seqNotes.delete(id);
|
|
33895
|
+
for (const [id, a] of this.admitted) if (a.channelId === channelId) this.admitted.delete(id);
|
|
33896
|
+
}
|
|
33897
|
+
append(channelId, e) {
|
|
33898
|
+
if (!this.opts.enabled) return;
|
|
33899
|
+
this.expireIfIdle(channelId);
|
|
33900
|
+
let ch = this.channels.get(channelId);
|
|
33901
|
+
if (ch === void 0) {
|
|
33902
|
+
ch = { entries: [], lastAppend: 0 };
|
|
33903
|
+
this.channels.set(channelId, ch);
|
|
33904
|
+
}
|
|
33905
|
+
const bufSeq = this.nextSeq++;
|
|
33906
|
+
this.lastSeq.set(channelId, bufSeq);
|
|
33907
|
+
ch.entries.push({ ...e, bufSeq });
|
|
33908
|
+
if (ch.entries.length > this.opts.cap) ch.entries.splice(0, ch.entries.length - this.opts.cap);
|
|
33909
|
+
ch.lastAppend = this.opts.now();
|
|
33910
|
+
}
|
|
33911
|
+
/** Reset (c): more than `idleMs` since the channel's last append clears it, checked on every read and append. */
|
|
33912
|
+
expireIfIdle(channelId) {
|
|
33913
|
+
const ch = this.channels.get(channelId);
|
|
33914
|
+
if (ch !== void 0 && this.opts.now() - ch.lastAppend > this.opts.idleMs) this.reset(channelId);
|
|
33915
|
+
}
|
|
33916
|
+
};
|
|
33917
|
+
function deliver(e) {
|
|
33918
|
+
return { role: e.role, text: e.text, seq: 0, ...e.origin ? { origin: e.origin } : {} };
|
|
33919
|
+
}
|
|
33920
|
+
|
|
33763
33921
|
// src/voice/defer-intent.ts
|
|
33764
33922
|
var NEEDS_RUNTIME = [
|
|
33765
33923
|
// Current time / date / day.
|
|
@@ -35113,6 +35271,10 @@ var OWNER_DM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
|
35113
35271
|
var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
35114
35272
|
Object.freeze({ tool: "current_time", mode: "read" })
|
|
35115
35273
|
]);
|
|
35274
|
+
var RELAYED_VOICE_GRANTS = Object.freeze([
|
|
35275
|
+
Object.freeze({ tool: "current_time", mode: "read" }),
|
|
35276
|
+
Object.freeze({ tool: "web_search", mode: "read" })
|
|
35277
|
+
]);
|
|
35116
35278
|
var NO_TOOL_GRANTS = Object.freeze([]);
|
|
35117
35279
|
var WS_CLOSE_SESSION_SUPERSEDED2 = 4409;
|
|
35118
35280
|
var OWNER_REPLAY_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
@@ -35472,6 +35634,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35472
35634
|
workspaceDir: this.cfg.memoryWorkspaceDir,
|
|
35473
35635
|
factsCharBudget: this.cfg.memoryFactsCharBudget
|
|
35474
35636
|
});
|
|
35637
|
+
this.callBuffer = new CallBuffer({
|
|
35638
|
+
cap: CALL_BUFFER_CAP,
|
|
35639
|
+
idleMs: CALL_BUFFER_IDLE_MS,
|
|
35640
|
+
now: () => Date.now(),
|
|
35641
|
+
enabled: (this.cfg.memoryProvider ?? "securecoms") !== "none"
|
|
35642
|
+
});
|
|
35475
35643
|
this.provider = new MlsGroupKeyProvider({
|
|
35476
35644
|
store: this.store,
|
|
35477
35645
|
backend: this.backend,
|
|
@@ -36295,6 +36463,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36295
36463
|
startedAt = 0;
|
|
36296
36464
|
store;
|
|
36297
36465
|
memory;
|
|
36466
|
+
/**
|
|
36467
|
+
* Voice fast-path privacy, Task 4b (spec §2.1, OQ-1 ruled): the current calls' spoken turns, in process only. The
|
|
36468
|
+
* ONLY history a voice-scoped turn sees; read through voiceScopedHistory, once per turn, at ingest.
|
|
36469
|
+
*/
|
|
36470
|
+
callBuffer;
|
|
36298
36471
|
/**
|
|
36299
36472
|
* Markdown memory 7a: the one owner of every note mutation and of the
|
|
36300
36473
|
* delivery cache (spec §6.3, §7.2). One per store, so one per identity.
|
|
@@ -37942,6 +38115,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37942
38115
|
const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
|
|
37943
38116
|
const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(body.text);
|
|
37944
38117
|
if (opts.inReplyTo !== void 0 && this.isOwnerVerifyFailed(opts.inReplyTo)) return;
|
|
38118
|
+
if (body.kind === "text" && opts.callBufferReply === true && opts.inReplyTo !== void 0 && !this.spokenReplyIds.has(id) && !isBridgeMemoryLine(body.text) && this.callBuffer.isVoiceScopedId(opts.inReplyTo)) {
|
|
38119
|
+
this.callBuffer.appendAgent(channelId, body.text, "reply");
|
|
38120
|
+
}
|
|
37945
38121
|
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
38122
|
if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, body.text, id, modality, purgeEpoch, origin);
|
|
37947
38123
|
if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, body.text, id, replyCapture ?? null, origin);
|
|
@@ -38712,6 +38888,27 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38712
38888
|
mayReadHistoryForTurn(messageId, authorization) {
|
|
38713
38889
|
return !this.isOwnerVerifyFailed(messageId) && this.mayReadOpenClawMemoryForTurn(authorization);
|
|
38714
38890
|
}
|
|
38891
|
+
/**
|
|
38892
|
+
* Voice fast-path privacy (spec §2.1, OQ-1 ruled): the ONLY history source of a voice-scoped turn, the call buffer.
|
|
38893
|
+
* Never reads the memory provider, the store, the unified store or the 7b index. A-6: called once per turn, at ingest
|
|
38894
|
+
* (takeVoiceSnapshot), after the reset signals and before the turn's own append.
|
|
38895
|
+
*/
|
|
38896
|
+
voiceScopedHistory(channelId, limit, upTo) {
|
|
38897
|
+
return this.callBuffer.recent(channelId, limit, upTo);
|
|
38898
|
+
}
|
|
38899
|
+
/**
|
|
38900
|
+
* Task 4b (A-6): the turn's snapshot, taken BEFORE its own text is appended (audit finding 2). Final fix I-1: a turn
|
|
38901
|
+
* already admitted to this call (a redelivered row) is snapshotted up to just before its own entry, as it was the
|
|
38902
|
+
* first time, so its snapshot never contains itself.
|
|
38903
|
+
*/
|
|
38904
|
+
takeVoiceSnapshot(channelId, messageId) {
|
|
38905
|
+
const upTo = this.callBuffer.admittedAt(messageId);
|
|
38906
|
+
return {
|
|
38907
|
+
recent: this.voiceScopedHistory(channelId, _BridgeAgent.TURN_MEMORY_LIMIT, upTo),
|
|
38908
|
+
endSeq: upTo ?? this.callBuffer.seqAt(channelId),
|
|
38909
|
+
generation: this.callBuffer.generation(channelId)
|
|
38910
|
+
};
|
|
38911
|
+
}
|
|
38715
38912
|
/** An inbound turn may receive OpenClaw memory only from an active owner DM. */
|
|
38716
38913
|
mayReadOpenClawMemoryForTurn(authorization) {
|
|
38717
38914
|
return this.cfg.memoryProvider !== "openclaw" || authorization.sender_membership === "active" && authorization.audience === "owner_dm";
|
|
@@ -38721,16 +38918,26 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38721
38918
|
* stays owner-DM-only; every provider failure degrades to empty context and
|
|
38722
38919
|
* never falls back to the runtime's retired plaintext JSON files.
|
|
38723
38920
|
*/
|
|
38724
|
-
async memoryForTurn(channelId, messageId, authorization, limit = 50) {
|
|
38921
|
+
async memoryForTurn(channelId, messageId, authorization, limit = 50, voice) {
|
|
38725
38922
|
if (!this.mayReadHistoryForTurn(messageId, authorization)) {
|
|
38726
38923
|
return { recent: [], facts: [] };
|
|
38727
38924
|
}
|
|
38925
|
+
if (voice) return { recent: this.voiceFrameTurns(voice.snapshot?.recent ?? []), facts: [] };
|
|
38728
38926
|
try {
|
|
38729
38927
|
return this.fenceMemory(channelId, await this.memory.context(channelId, limit), holdsManifestGrant(authorization));
|
|
38730
38928
|
} catch {
|
|
38731
38929
|
return { recent: [], facts: [] };
|
|
38732
38930
|
}
|
|
38733
38931
|
}
|
|
38932
|
+
/**
|
|
38933
|
+
* Task 6 (spec §2.2 A-1, N1): call-buffer entries as runtime-frame turns. Under the voice fence (a pin is loaded) the
|
|
38934
|
+
* fast model's own lines (origin "fast_path") are dropped, as fenceMemory drops them from store history; the fast
|
|
38935
|
+
* prompt keeps them. The internal origin is not carried into the frame; `seq` stays 0 (A-5).
|
|
38936
|
+
*/
|
|
38937
|
+
voiceFrameTurns(entries) {
|
|
38938
|
+
const fenced = this.voiceFenceOn();
|
|
38939
|
+
return entries.filter((e) => !(fenced && e.origin === "fast_path")).map((e) => ({ seq: e.seq, role: e.role, text: e.text }));
|
|
38940
|
+
}
|
|
38734
38941
|
/**
|
|
38735
38942
|
* 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
38943
|
* turn (manifest, fallback, greeting, relayed, constants-only alike). It never depends on a turn's grants.
|
|
@@ -38765,7 +38972,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38765
38972
|
if (!strict && !ctx.recent.some((t) => t.role === "agent")) return ctx;
|
|
38766
38973
|
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
38767
38974
|
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();
|
|
38975
|
+
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
38976
|
const fence = this.fenceContext(channelId);
|
|
38770
38977
|
const recent = ctx.recent.filter((t) => {
|
|
38771
38978
|
const m = meta.get(t.seq);
|
|
@@ -38784,7 +38991,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38784
38991
|
if (recent.length === 0) return false;
|
|
38785
38992
|
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
38786
38993
|
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();
|
|
38994
|
+
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
38995
|
return recent.some((t) => t.role !== "agent" && isRelayClassPeer({ peerId: t.peerId, modality: meta.get(t.seq)?.modality }, ownerId));
|
|
38789
38996
|
}
|
|
38790
38997
|
/**
|
|
@@ -39080,9 +39287,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39080
39287
|
writeServiceLog2("warn", "memory.recall_prepare_failed", {}, e);
|
|
39081
39288
|
}
|
|
39082
39289
|
}
|
|
39290
|
+
/**
|
|
39291
|
+
* Voice fast-path privacy, Task 7 (spec §4): the frame-keyed delivery check. A voice-scoped frame gets no notes, no
|
|
39292
|
+
* recall and no unified items, whatever its authorization says. Keys on the frame's own marks, NEVER on the call
|
|
39293
|
+
* buffer's id set (a reset evicts ids; the staged frame must stay scoped).
|
|
39294
|
+
*/
|
|
39295
|
+
frameIsVoiceScoped(m) {
|
|
39296
|
+
return m.voice_scoped === true || m.transcribed === true || m.speaker !== void 0;
|
|
39297
|
+
}
|
|
39083
39298
|
/** ws flush (synchronous). Q1 to Q3 and Q6 again at delivery, on the ORIGINAL message; Q4 lives in withConsumerRecall. */
|
|
39084
39299
|
recallForDelivery(m) {
|
|
39085
39300
|
if (this.unifiedOn) return void 0;
|
|
39301
|
+
if (this.frameIsVoiceScoped(m)) return void 0;
|
|
39086
39302
|
if (!this.recallProviderOn() || !this.notesReadable(m.authorization)) return void 0;
|
|
39087
39303
|
return this.recall.deliverable(m.message_id, m.channel_id);
|
|
39088
39304
|
}
|
|
@@ -39315,6 +39531,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39315
39531
|
/** ws flush (synchronous). The predicate and the owner scope again at delivery, on the ORIGINAL message (H-4, Review Focus 3). */
|
|
39316
39532
|
unifiedForDelivery(m) {
|
|
39317
39533
|
if (!this.unifiedProviderOn()) return void 0;
|
|
39534
|
+
if (this.frameIsVoiceScoped(m)) return void 0;
|
|
39318
39535
|
const a = m.authorization;
|
|
39319
39536
|
if (!(a?.memory_write === true || a?.relay_speaker === "owner")) return void 0;
|
|
39320
39537
|
const scopeId = this.ownerScopeNow(m.channel_id);
|
|
@@ -39550,6 +39767,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39550
39767
|
notesForDelivery(m) {
|
|
39551
39768
|
if (this.unifiedOn) return { generation: this.notes.appliedGeneration() };
|
|
39552
39769
|
const generation = this.notes.appliedGeneration();
|
|
39770
|
+
if (this.frameIsVoiceScoped(m)) return { generation };
|
|
39553
39771
|
if (!this.notesReadable(m.authorization)) return { generation };
|
|
39554
39772
|
const d = this.notes.deliverable(m.channel_id);
|
|
39555
39773
|
if (d.miss) {
|
|
@@ -39909,7 +40127,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39909
40127
|
* deferring fast reply was actually published — F3).
|
|
39910
40128
|
*/
|
|
39911
40129
|
async speakAndSettle(turn, plan, held, heldAfterAck, fastText, guidance, fastSpeaker, greeting) {
|
|
39912
|
-
const outcome = plan.kind === "ack" ? await this.sendFastLine(turn
|
|
40130
|
+
const outcome = plan.kind === "ack" ? await this.sendFastLine(turn, plan.line) ? "deferred" : "failed" : await this.runFastReply(turn, fastText, guidance, fastSpeaker, greeting);
|
|
39913
40131
|
if (outcome === "answered") await this.settleHeldRuntimeMessage(held, false);
|
|
39914
40132
|
else await this.settleHeldRuntimeMessage(outcome === "deferred" ? heldAfterAck : held, true);
|
|
39915
40133
|
}
|
|
@@ -39928,19 +40146,29 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39928
40146
|
async settleFastReplies() {
|
|
39929
40147
|
while (this.fastReplyChains.size) await Promise.all([...this.fastReplyChains.values()]);
|
|
39930
40148
|
}
|
|
39931
|
-
/**
|
|
39932
|
-
|
|
40149
|
+
/**
|
|
40150
|
+
* Send a line the voice fast path speaks: its memory record is "voice" (H1).
|
|
40151
|
+
* Task 4b (spec §2.1 agent appends (1), A-1, A-2): once published, the line enters the call buffer as "fast_path",
|
|
40152
|
+
* unless the turn it answers failed owner verification (its id, or any fragment of a coalesced turn). A line answering
|
|
40153
|
+
* a turn the ingest did not find voice-scoped is not appended either (defense in depth: the fast path fires only on
|
|
40154
|
+
* voice-scoped turns, Task 5).
|
|
40155
|
+
*/
|
|
40156
|
+
async sendSpoken(channelId, text, messageId, turn) {
|
|
39933
40157
|
this.spokenReplyIds.add(messageId);
|
|
39934
40158
|
try {
|
|
39935
40159
|
await this.send(channelId, text, messageId);
|
|
39936
40160
|
} finally {
|
|
39937
40161
|
this.spokenReplyIds.delete(messageId);
|
|
39938
40162
|
}
|
|
40163
|
+
if (turn.voiceScoped === true && turn.anyFragmentVerifyFailed !== true && !this.isOwnerVerifyFailed(turn.messageId)) {
|
|
40164
|
+
this.callBuffer.appendAgent(channelId, text, "fast_path");
|
|
40165
|
+
}
|
|
39939
40166
|
}
|
|
39940
40167
|
/** Speak a fixed acknowledgement line; failures are counted, never thrown. */
|
|
39941
|
-
async sendFastLine(
|
|
40168
|
+
async sendFastLine(turn, line) {
|
|
40169
|
+
const { channelId, messageId } = turn;
|
|
39942
40170
|
try {
|
|
39943
|
-
await this.sendSpoken(channelId, line, fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, messageId));
|
|
40171
|
+
await this.sendSpoken(channelId, line, fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, messageId), turn);
|
|
39944
40172
|
this.fastPathSent++;
|
|
39945
40173
|
return true;
|
|
39946
40174
|
} catch (e) {
|
|
@@ -39954,8 +40182,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39954
40182
|
* the fast model's reply — or, in `ack_only`, the greeting template — is
|
|
39955
40183
|
* spoken, and the outcome decides whether the runtime answers next.
|
|
39956
40184
|
*
|
|
39957
|
-
* MUST NEVER throw: every failure path (
|
|
39958
|
-
*
|
|
40185
|
+
* MUST NEVER throw: every failure path (the LLM call, normalization,
|
|
40186
|
+
* or the send itself) is caught, counted in
|
|
39959
40187
|
* `fastPathFailed`, and logged WITHOUT the turn text or the model's reply.
|
|
39960
40188
|
* The `fastReplyId(...)` send id is deterministic, so a duplicate that
|
|
39961
40189
|
* slipped past the in-memory dedupe (e.g. a restart) resolves to the same
|
|
@@ -39969,20 +40197,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39969
40197
|
try {
|
|
39970
40198
|
if ((this.cfg.voiceFastPathMode ?? "full") === "ack_only") {
|
|
39971
40199
|
if (!isGreeting) return "failed";
|
|
39972
|
-
const ok = await this.sendFastLine(m
|
|
40200
|
+
const ok = await this.sendFastLine(m, ackOnlyGreeting(this.voiceAgentName(), speaker?.displayName));
|
|
39973
40201
|
return ok ? "answered" : "failed";
|
|
39974
40202
|
}
|
|
39975
40203
|
const fastLlm = this.fastLlm;
|
|
39976
40204
|
if (!fastLlm || !this.cfg.voice) return "failed";
|
|
39977
|
-
let memory;
|
|
39978
|
-
|
|
39979
|
-
|
|
39980
|
-
|
|
39981
|
-
|
|
39982
|
-
|
|
39983
|
-
}
|
|
39984
|
-
memory = { recent: [], facts: [] };
|
|
39985
|
-
}
|
|
40205
|
+
let memory = { recent: [], facts: [] };
|
|
40206
|
+
const snapshot = m.voiceSnapshot;
|
|
40207
|
+
if (m.voiceScoped === true && snapshot !== void 0 && m.anyFragmentVerifyFailed !== true && this.mayReadHistoryForTurn(m.messageId, m.authorization)) {
|
|
40208
|
+
memory = {
|
|
40209
|
+
recent: [...snapshot.recent, ...this.callBuffer.agentAfter(m.channelId, { seq: snapshot.endSeq, generation: snapshot.generation })].slice(-FAST_PATH_MEMORY_TURNS),
|
|
40210
|
+
facts: []
|
|
40211
|
+
};
|
|
39986
40212
|
}
|
|
39987
40213
|
const messages = buildFastMessages({
|
|
39988
40214
|
persona: this.cfg.voice.persona,
|
|
@@ -40005,7 +40231,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40005
40231
|
const refusal = !isGreeting && isRefusal(split3.reply);
|
|
40006
40232
|
const defer = split3.defer || refusal;
|
|
40007
40233
|
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));
|
|
40234
|
+
await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId), m);
|
|
40009
40235
|
this.fastPathSent++;
|
|
40010
40236
|
if (defer) this.fastPathDeferred++;
|
|
40011
40237
|
if (refusal) this.fastPathRefusalDeferred++;
|
|
@@ -40069,6 +40295,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40069
40295
|
async memoryAtRelease(message) {
|
|
40070
40296
|
const prior = message.memory;
|
|
40071
40297
|
if (!prior || !this.mayReadHistoryForTurn(message.message_id, message.authorization)) return prior;
|
|
40298
|
+
if (this.frameIsVoiceScoped(message)) {
|
|
40299
|
+
const tails = this.voiceCoalescedTails.get(message.message_id) ?? [];
|
|
40300
|
+
if (tails.some((id) => this.isOwnerVerifyFailed(id))) return prior;
|
|
40301
|
+
const noted = this.callBuffer.seqNoted(message.message_id);
|
|
40302
|
+
if (noted === void 0) return prior;
|
|
40303
|
+
const spoken2 = this.voiceFrameTurns(this.callBuffer.agentAfter(message.channel_id, noted));
|
|
40304
|
+
if (spoken2.length === 0) return prior;
|
|
40305
|
+
return { recent: [...prior.recent, ...spoken2].slice(-_BridgeAgent.TURN_MEMORY_LIMIT), facts: [] };
|
|
40306
|
+
}
|
|
40072
40307
|
let fresh;
|
|
40073
40308
|
try {
|
|
40074
40309
|
fresh = await this.memory.context(message.channel_id, _BridgeAgent.TURN_MEMORY_LIMIT);
|
|
@@ -40580,9 +40815,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40580
40815
|
});
|
|
40581
40816
|
}
|
|
40582
40817
|
/** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
|
|
40583
|
-
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId) {
|
|
40818
|
+
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId, voiceScoped) {
|
|
40584
40819
|
const held = await this.heldPosition(channelId);
|
|
40585
|
-
const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict });
|
|
40820
|
+
const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict, voiceScoped });
|
|
40586
40821
|
if (!(auth.audience === "team_room" && auth.tool_grants.length > 0)) return auth;
|
|
40587
40822
|
if (this.roomGrantEpochs.size >= ROOM_GRANT_TRACK_MAX || held.epoch === null || held.groupId === null) {
|
|
40588
40823
|
writeServiceLog2("warn", "room_grant.untracked");
|
|
@@ -40597,17 +40832,22 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40597
40832
|
* guess. The returned object and grant array are frozen before they cross the
|
|
40598
40833
|
* daemon boundary so one consumer cannot widen a later execution check.
|
|
40599
40834
|
*
|
|
40600
|
-
* Grants (architect ruling 2026-10-02):
|
|
40601
|
-
* the kill switch is on, the channel is a DM, and EITHER
|
|
40835
|
+
* Grants (architect ruling 2026-10-02; P-5 and G-1, spec §9): an owner-DM
|
|
40836
|
+
* constant only when the kill switch is on, the channel is a DM, and EITHER
|
|
40602
40837
|
* (a) audience is owner_dm AND the sender is the channel's pinned DM
|
|
40603
40838
|
* counterpart (`channelOwner`, never "anomalous"). A server-marked
|
|
40604
40839
|
* `is_agent_owner` on a member who is NOT the DM's single human
|
|
40605
40840
|
* counterpart gets nothing; on the counterpart it is trusted (see
|
|
40606
|
-
* LIMIT below);
|
|
40841
|
+
* LIMIT below). OWNER_DM_DEFAULT_READ_GRANTS on a typed turn;
|
|
40842
|
+
* RELAYED_VOICE_GRANTS and no memory_write when `opts.voiceScoped`
|
|
40843
|
+
* (G-1: the ingest predicate isVoiceScopedTurn, spec §1); OR
|
|
40607
40844
|
* (b) the sender is this channel's server-marked relay (`is_relay`) and the
|
|
40608
40845
|
* relayed `speaker_identity_id` (passed by the caller only for relay
|
|
40609
40846
|
* turns) is that same pinned owner, whose roster row is the agent's
|
|
40610
|
-
* owner. A speaker claim from any non-relay
|
|
40847
|
+
* owner: RELAYED_VOICE_GRANTS. A speaker claim from any non-relay
|
|
40848
|
+
* sender gets nothing.
|
|
40849
|
+
* RELAYED_VOICE_GRANTS is never manifest-eligible: effectiveToolGrants gives
|
|
40850
|
+
* the constant by reference, or the fallback when a good manifest is held.
|
|
40611
40851
|
* Both need the roster fetched at `heldEpoch` (the agent's current MLS
|
|
40612
40852
|
* epoch for the channel), or the DM marked epoch-fresh at this ingest
|
|
40613
40853
|
* cycle's refresh (audit M-B4).
|
|
@@ -40651,9 +40891,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40651
40891
|
const ownerDirect = audience === "owner_dm" && senderId === ownerId;
|
|
40652
40892
|
const owner = roster?.get(ownerId);
|
|
40653
40893
|
const ownerRelayed = relaySpeakerId !== void 0 && sender.is_relay && relaySpeakerId === ownerId && owner !== void 0 && !owner.is_agent && owner.is_agent_owner;
|
|
40654
|
-
if (ownerDirect
|
|
40894
|
+
if (ownerDirect) tool_grants = opts.voiceScoped === true ? RELAYED_VOICE_GRANTS : OWNER_DM_DEFAULT_READ_GRANTS;
|
|
40895
|
+
if (ownerRelayed) tool_grants = RELAYED_VOICE_GRANTS;
|
|
40655
40896
|
if (ownerRelayed) relaySpeaker = "owner";
|
|
40656
|
-
if (ownerDirect && relaySpeakerId === void 0 &&
|
|
40897
|
+
if (ownerDirect && relaySpeakerId === void 0 && // G-1: never on a voice-scoped turn.
|
|
40898
|
+
opts.voiceScoped !== true && (this.cfg.memoryProvider ?? "securecoms") === "securecoms" && // Final review M-4: the live payload flag too, so with notes off the
|
|
40657
40899
|
// runtime does not offer memory_note at all. Not memoryConfigConfirmed:
|
|
40658
40900
|
// a turn the boot drain pulls before the boot refetch keeps its mark,
|
|
40659
40901
|
// and the read and write gates (notesProviderOn) hold it until then.
|
|
@@ -40670,11 +40912,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40670
40912
|
const binding = this.manifestBinding;
|
|
40671
40913
|
const boundNow = binding !== null && binding.state === "bound" && binding.counter === this.toolGrantCounter;
|
|
40672
40914
|
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;
|
|
40915
|
+
if (tool_grants === OWNER_DM_DEFAULT_READ_GRANTS || tool_grants === RELAYED_VOICE_GRANTS) {
|
|
40916
|
+
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
40917
|
const r = effectiveToolGrants({
|
|
40676
40918
|
audience: "owner_dm",
|
|
40677
|
-
base:
|
|
40919
|
+
base: tool_grants,
|
|
40678
40920
|
manifest,
|
|
40679
40921
|
eligible,
|
|
40680
40922
|
manifestSwitchOn: this.ownerDmManifestGrants,
|
|
@@ -41396,6 +41638,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41396
41638
|
const acked = [];
|
|
41397
41639
|
for (const m of entries) {
|
|
41398
41640
|
try {
|
|
41641
|
+
if (m.voiceMode === false) this.callBuffer.reset(m.channelId);
|
|
41399
41642
|
const encId = m.envelope.enclave_id;
|
|
41400
41643
|
if (encId && !this.channelEnclaveRecorded.has(m.channelId)) {
|
|
41401
41644
|
this.channelEnclaveRecorded.add(m.channelId);
|
|
@@ -41553,6 +41796,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41553
41796
|
continue;
|
|
41554
41797
|
}
|
|
41555
41798
|
if (typeof parsed.text === "string") {
|
|
41799
|
+
if (parsed.text.trim() === VOICE_CALL_START_MARKER) this.callBuffer.reset(m.channelId);
|
|
41556
41800
|
const meta = this.freshMeta(m.channelId);
|
|
41557
41801
|
const senderType = m.envelope.sender_type;
|
|
41558
41802
|
const senderIsAgent = senderType === "agent" ? true : senderType === "human" ? false : void 0;
|
|
@@ -41594,23 +41838,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41594
41838
|
const senderName = this.freshRoster(m.channelId)?.get(
|
|
41595
41839
|
m.envelope.sender_id
|
|
41596
41840
|
)?.name;
|
|
41841
|
+
const voiceScoped = isVoiceScopedTurn({
|
|
41842
|
+
speaker,
|
|
41843
|
+
transcribed: parsed.transcribed,
|
|
41844
|
+
senderIsRelay: this.freshRoster(m.channelId)?.get(m.envelope.sender_id)?.is_relay === true,
|
|
41845
|
+
text: parsed.text
|
|
41846
|
+
});
|
|
41597
41847
|
const ownerVerdict = await this.verifyOwnerTurn(m);
|
|
41598
41848
|
const turnAuthorization = await this.authorizeTurn(
|
|
41599
41849
|
m.channelId,
|
|
41600
41850
|
m.messageId,
|
|
41601
41851
|
m.envelope.sender_id,
|
|
41602
41852
|
ownerVerdict,
|
|
41603
|
-
speaker?.identityId
|
|
41853
|
+
speaker?.identityId,
|
|
41854
|
+
voiceScoped
|
|
41604
41855
|
);
|
|
41605
41856
|
await this.prepareNotesForTurn(m.channelId, turnAuthorization);
|
|
41606
|
-
const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization);
|
|
41607
41857
|
const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
|
|
41608
41858
|
const humanTurn = senderIsAgent !== true || speaker !== void 0;
|
|
41609
|
-
const fastPathEligible = Boolean(m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
41859
|
+
const fastPathEligible = Boolean(voiceScoped && m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
41610
41860
|
const turnText = parsed.text;
|
|
41611
41861
|
const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
|
|
41612
41862
|
const windowEligible = voiceWindowEligible({ voiceMode: m.voiceMode, speaker, transcribed: parsed.transcribed, voiceGuidance, isCallStart });
|
|
41613
41863
|
const coalescible = this.voiceCoalesceWindowMs > 0 && windowEligible;
|
|
41864
|
+
const callPeer = admitsToCallBuffer({ speaker, transcribed: parsed.transcribed });
|
|
41865
|
+
let voiceSnapshot;
|
|
41866
|
+
if (voiceScoped) {
|
|
41867
|
+
this.callBuffer.markVoiceScoped(m.channelId, m.messageId);
|
|
41868
|
+
if (!coalescible) {
|
|
41869
|
+
voiceSnapshot = this.takeVoiceSnapshot(m.channelId, m.messageId);
|
|
41870
|
+
if (callPeer && !this.isOwnerVerifyFailed(m.messageId)) this.callBuffer.appendPeer(m.channelId, parsed.text, m.messageId);
|
|
41871
|
+
}
|
|
41872
|
+
}
|
|
41873
|
+
const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization, void 0, voiceScoped ? { snapshot: voiceSnapshot } : void 0);
|
|
41614
41874
|
if (!coalescible) {
|
|
41615
41875
|
const kind2 = m.voiceMode === true || parsed.transcribed === true ? "voice" : "text";
|
|
41616
41876
|
if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId: m.channelId, messageId: m.messageId, text: parsed.text, authorization: turnAuthorization, kind: kind2 });
|
|
@@ -41626,7 +41886,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41626
41886
|
const fastGreeting = isCallStart && fastPathFiring;
|
|
41627
41887
|
const runtimeVoice = (ackSent) => m.voiceMode ? {
|
|
41628
41888
|
voice: true,
|
|
41629
|
-
|
|
41889
|
+
// Task 6 (OQ-3): a voice-scoped frame's guidance ends with VOICE_CALL_CLAUSE, on all three frames
|
|
41890
|
+
// (held, coalesced after-ack, after-ack); a typed turn during a call keeps its guidance.
|
|
41891
|
+
guidance: `${ackSent ? `${voiceGuidance} ${FAST_PATH_SPOKEN_GUIDANCE}` : voiceGuidance}${voiceScoped ? ` ${VOICE_CALL_CLAUSE}` : ""}`
|
|
41630
41892
|
} : {};
|
|
41631
41893
|
const runtimeEntry = (ackSent, memory) => ({
|
|
41632
41894
|
channel_id: m.channelId,
|
|
@@ -41647,6 +41909,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41647
41909
|
// identify: the envelope's own claim (relay or on-device
|
|
41648
41910
|
// transcript) or a relay-attributed turn.
|
|
41649
41911
|
...parsed.transcribed === true || speaker !== void 0 ? { transcribed: true } : {},
|
|
41912
|
+
...voiceScoped ? { voice_scoped: true } : {},
|
|
41650
41913
|
...runtimeVoice(ackSent)
|
|
41651
41914
|
});
|
|
41652
41915
|
if (coalescible) {
|
|
@@ -41676,6 +41939,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41676
41939
|
acked.push(m.queueId);
|
|
41677
41940
|
continue;
|
|
41678
41941
|
}
|
|
41942
|
+
const bufferGeneration = this.callBuffer.generation(m.channelId);
|
|
41679
41943
|
const held = runtimeEntry(false, await runtimeMemory);
|
|
41680
41944
|
if (!await this.stageHeldRuntimeMessage(held)) continue;
|
|
41681
41945
|
this.noteVoiceCoalesced(m.messageId);
|
|
@@ -41686,7 +41950,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41686
41950
|
closed: false,
|
|
41687
41951
|
pending: Promise.resolve(),
|
|
41688
41952
|
purgeEpoch: purgeEpochAtReceipt.get(m.channelId),
|
|
41689
|
-
itemCapture: itemCaptureAtReceipt.get(m.channelId)
|
|
41953
|
+
itemCapture: itemCaptureAtReceipt.get(m.channelId),
|
|
41954
|
+
bufferGeneration
|
|
41690
41955
|
};
|
|
41691
41956
|
this.voiceCoalescing.set(windowKey, win);
|
|
41692
41957
|
const openedAt = Date.now();
|
|
@@ -41702,9 +41967,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41702
41967
|
const [firstId, ...tailIds] = win.ids;
|
|
41703
41968
|
if (tailIds.length) this.voiceCoalescedTails.set(firstId, tailIds);
|
|
41704
41969
|
const text = win.parts.join(" ");
|
|
41970
|
+
const sameCall = this.callBuffer.generation(channelId) === win.bufferGeneration;
|
|
41971
|
+
const anyFragmentVerifyFailed = win.ids.some((id) => this.isOwnerVerifyFailed(id));
|
|
41972
|
+
let windowSnapshot;
|
|
41973
|
+
if (sameCall) {
|
|
41974
|
+
this.callBuffer.markVoiceScoped(channelId, firstId);
|
|
41975
|
+
windowSnapshot = this.takeVoiceSnapshot(channelId, firstId);
|
|
41976
|
+
}
|
|
41977
|
+
if (callPeer && sameCall && !anyFragmentVerifyFailed) this.callBuffer.appendPeer(channelId, text, firstId);
|
|
41705
41978
|
if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
|
|
41706
41979
|
else await this.prepareRecallForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
|
|
41707
|
-
const
|
|
41980
|
+
const windowMemory = await this.memoryForTurn(channelId, firstId, turnAuthorization, void 0, { snapshot: anyFragmentVerifyFailed ? void 0 : windowSnapshot });
|
|
41981
|
+
const merged = { ...held, text, memory: windowMemory };
|
|
41708
41982
|
if (this.unifiedOn) {
|
|
41709
41983
|
const snap = win.itemCapture ?? { ...this.store.itemCapture(channelId), purgeEpoch: -1 };
|
|
41710
41984
|
const work = this.tapUnifiedVoice(merged, { stamp: this.ingestGeneration.get(firstId), capture: { ...snap, purgeEpoch: win.purgeEpoch ?? -1 } });
|
|
@@ -41732,7 +42006,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41732
42006
|
const release = this.reserveFastReply(channelId);
|
|
41733
42007
|
try {
|
|
41734
42008
|
await this.speakAndSettle(
|
|
41735
|
-
{ channelId, messageId: firstId, senderId, authorization: turnAuthorization },
|
|
42009
|
+
{ channelId, messageId: firstId, senderId, authorization: turnAuthorization, voiceScoped: voiceScoped && sameCall, voiceSnapshot: windowSnapshot, anyFragmentVerifyFailed },
|
|
41736
42010
|
plan,
|
|
41737
42011
|
merged,
|
|
41738
42012
|
{ ...merged, ...runtimeVoice(true) },
|
|
@@ -41764,7 +42038,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41764
42038
|
channelId: m.channelId,
|
|
41765
42039
|
messageId: m.messageId,
|
|
41766
42040
|
senderId: m.envelope.sender_id,
|
|
41767
|
-
authorization: turnAuthorization
|
|
42041
|
+
authorization: turnAuthorization,
|
|
42042
|
+
voiceScoped,
|
|
42043
|
+
voiceSnapshot
|
|
41768
42044
|
};
|
|
41769
42045
|
const guidanceForFast = voiceGuidance;
|
|
41770
42046
|
const greeting = fastGreeting;
|
|
@@ -44033,10 +44309,13 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
44033
44309
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
44034
44310
|
return;
|
|
44035
44311
|
}
|
|
44312
|
+
const record = typeof m.in_reply_to === "string" ? inflight.get(m.in_reply_to) : void 0;
|
|
44313
|
+
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
44314
|
const sendOpts = {
|
|
44037
44315
|
agentTurn: isAgentTurn(agent, m),
|
|
44038
44316
|
...typeof m.in_reply_to === "string" ? { inReplyTo: m.in_reply_to } : {},
|
|
44039
|
-
...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {}
|
|
44317
|
+
...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {},
|
|
44318
|
+
...callBufferReply ? { callBufferReply: true } : {}
|
|
44040
44319
|
};
|
|
44041
44320
|
if (m.a2a && typeof m.a2a === "object") {
|
|
44042
44321
|
const a = m.a2a;
|