@securecoms/agent-bridge 0.30.2 → 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 +53 -0
- package/dist/bin.js +571 -91
- package/dist/bridge-agent.d.ts +149 -15
- package/dist/consumer-grants.d.ts +15 -0
- package/dist/grants/binding.d.ts +30 -2
- package/dist/grants/manifest.d.ts +11 -1
- package/dist/grants/search-providers.d.ts +9 -0
- package/dist/http-server.d.ts +2 -0
- package/dist/index.js +550 -71
- package/dist/preflight-upgrade.d.cts +1 -0
- package/dist/preflight.cjs +1 -0
- package/dist/service/runtime-config-dir.d.ts +4 -0
- 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/dist/ws-server.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29101,7 +29101,7 @@ function loadPackageMeta(importMetaUrl) {
|
|
|
29101
29101
|
}
|
|
29102
29102
|
|
|
29103
29103
|
// src/bridge-agent.ts
|
|
29104
|
-
import { existsSync as existsSync5, promises as fsp, statSync as statSync4 } from "node:fs";
|
|
29104
|
+
import { existsSync as existsSync5, promises as fsp, readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
|
|
29105
29105
|
import { join as join17, dirname as dirname5 } from "node:path";
|
|
29106
29106
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
29107
29107
|
|
|
@@ -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.
|
|
@@ -34032,6 +34190,15 @@ import {
|
|
|
34032
34190
|
writeFileSync
|
|
34033
34191
|
} from "node:fs";
|
|
34034
34192
|
import { basename as basename4, isAbsolute, join as join15 } from "node:path";
|
|
34193
|
+
|
|
34194
|
+
// src/grants/search-providers.ts
|
|
34195
|
+
var SEARCH_PROVIDERS = /* @__PURE__ */ new Set(["brave", "tavily", "parallel", "exa", "firecrawl"]);
|
|
34196
|
+
var OWNER_DM_SEARCH_DAILY_CAP_MAX = 2e3;
|
|
34197
|
+
function isSearchProvider(value) {
|
|
34198
|
+
return typeof value === "string" && SEARCH_PROVIDERS.has(value);
|
|
34199
|
+
}
|
|
34200
|
+
|
|
34201
|
+
// src/service/runtime-config-dir.ts
|
|
34035
34202
|
var MEMORY_FILE = "MEMORY.md";
|
|
34036
34203
|
var REAL_FS = {
|
|
34037
34204
|
mkdir: (p, mode) => mkdirSync5(p, { recursive: true, mode }),
|
|
@@ -34214,10 +34381,14 @@ function readLocalReport(home, identityId) {
|
|
|
34214
34381
|
const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
|
|
34215
34382
|
Object.entries(raw).filter(([, v]) => typeof v === "string")
|
|
34216
34383
|
) : {};
|
|
34384
|
+
const capRaw = doc.owner_dm_search_cap_effective;
|
|
34385
|
+
const ownerDmCap = typeof capRaw === "number" && Number.isInteger(capRaw) && capRaw >= 0 && capRaw <= OWNER_DM_SEARCH_DAILY_CAP_MAX ? capRaw : void 0;
|
|
34217
34386
|
return {
|
|
34218
34387
|
persona,
|
|
34219
34388
|
has_search_key: hasSearchKey,
|
|
34220
34389
|
has_llm_key: hasLlmKey,
|
|
34390
|
+
has_extract_provider: doc.has_extract_provider === true,
|
|
34391
|
+
...ownerDmCap !== void 0 ? { owner_dm_search_cap_effective: ownerDmCap } : {},
|
|
34221
34392
|
legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source),
|
|
34222
34393
|
secret_paths: parseSecretPathsReport(doc.secret_paths)
|
|
34223
34394
|
};
|
|
@@ -34416,7 +34587,8 @@ function loadOwnerGrantPin(opts) {
|
|
|
34416
34587
|
// src/grants/manifest.ts
|
|
34417
34588
|
var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
|
|
34418
34589
|
var MANIFEST_KNOWN_TOOLS = Object.freeze({
|
|
34419
|
-
owner_dm: Object.freeze(["web_search", "fetch_url", "http_request", "sql_query"]),
|
|
34590
|
+
owner_dm: Object.freeze(["web_search", "web_extract", "fetch_url", "http_request", "sql_query"]),
|
|
34591
|
+
// web_extract: owner DM only, never team_room (spec §5.4, D5)
|
|
34420
34592
|
team_room: Object.freeze(["web_search"])
|
|
34421
34593
|
});
|
|
34422
34594
|
function manifestSignedBytes(manifest) {
|
|
@@ -34491,7 +34663,11 @@ function parseConfig(v) {
|
|
|
34491
34663
|
}
|
|
34492
34664
|
if ("search" in v) {
|
|
34493
34665
|
const sr = v.search;
|
|
34494
|
-
if (!isPlainObject(sr) || !exactKeys(sr, ["provider"]
|
|
34666
|
+
if (!isPlainObject(sr) || !exactKeys(sr, ["provider"], ["owner_dm_daily_cap"]) || !isSearchProvider(sr.provider)) return null;
|
|
34667
|
+
if ("owner_dm_daily_cap" in sr) {
|
|
34668
|
+
const cap = sr.owner_dm_daily_cap;
|
|
34669
|
+
if (typeof cap !== "number" || !Number.isSafeInteger(cap) || cap < 0 || cap > OWNER_DM_SEARCH_DAILY_CAP_MAX) return null;
|
|
34670
|
+
}
|
|
34495
34671
|
}
|
|
34496
34672
|
const mem = v.memory;
|
|
34497
34673
|
if (!isPlainObject(mem) || !exactKeys(mem, ["mode"], ["recall_days"])) return null;
|
|
@@ -34501,7 +34677,12 @@ function parseConfig(v) {
|
|
|
34501
34677
|
return Object.freeze({
|
|
34502
34678
|
persona_sha256: v.persona_sha256,
|
|
34503
34679
|
...isPlainObject(v.llm) ? { llm: Object.freeze({ provider: v.llm.provider, model: v.llm.model }) } : {},
|
|
34504
|
-
...isPlainObject(v.search) ? {
|
|
34680
|
+
...isPlainObject(v.search) ? {
|
|
34681
|
+
search: Object.freeze({
|
|
34682
|
+
provider: v.search.provider,
|
|
34683
|
+
..."owner_dm_daily_cap" in v.search ? { owner_dm_daily_cap: v.search.owner_dm_daily_cap } : {}
|
|
34684
|
+
})
|
|
34685
|
+
} : {},
|
|
34505
34686
|
...isStringArray(v.tools_allow) ? { tools_allow: Object.freeze([...v.tools_allow]) } : {},
|
|
34506
34687
|
flags: Object.freeze([...v.flags]),
|
|
34507
34688
|
memory: Object.freeze({ mode: mem.mode, ..."recall_days" in mem ? { recall_days: mem.recall_days } : {} }),
|
|
@@ -34732,7 +34913,20 @@ function bindTargets(raw, signed, equals) {
|
|
|
34732
34913
|
}
|
|
34733
34914
|
return { written, mismatches };
|
|
34734
34915
|
}
|
|
34735
|
-
function
|
|
34916
|
+
function signedManifestTools(manifest, audience, searchProvidersCapable) {
|
|
34917
|
+
return MANIFEST_KNOWN_TOOLS[audience].filter((t) => (manifest.grants[audience] ?? []).includes(t) && (t !== "web_extract" || searchProvidersCapable));
|
|
34918
|
+
}
|
|
34919
|
+
function manifestPolicyFor(manifest, switches, searchProvidersCapable) {
|
|
34920
|
+
return {
|
|
34921
|
+
v: 1,
|
|
34922
|
+
manifest_tools: [.../* @__PURE__ */ new Set([
|
|
34923
|
+
...switches.ownerDm ? signedManifestTools(manifest, "owner_dm", searchProvidersCapable) : [],
|
|
34924
|
+
...switches.room ? signedManifestTools(manifest, "team_room", searchProvidersCapable) : []
|
|
34925
|
+
])].sort(),
|
|
34926
|
+
secret_path_mode: "enforce"
|
|
34927
|
+
};
|
|
34928
|
+
}
|
|
34929
|
+
function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true }, searchProvidersCapable = false) {
|
|
34736
34930
|
const mismatches = /* @__PURE__ */ new Set();
|
|
34737
34931
|
const cfg = manifest.config;
|
|
34738
34932
|
const out = {};
|
|
@@ -34761,6 +34955,7 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
|
|
|
34761
34955
|
for (const name of Object.keys(files)) {
|
|
34762
34956
|
if (name !== LLM_SOURCE && !PERSONA_FILES.includes(name)) mismatches.add("files");
|
|
34763
34957
|
}
|
|
34958
|
+
const ownerDmTools = signedManifestTools(manifest, "owner_dm", searchProvidersCapable);
|
|
34764
34959
|
const sj = isObj2(payload.skills_json) ? payload.skills_json : void 0;
|
|
34765
34960
|
const eps = bindTargets(sj?.endpoints, manifest.endpoints, endpointEquals);
|
|
34766
34961
|
const dbs = bindTargets(sj?.databases, manifest.databases, databaseEquals);
|
|
@@ -34778,14 +34973,20 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
|
|
|
34778
34973
|
appliedSearch = searchBlock;
|
|
34779
34974
|
} else {
|
|
34780
34975
|
mismatches.add("search");
|
|
34781
|
-
|
|
34976
|
+
const usablePrior = cfg.search !== void 0 && prior.search?.provider === cfg.search.provider ? prior.search : void 0;
|
|
34977
|
+
searchBlock = cfg.search === void 0 ? void 0 : usablePrior ?? Object.freeze({ provider: cfg.search.provider });
|
|
34782
34978
|
}
|
|
34783
34979
|
const skillsDoc = {
|
|
34784
34980
|
endpoints: eps.written,
|
|
34785
34981
|
databases: dbs.written,
|
|
34786
34982
|
flags: [...cfg.flags],
|
|
34787
34983
|
...deny !== void 0 || cfg.tools_allow !== void 0 ? { tools: { ...deny !== void 0 ? { deny } : {}, ...cfg.tools_allow !== void 0 ? { allow: [...cfg.tools_allow] } : {} } } : {},
|
|
34788
|
-
...searchBlock ? { search: searchBlock } : {}
|
|
34984
|
+
...searchBlock ? { search: searchBlock } : {},
|
|
34985
|
+
// D31: the marker the runtime keys D24 (iii) on; written under every manifest, true when config.search is signed.
|
|
34986
|
+
search_signed: cfg.search !== void 0,
|
|
34987
|
+
// D36, D39: the signed owner-DM cap, only when the owner-DM switch is on and the manifest grants an owner-DM tool;
|
|
34988
|
+
// otherwise none, so the runtime clamps a signed raise to the default. Never read from the payload.
|
|
34989
|
+
...cfg.search?.owner_dm_daily_cap !== void 0 && switches.ownerDm && ownerDmTools.length > 0 ? { owner_dm_search_cap: cfg.search.owner_dm_daily_cap } : {}
|
|
34789
34990
|
};
|
|
34790
34991
|
out["skills.json"] = JSON.stringify(skillsDoc, null, 2) + "\n";
|
|
34791
34992
|
const signedMemory = { mode: cfg.memory.mode, ...cfg.memory.recall_days !== void 0 ? { recall_days: cfg.memory.recall_days } : {} };
|
|
@@ -34816,12 +35017,7 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
|
|
|
34816
35017
|
mismatches.add("legacy_assign");
|
|
34817
35018
|
legacyDecisions = decisions.filter((d) => isObj2(d) && d.action === "delete");
|
|
34818
35019
|
}
|
|
34819
|
-
const
|
|
34820
|
-
const policy = {
|
|
34821
|
-
v: 1,
|
|
34822
|
-
manifest_tools: [.../* @__PURE__ */ new Set([...switches.ownerDm ? signedTools("owner_dm") : [], ...switches.room ? signedTools("team_room") : []])].sort(),
|
|
34823
|
-
secret_path_mode: "enforce"
|
|
34824
|
-
};
|
|
35020
|
+
const policy = manifestPolicyFor(manifest, switches, searchProvidersCapable);
|
|
34825
35021
|
return {
|
|
34826
35022
|
mismatches: CONFIG_BINDING_FIELDS.filter((f) => mismatches.has(f)).sort(),
|
|
34827
35023
|
targetMismatches: { endpoint: eps.mismatches, database: dbs.mismatches },
|
|
@@ -34960,6 +35156,8 @@ var TOOL_ROOM_CLASSES = Object.freeze({
|
|
|
34960
35156
|
current_time: Object.freeze(["room_safe_read"]),
|
|
34961
35157
|
echo: Object.freeze(["room_safe_read"]),
|
|
34962
35158
|
web_search: Object.freeze(["owner_credential"]),
|
|
35159
|
+
web_extract: Object.freeze(["owner_credential", "egress"]),
|
|
35160
|
+
// D5: spends the owner's key and sends URLs out
|
|
34963
35161
|
fetch_url: Object.freeze(["egress"]),
|
|
34964
35162
|
sql_query: Object.freeze(["owner_credential"]),
|
|
34965
35163
|
http_request: Object.freeze(["owner_credential", "egress", "side_effect"])
|
|
@@ -35073,6 +35271,10 @@ var OWNER_DM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
|
35073
35271
|
var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
35074
35272
|
Object.freeze({ tool: "current_time", mode: "read" })
|
|
35075
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
|
+
]);
|
|
35076
35278
|
var NO_TOOL_GRANTS = Object.freeze([]);
|
|
35077
35279
|
var WS_CLOSE_SESSION_SUPERSEDED2 = 4409;
|
|
35078
35280
|
var OWNER_REPLAY_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
@@ -35381,6 +35583,8 @@ function hexToBytes4(h) {
|
|
|
35381
35583
|
return new Uint8Array(Buffer.from(h, "hex"));
|
|
35382
35584
|
}
|
|
35383
35585
|
var BOOT_DRAIN_WAIT_MS_DEFAULT = 6e4;
|
|
35586
|
+
var SEARCH_CAPABILITY_REFETCH_DEBOUNCE_MS = 1e3;
|
|
35587
|
+
var SEARCH_CAPABILITY_REFETCH_MAX_WAIT_MS = 1e4;
|
|
35384
35588
|
var BootDrainRotationRefusedError = class extends Error {
|
|
35385
35589
|
constructor() {
|
|
35386
35590
|
super("key-package rotation refused: the boot drain has not settled");
|
|
@@ -35430,6 +35634,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35430
35634
|
workspaceDir: this.cfg.memoryWorkspaceDir,
|
|
35431
35635
|
factsCharBudget: this.cfg.memoryFactsCharBudget
|
|
35432
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
|
+
});
|
|
35433
35643
|
this.provider = new MlsGroupKeyProvider({
|
|
35434
35644
|
store: this.store,
|
|
35435
35645
|
backend: this.backend,
|
|
@@ -35576,6 +35786,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35576
35786
|
this.actionClock = deps.actionThrottleNow ?? Date.now;
|
|
35577
35787
|
this.rng = deps.rng ?? Math.random;
|
|
35578
35788
|
this.bootDrainWaitMs = deps.bootDrainWaitMs ?? BOOT_DRAIN_WAIT_MS_DEFAULT;
|
|
35789
|
+
this.searchCapabilityRefetchDebounceMs = deps.searchCapabilityRefetchDebounceMs ?? SEARCH_CAPABILITY_REFETCH_DEBOUNCE_MS;
|
|
35790
|
+
this.searchCapabilityRefetchMaxWaitMs = deps.searchCapabilityRefetchMaxWaitMs ?? SEARCH_CAPABILITY_REFETCH_MAX_WAIT_MS;
|
|
35579
35791
|
this.replySleep = deps.replySleep ?? ((ms) => new Promise((resolve) => {
|
|
35580
35792
|
const t = setTimeout(resolve, ms);
|
|
35581
35793
|
t.unref?.();
|
|
@@ -36014,6 +36226,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36014
36226
|
bootSweepTimer;
|
|
36015
36227
|
rng;
|
|
36016
36228
|
bootDrainWaitMs;
|
|
36229
|
+
searchCapabilityRefetchDebounceMs;
|
|
36230
|
+
searchCapabilityRefetchMaxWaitMs;
|
|
36017
36231
|
/** Resolved (and cleared) each time an ingest cycle settles: see start(). */
|
|
36018
36232
|
ingestSettledWaiters = [];
|
|
36019
36233
|
/** The boot key-package publish, when start() had to defer it. Test seam. */
|
|
@@ -36249,6 +36463,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36249
36463
|
startedAt = 0;
|
|
36250
36464
|
store;
|
|
36251
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;
|
|
36252
36471
|
/**
|
|
36253
36472
|
* Markdown memory 7a: the one owner of every note mutation and of the
|
|
36254
36473
|
* delivery cache (spec §6.3, §7.2). One per store, so one per identity.
|
|
@@ -36459,9 +36678,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36459
36678
|
}
|
|
36460
36679
|
const skillsJson = payload.skills_json ?? {};
|
|
36461
36680
|
if (version && inForce === null) {
|
|
36681
|
+
let asDelivered = skillsJson;
|
|
36682
|
+
if (skillsJson !== null && typeof skillsJson === "object" && !Array.isArray(skillsJson)) {
|
|
36683
|
+
const { owner_dm_search_cap: _cap, search_signed: _signed, ...rest } = skillsJson;
|
|
36684
|
+
asDelivered = rest;
|
|
36685
|
+
}
|
|
36462
36686
|
this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, {
|
|
36463
36687
|
...payload.files ?? {},
|
|
36464
|
-
"skills.json": `${JSON.stringify(
|
|
36688
|
+
"skills.json": `${JSON.stringify(asDelivered, null, 2)}
|
|
36465
36689
|
`
|
|
36466
36690
|
});
|
|
36467
36691
|
}
|
|
@@ -36516,7 +36740,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36516
36740
|
}
|
|
36517
36741
|
let bound;
|
|
36518
36742
|
try {
|
|
36519
|
-
bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants });
|
|
36743
|
+
bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.searchProvidersCapable());
|
|
36520
36744
|
} catch (e) {
|
|
36521
36745
|
bound = null;
|
|
36522
36746
|
return this.bindFailed(manifest, payload.memory, e);
|
|
@@ -36535,9 +36759,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36535
36759
|
return true;
|
|
36536
36760
|
}
|
|
36537
36761
|
try {
|
|
36538
|
-
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, bound.policy);
|
|
36539
36762
|
this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, bound.files);
|
|
36540
36763
|
if (manifest.config.llm === void 0) this.grantWriters.removeManagedLlmSource(this.home, this.cfg.identityId);
|
|
36764
|
+
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.searchProvidersCapable()));
|
|
36541
36765
|
} catch (e) {
|
|
36542
36766
|
this.manifestBinding = null;
|
|
36543
36767
|
throw e;
|
|
@@ -36590,6 +36814,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36590
36814
|
/** The report half of refetchRuntimeConfigOnce (split out for the plan 4 Task 11 bind_failed path). */
|
|
36591
36815
|
async postRuntimeConfigReport(base, version, skillsJson) {
|
|
36592
36816
|
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
36817
|
+
const reportedSearchProviders = this.searchProvidersCapable();
|
|
36593
36818
|
const report = {
|
|
36594
36819
|
applied_version: version,
|
|
36595
36820
|
paths: checkConfiguredPaths(skillsJson),
|
|
@@ -36604,6 +36829,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36604
36829
|
// to the backend. The backend needs it to generate a truthful tool
|
|
36605
36830
|
// paragraph, so it rides along here.
|
|
36606
36831
|
has_search_key: local?.has_search_key ?? false,
|
|
36832
|
+
// L-2 (spec §7.9): only the runtime knows whether an extract-capable provider key is readable; never derived
|
|
36833
|
+
// from has_search_key. An older runtime omits it: false.
|
|
36834
|
+
// Final review I-2 (controller ruling): AND the current search_providers_v2, so the web never shows extract while
|
|
36835
|
+
// the bridge withholds web_extract from the runtime.
|
|
36836
|
+
has_extract_provider: (local?.has_extract_provider ?? false) && reportedSearchProviders,
|
|
36837
|
+
// D39: the owner-DM daily cap the runtime enforces, passed through when it reported a valid one; never the signed value.
|
|
36838
|
+
...local?.owner_dm_search_cap_effective !== void 0 ? { owner_dm_search_cap_effective: local.owner_dm_search_cap_effective } : {},
|
|
36607
36839
|
// Same idea, for the BYOK LLM key file: only the runtime can see
|
|
36608
36840
|
// whether one is configured in its own environment.
|
|
36609
36841
|
has_llm_key: local?.has_llm_key ?? false,
|
|
@@ -36643,7 +36875,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36643
36875
|
// Plan 4 Task 15 (OQ-5): sorted, unique enums; capability is the latest hello's class.
|
|
36644
36876
|
config_binding_mismatches: [...this.configBindingMismatches],
|
|
36645
36877
|
target_mismatches: ["database", "endpoint"].filter((k) => this.targetMismatches[k] > 0),
|
|
36646
|
-
capability: this.lastConsumerCapability
|
|
36878
|
+
capability: this.lastConsumerCapability,
|
|
36879
|
+
// D28 (§11.1 step 3): a separate key, never a value in the capability enum. False with no runtime attached.
|
|
36880
|
+
search_providers_v2: reportedSearchProviders,
|
|
36881
|
+
// D19 item 4: content-free; the signed provider is one the attached runtime(s) cannot honour.
|
|
36882
|
+
search_provider_unsupported: this.searchProviderUnsupported(reportedSearchProviders)
|
|
36647
36883
|
}
|
|
36648
36884
|
};
|
|
36649
36885
|
const reportedVerifyFailures = this.ownerVerifyFailures;
|
|
@@ -36664,6 +36900,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36664
36900
|
return;
|
|
36665
36901
|
}
|
|
36666
36902
|
this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
|
|
36903
|
+
this.lastReportedSearchProviders = reportedSearchProviders;
|
|
36667
36904
|
if (version) writeServiceLog2("info", "runtime_config.applied", { version });
|
|
36668
36905
|
else writeServiceLog2("info", "runtime_config.legacy_memory_reported");
|
|
36669
36906
|
}
|
|
@@ -36733,6 +36970,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36733
36970
|
this.startedAt = Date.now();
|
|
36734
36971
|
await this.restoreRuntimeInbox();
|
|
36735
36972
|
await this.loadToolGrantState();
|
|
36973
|
+
this.recomputeManifestPolicy(false);
|
|
36736
36974
|
try {
|
|
36737
36975
|
this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
|
|
36738
36976
|
} catch (e) {
|
|
@@ -37877,6 +38115,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37877
38115
|
const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
|
|
37878
38116
|
const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(body.text);
|
|
37879
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
|
+
}
|
|
37880
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";
|
|
37881
38122
|
if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, body.text, id, modality, purgeEpoch, origin);
|
|
37882
38123
|
if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, body.text, id, replyCapture ?? null, origin);
|
|
@@ -38647,6 +38888,27 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38647
38888
|
mayReadHistoryForTurn(messageId, authorization) {
|
|
38648
38889
|
return !this.isOwnerVerifyFailed(messageId) && this.mayReadOpenClawMemoryForTurn(authorization);
|
|
38649
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
|
+
}
|
|
38650
38912
|
/** An inbound turn may receive OpenClaw memory only from an active owner DM. */
|
|
38651
38913
|
mayReadOpenClawMemoryForTurn(authorization) {
|
|
38652
38914
|
return this.cfg.memoryProvider !== "openclaw" || authorization.sender_membership === "active" && authorization.audience === "owner_dm";
|
|
@@ -38656,16 +38918,26 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38656
38918
|
* stays owner-DM-only; every provider failure degrades to empty context and
|
|
38657
38919
|
* never falls back to the runtime's retired plaintext JSON files.
|
|
38658
38920
|
*/
|
|
38659
|
-
async memoryForTurn(channelId, messageId, authorization, limit = 50) {
|
|
38921
|
+
async memoryForTurn(channelId, messageId, authorization, limit = 50, voice) {
|
|
38660
38922
|
if (!this.mayReadHistoryForTurn(messageId, authorization)) {
|
|
38661
38923
|
return { recent: [], facts: [] };
|
|
38662
38924
|
}
|
|
38925
|
+
if (voice) return { recent: this.voiceFrameTurns(voice.snapshot?.recent ?? []), facts: [] };
|
|
38663
38926
|
try {
|
|
38664
38927
|
return this.fenceMemory(channelId, await this.memory.context(channelId, limit), holdsManifestGrant(authorization));
|
|
38665
38928
|
} catch {
|
|
38666
38929
|
return { recent: [], facts: [] };
|
|
38667
38930
|
}
|
|
38668
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
|
+
}
|
|
38669
38941
|
/**
|
|
38670
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
|
|
38671
38943
|
* turn (manifest, fallback, greeting, relayed, constants-only alike). It never depends on a turn's grants.
|
|
@@ -38700,7 +38972,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38700
38972
|
if (!strict && !ctx.recent.some((t) => t.role === "agent")) return ctx;
|
|
38701
38973
|
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
38702
38974
|
const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
|
|
38703
|
-
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();
|
|
38704
38976
|
const fence = this.fenceContext(channelId);
|
|
38705
38977
|
const recent = ctx.recent.filter((t) => {
|
|
38706
38978
|
const m = meta.get(t.seq);
|
|
@@ -38719,7 +38991,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38719
38991
|
if (recent.length === 0) return false;
|
|
38720
38992
|
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
38721
38993
|
const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
|
|
38722
|
-
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();
|
|
38723
38995
|
return recent.some((t) => t.role !== "agent" && isRelayClassPeer({ peerId: t.peerId, modality: meta.get(t.seq)?.modality }, ownerId));
|
|
38724
38996
|
}
|
|
38725
38997
|
/**
|
|
@@ -39015,9 +39287,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39015
39287
|
writeServiceLog2("warn", "memory.recall_prepare_failed", {}, e);
|
|
39016
39288
|
}
|
|
39017
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
|
+
}
|
|
39018
39298
|
/** ws flush (synchronous). Q1 to Q3 and Q6 again at delivery, on the ORIGINAL message; Q4 lives in withConsumerRecall. */
|
|
39019
39299
|
recallForDelivery(m) {
|
|
39020
39300
|
if (this.unifiedOn) return void 0;
|
|
39301
|
+
if (this.frameIsVoiceScoped(m)) return void 0;
|
|
39021
39302
|
if (!this.recallProviderOn() || !this.notesReadable(m.authorization)) return void 0;
|
|
39022
39303
|
return this.recall.deliverable(m.message_id, m.channel_id);
|
|
39023
39304
|
}
|
|
@@ -39250,6 +39531,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39250
39531
|
/** ws flush (synchronous). The predicate and the owner scope again at delivery, on the ORIGINAL message (H-4, Review Focus 3). */
|
|
39251
39532
|
unifiedForDelivery(m) {
|
|
39252
39533
|
if (!this.unifiedProviderOn()) return void 0;
|
|
39534
|
+
if (this.frameIsVoiceScoped(m)) return void 0;
|
|
39253
39535
|
const a = m.authorization;
|
|
39254
39536
|
if (!(a?.memory_write === true || a?.relay_speaker === "owner")) return void 0;
|
|
39255
39537
|
const scopeId = this.ownerScopeNow(m.channel_id);
|
|
@@ -39485,6 +39767,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39485
39767
|
notesForDelivery(m) {
|
|
39486
39768
|
if (this.unifiedOn) return { generation: this.notes.appliedGeneration() };
|
|
39487
39769
|
const generation = this.notes.appliedGeneration();
|
|
39770
|
+
if (this.frameIsVoiceScoped(m)) return { generation };
|
|
39488
39771
|
if (!this.notesReadable(m.authorization)) return { generation };
|
|
39489
39772
|
const d = this.notes.deliverable(m.channel_id);
|
|
39490
39773
|
if (d.miss) {
|
|
@@ -39844,7 +40127,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39844
40127
|
* deferring fast reply was actually published — F3).
|
|
39845
40128
|
*/
|
|
39846
40129
|
async speakAndSettle(turn, plan, held, heldAfterAck, fastText, guidance, fastSpeaker, greeting) {
|
|
39847
|
-
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);
|
|
39848
40131
|
if (outcome === "answered") await this.settleHeldRuntimeMessage(held, false);
|
|
39849
40132
|
else await this.settleHeldRuntimeMessage(outcome === "deferred" ? heldAfterAck : held, true);
|
|
39850
40133
|
}
|
|
@@ -39863,19 +40146,29 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39863
40146
|
async settleFastReplies() {
|
|
39864
40147
|
while (this.fastReplyChains.size) await Promise.all([...this.fastReplyChains.values()]);
|
|
39865
40148
|
}
|
|
39866
|
-
/**
|
|
39867
|
-
|
|
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) {
|
|
39868
40157
|
this.spokenReplyIds.add(messageId);
|
|
39869
40158
|
try {
|
|
39870
40159
|
await this.send(channelId, text, messageId);
|
|
39871
40160
|
} finally {
|
|
39872
40161
|
this.spokenReplyIds.delete(messageId);
|
|
39873
40162
|
}
|
|
40163
|
+
if (turn.voiceScoped === true && turn.anyFragmentVerifyFailed !== true && !this.isOwnerVerifyFailed(turn.messageId)) {
|
|
40164
|
+
this.callBuffer.appendAgent(channelId, text, "fast_path");
|
|
40165
|
+
}
|
|
39874
40166
|
}
|
|
39875
40167
|
/** Speak a fixed acknowledgement line; failures are counted, never thrown. */
|
|
39876
|
-
async sendFastLine(
|
|
40168
|
+
async sendFastLine(turn, line) {
|
|
40169
|
+
const { channelId, messageId } = turn;
|
|
39877
40170
|
try {
|
|
39878
|
-
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);
|
|
39879
40172
|
this.fastPathSent++;
|
|
39880
40173
|
return true;
|
|
39881
40174
|
} catch (e) {
|
|
@@ -39889,8 +40182,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39889
40182
|
* the fast model's reply — or, in `ack_only`, the greeting template — is
|
|
39890
40183
|
* spoken, and the outcome decides whether the runtime answers next.
|
|
39891
40184
|
*
|
|
39892
|
-
* MUST NEVER throw: every failure path (
|
|
39893
|
-
*
|
|
40185
|
+
* MUST NEVER throw: every failure path (the LLM call, normalization,
|
|
40186
|
+
* or the send itself) is caught, counted in
|
|
39894
40187
|
* `fastPathFailed`, and logged WITHOUT the turn text or the model's reply.
|
|
39895
40188
|
* The `fastReplyId(...)` send id is deterministic, so a duplicate that
|
|
39896
40189
|
* slipped past the in-memory dedupe (e.g. a restart) resolves to the same
|
|
@@ -39904,20 +40197,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39904
40197
|
try {
|
|
39905
40198
|
if ((this.cfg.voiceFastPathMode ?? "full") === "ack_only") {
|
|
39906
40199
|
if (!isGreeting) return "failed";
|
|
39907
|
-
const ok = await this.sendFastLine(m
|
|
40200
|
+
const ok = await this.sendFastLine(m, ackOnlyGreeting(this.voiceAgentName(), speaker?.displayName));
|
|
39908
40201
|
return ok ? "answered" : "failed";
|
|
39909
40202
|
}
|
|
39910
40203
|
const fastLlm = this.fastLlm;
|
|
39911
40204
|
if (!fastLlm || !this.cfg.voice) return "failed";
|
|
39912
|
-
let memory;
|
|
39913
|
-
|
|
39914
|
-
|
|
39915
|
-
|
|
39916
|
-
|
|
39917
|
-
|
|
39918
|
-
}
|
|
39919
|
-
memory = { recent: [], facts: [] };
|
|
39920
|
-
}
|
|
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
|
+
};
|
|
39921
40212
|
}
|
|
39922
40213
|
const messages = buildFastMessages({
|
|
39923
40214
|
persona: this.cfg.voice.persona,
|
|
@@ -39940,7 +40231,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39940
40231
|
const refusal = !isGreeting && isRefusal(split3.reply);
|
|
39941
40232
|
const defer = split3.defer || refusal;
|
|
39942
40233
|
const reply = refusal ? deferAck(m.messageId) : split3.reply;
|
|
39943
|
-
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);
|
|
39944
40235
|
this.fastPathSent++;
|
|
39945
40236
|
if (defer) this.fastPathDeferred++;
|
|
39946
40237
|
if (refusal) this.fastPathRefusalDeferred++;
|
|
@@ -40004,6 +40295,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40004
40295
|
async memoryAtRelease(message) {
|
|
40005
40296
|
const prior = message.memory;
|
|
40006
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
|
+
}
|
|
40007
40307
|
let fresh;
|
|
40008
40308
|
try {
|
|
40009
40309
|
fresh = await this.memory.context(message.channel_id, _BridgeAgent.TURN_MEMORY_LIMIT);
|
|
@@ -40515,9 +40815,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40515
40815
|
});
|
|
40516
40816
|
}
|
|
40517
40817
|
/** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
|
|
40518
|
-
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId) {
|
|
40818
|
+
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId, voiceScoped) {
|
|
40519
40819
|
const held = await this.heldPosition(channelId);
|
|
40520
|
-
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 });
|
|
40521
40821
|
if (!(auth.audience === "team_room" && auth.tool_grants.length > 0)) return auth;
|
|
40522
40822
|
if (this.roomGrantEpochs.size >= ROOM_GRANT_TRACK_MAX || held.epoch === null || held.groupId === null) {
|
|
40523
40823
|
writeServiceLog2("warn", "room_grant.untracked");
|
|
@@ -40532,17 +40832,22 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40532
40832
|
* guess. The returned object and grant array are frozen before they cross the
|
|
40533
40833
|
* daemon boundary so one consumer cannot widen a later execution check.
|
|
40534
40834
|
*
|
|
40535
|
-
* Grants (architect ruling 2026-10-02):
|
|
40536
|
-
* 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
|
|
40537
40837
|
* (a) audience is owner_dm AND the sender is the channel's pinned DM
|
|
40538
40838
|
* counterpart (`channelOwner`, never "anomalous"). A server-marked
|
|
40539
40839
|
* `is_agent_owner` on a member who is NOT the DM's single human
|
|
40540
40840
|
* counterpart gets nothing; on the counterpart it is trusted (see
|
|
40541
|
-
* 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
|
|
40542
40844
|
* (b) the sender is this channel's server-marked relay (`is_relay`) and the
|
|
40543
40845
|
* relayed `speaker_identity_id` (passed by the caller only for relay
|
|
40544
40846
|
* turns) is that same pinned owner, whose roster row is the agent's
|
|
40545
|
-
* 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.
|
|
40546
40851
|
* Both need the roster fetched at `heldEpoch` (the agent's current MLS
|
|
40547
40852
|
* epoch for the channel), or the DM marked epoch-fresh at this ingest
|
|
40548
40853
|
* cycle's refresh (audit M-B4).
|
|
@@ -40586,9 +40891,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40586
40891
|
const ownerDirect = audience === "owner_dm" && senderId === ownerId;
|
|
40587
40892
|
const owner = roster?.get(ownerId);
|
|
40588
40893
|
const ownerRelayed = relaySpeakerId !== void 0 && sender.is_relay && relaySpeakerId === ownerId && owner !== void 0 && !owner.is_agent && owner.is_agent_owner;
|
|
40589
|
-
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;
|
|
40590
40896
|
if (ownerRelayed) relaySpeaker = "owner";
|
|
40591
|
-
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
|
|
40592
40899
|
// runtime does not offer memory_note at all. Not memoryConfigConfirmed:
|
|
40593
40900
|
// a turn the boot drain pulls before the boot refetch keeps its mark,
|
|
40594
40901
|
// and the read and write gates (notesProviderOn) hold it until then.
|
|
@@ -40605,11 +40912,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40605
40912
|
const binding = this.manifestBinding;
|
|
40606
40913
|
const boundNow = binding !== null && binding.state === "bound" && binding.counter === this.toolGrantCounter;
|
|
40607
40914
|
const signedTargets = boundNow ? binding.signedTargets : { endpoints: 0, databases: 0 };
|
|
40608
|
-
if (tool_grants === OWNER_DM_DEFAULT_READ_GRANTS) {
|
|
40609
|
-
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;
|
|
40610
40917
|
const r = effectiveToolGrants({
|
|
40611
40918
|
audience: "owner_dm",
|
|
40612
|
-
base:
|
|
40919
|
+
base: tool_grants,
|
|
40613
40920
|
manifest,
|
|
40614
40921
|
eligible,
|
|
40615
40922
|
manifestSwitchOn: this.ownerDmManifestGrants,
|
|
@@ -41331,6 +41638,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41331
41638
|
const acked = [];
|
|
41332
41639
|
for (const m of entries) {
|
|
41333
41640
|
try {
|
|
41641
|
+
if (m.voiceMode === false) this.callBuffer.reset(m.channelId);
|
|
41334
41642
|
const encId = m.envelope.enclave_id;
|
|
41335
41643
|
if (encId && !this.channelEnclaveRecorded.has(m.channelId)) {
|
|
41336
41644
|
this.channelEnclaveRecorded.add(m.channelId);
|
|
@@ -41488,6 +41796,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41488
41796
|
continue;
|
|
41489
41797
|
}
|
|
41490
41798
|
if (typeof parsed.text === "string") {
|
|
41799
|
+
if (parsed.text.trim() === VOICE_CALL_START_MARKER) this.callBuffer.reset(m.channelId);
|
|
41491
41800
|
const meta = this.freshMeta(m.channelId);
|
|
41492
41801
|
const senderType = m.envelope.sender_type;
|
|
41493
41802
|
const senderIsAgent = senderType === "agent" ? true : senderType === "human" ? false : void 0;
|
|
@@ -41529,23 +41838,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41529
41838
|
const senderName = this.freshRoster(m.channelId)?.get(
|
|
41530
41839
|
m.envelope.sender_id
|
|
41531
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
|
+
});
|
|
41532
41847
|
const ownerVerdict = await this.verifyOwnerTurn(m);
|
|
41533
41848
|
const turnAuthorization = await this.authorizeTurn(
|
|
41534
41849
|
m.channelId,
|
|
41535
41850
|
m.messageId,
|
|
41536
41851
|
m.envelope.sender_id,
|
|
41537
41852
|
ownerVerdict,
|
|
41538
|
-
speaker?.identityId
|
|
41853
|
+
speaker?.identityId,
|
|
41854
|
+
voiceScoped
|
|
41539
41855
|
);
|
|
41540
41856
|
await this.prepareNotesForTurn(m.channelId, turnAuthorization);
|
|
41541
|
-
const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization);
|
|
41542
41857
|
const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
|
|
41543
41858
|
const humanTurn = senderIsAgent !== true || speaker !== void 0;
|
|
41544
|
-
const fastPathEligible = Boolean(m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
41859
|
+
const fastPathEligible = Boolean(voiceScoped && m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
41545
41860
|
const turnText = parsed.text;
|
|
41546
41861
|
const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
|
|
41547
41862
|
const windowEligible = voiceWindowEligible({ voiceMode: m.voiceMode, speaker, transcribed: parsed.transcribed, voiceGuidance, isCallStart });
|
|
41548
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);
|
|
41549
41874
|
if (!coalescible) {
|
|
41550
41875
|
const kind2 = m.voiceMode === true || parsed.transcribed === true ? "voice" : "text";
|
|
41551
41876
|
if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId: m.channelId, messageId: m.messageId, text: parsed.text, authorization: turnAuthorization, kind: kind2 });
|
|
@@ -41561,7 +41886,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41561
41886
|
const fastGreeting = isCallStart && fastPathFiring;
|
|
41562
41887
|
const runtimeVoice = (ackSent) => m.voiceMode ? {
|
|
41563
41888
|
voice: true,
|
|
41564
|
-
|
|
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}` : ""}`
|
|
41565
41892
|
} : {};
|
|
41566
41893
|
const runtimeEntry = (ackSent, memory) => ({
|
|
41567
41894
|
channel_id: m.channelId,
|
|
@@ -41582,6 +41909,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41582
41909
|
// identify: the envelope's own claim (relay or on-device
|
|
41583
41910
|
// transcript) or a relay-attributed turn.
|
|
41584
41911
|
...parsed.transcribed === true || speaker !== void 0 ? { transcribed: true } : {},
|
|
41912
|
+
...voiceScoped ? { voice_scoped: true } : {},
|
|
41585
41913
|
...runtimeVoice(ackSent)
|
|
41586
41914
|
});
|
|
41587
41915
|
if (coalescible) {
|
|
@@ -41611,6 +41939,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41611
41939
|
acked.push(m.queueId);
|
|
41612
41940
|
continue;
|
|
41613
41941
|
}
|
|
41942
|
+
const bufferGeneration = this.callBuffer.generation(m.channelId);
|
|
41614
41943
|
const held = runtimeEntry(false, await runtimeMemory);
|
|
41615
41944
|
if (!await this.stageHeldRuntimeMessage(held)) continue;
|
|
41616
41945
|
this.noteVoiceCoalesced(m.messageId);
|
|
@@ -41621,7 +41950,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41621
41950
|
closed: false,
|
|
41622
41951
|
pending: Promise.resolve(),
|
|
41623
41952
|
purgeEpoch: purgeEpochAtReceipt.get(m.channelId),
|
|
41624
|
-
itemCapture: itemCaptureAtReceipt.get(m.channelId)
|
|
41953
|
+
itemCapture: itemCaptureAtReceipt.get(m.channelId),
|
|
41954
|
+
bufferGeneration
|
|
41625
41955
|
};
|
|
41626
41956
|
this.voiceCoalescing.set(windowKey, win);
|
|
41627
41957
|
const openedAt = Date.now();
|
|
@@ -41637,9 +41967,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41637
41967
|
const [firstId, ...tailIds] = win.ids;
|
|
41638
41968
|
if (tailIds.length) this.voiceCoalescedTails.set(firstId, tailIds);
|
|
41639
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);
|
|
41640
41978
|
if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
|
|
41641
41979
|
else await this.prepareRecallForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
|
|
41642
|
-
const
|
|
41980
|
+
const windowMemory = await this.memoryForTurn(channelId, firstId, turnAuthorization, void 0, { snapshot: anyFragmentVerifyFailed ? void 0 : windowSnapshot });
|
|
41981
|
+
const merged = { ...held, text, memory: windowMemory };
|
|
41643
41982
|
if (this.unifiedOn) {
|
|
41644
41983
|
const snap = win.itemCapture ?? { ...this.store.itemCapture(channelId), purgeEpoch: -1 };
|
|
41645
41984
|
const work = this.tapUnifiedVoice(merged, { stamp: this.ingestGeneration.get(firstId), capture: { ...snap, purgeEpoch: win.purgeEpoch ?? -1 } });
|
|
@@ -41667,7 +42006,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41667
42006
|
const release = this.reserveFastReply(channelId);
|
|
41668
42007
|
try {
|
|
41669
42008
|
await this.speakAndSettle(
|
|
41670
|
-
{ channelId, messageId: firstId, senderId, authorization: turnAuthorization },
|
|
42009
|
+
{ channelId, messageId: firstId, senderId, authorization: turnAuthorization, voiceScoped: voiceScoped && sameCall, voiceSnapshot: windowSnapshot, anyFragmentVerifyFailed },
|
|
41671
42010
|
plan,
|
|
41672
42011
|
merged,
|
|
41673
42012
|
{ ...merged, ...runtimeVoice(true) },
|
|
@@ -41699,7 +42038,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41699
42038
|
channelId: m.channelId,
|
|
41700
42039
|
messageId: m.messageId,
|
|
41701
42040
|
senderId: m.envelope.sender_id,
|
|
41702
|
-
authorization: turnAuthorization
|
|
42041
|
+
authorization: turnAuthorization,
|
|
42042
|
+
voiceScoped,
|
|
42043
|
+
voiceSnapshot
|
|
41703
42044
|
};
|
|
41704
42045
|
const guidanceForFast = voiceGuidance;
|
|
41705
42046
|
const greeting = fastGreeting;
|
|
@@ -42524,6 +42865,115 @@ ${channelId}`;
|
|
|
42524
42865
|
manifestConsumerAttached() {
|
|
42525
42866
|
return this.manifestConsumer !== null;
|
|
42526
42867
|
}
|
|
42868
|
+
/**
|
|
42869
|
+
* D28 (B-4): every socket that said hello, with whether its latest hello declared search_providers_v2 on the
|
|
42870
|
+
* manifest_grants_v1 predicate (ws-server.ts). The agent value is the AND over all of them, absent (false) with
|
|
42871
|
+
* none: unlike manifestConsumer, never newest wins. The per-delivery filter judges each socket by its own hello.
|
|
42872
|
+
*/
|
|
42873
|
+
searchProvidersSockets = /* @__PURE__ */ new Map();
|
|
42874
|
+
/**
|
|
42875
|
+
* The D28 value the files were last recomputed for; null until the first socket observation, so that observation
|
|
42876
|
+
* always recomputes (final review I-1), even when it is non-capable.
|
|
42877
|
+
*/
|
|
42878
|
+
searchProvidersState = null;
|
|
42879
|
+
searchCapabilityRefetchTimer = null;
|
|
42880
|
+
/** When the current burst's first change happened (valid while the timer is set). */
|
|
42881
|
+
searchCapabilityBurstStart = 0;
|
|
42882
|
+
/** The D28 value in the last report posted (null until one is); a change needs a refetch to reach the web. */
|
|
42883
|
+
lastReportedSearchProviders = null;
|
|
42884
|
+
noteSearchProvidersSocket(token, declared) {
|
|
42885
|
+
this.searchProvidersSockets.set(token, declared);
|
|
42886
|
+
this.onSearchProvidersMaybeChanged();
|
|
42887
|
+
}
|
|
42888
|
+
clearSearchProvidersSocket(token) {
|
|
42889
|
+
if (this.searchProvidersSockets.delete(token)) this.onSearchProvidersMaybeChanged();
|
|
42890
|
+
}
|
|
42891
|
+
/** D28: every attached runtime socket declared search_providers_v2; no socket attached means absent (false). */
|
|
42892
|
+
searchProvidersCapable() {
|
|
42893
|
+
if (this.searchProvidersSockets.size === 0) return false;
|
|
42894
|
+
for (const v of this.searchProvidersSockets.values()) if (!v) return false;
|
|
42895
|
+
return true;
|
|
42896
|
+
}
|
|
42897
|
+
/**
|
|
42898
|
+
* D19 item 3 (B-6): the search provider signed in the stored verified manifest in force (a pin loaded and a good
|
|
42899
|
+
* manifest), never the server payload; undefined with no pin, no manifest, or no signed search block. The
|
|
42900
|
+
* per-delivery filter (consumer-grants.ts withSearchCapability) and the S-28 report reason both read this.
|
|
42901
|
+
*/
|
|
42902
|
+
signedSearchProvider() {
|
|
42903
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
42904
|
+
return pin !== null ? this.toolGrantManifest?.config.search?.provider : void 0;
|
|
42905
|
+
}
|
|
42906
|
+
/** D19 item 4, D34: the in-force signed provider is not brave, and the attached runtime(s) lack the capability. */
|
|
42907
|
+
searchProviderUnsupported(capable) {
|
|
42908
|
+
if (capable) return false;
|
|
42909
|
+
const provider = this.signedSearchProvider();
|
|
42910
|
+
return provider !== void 0 && provider !== "brave";
|
|
42911
|
+
}
|
|
42912
|
+
/**
|
|
42913
|
+
* D35 (P-4): on a capability change, rewrite manifest-policy.json at once from the stored manifest, the kill
|
|
42914
|
+
* switches and the new capability, then a debounced refetch (one per burst) only when needed.
|
|
42915
|
+
*/
|
|
42916
|
+
onSearchProvidersMaybeChanged() {
|
|
42917
|
+
const capable = this.searchProvidersCapable();
|
|
42918
|
+
const prev = this.searchProvidersState;
|
|
42919
|
+
if (capable === prev) return;
|
|
42920
|
+
this.searchProvidersState = capable;
|
|
42921
|
+
this.recomputeManifestPolicy(capable);
|
|
42922
|
+
if (this.closingDown) return;
|
|
42923
|
+
if (prev === null && !capable) return;
|
|
42924
|
+
const now = Date.now();
|
|
42925
|
+
if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
|
|
42926
|
+
else this.searchCapabilityBurstStart = now;
|
|
42927
|
+
const delay = Math.max(0, Math.min(this.searchCapabilityRefetchDebounceMs, this.searchCapabilityBurstStart + this.searchCapabilityRefetchMaxWaitMs - now));
|
|
42928
|
+
this.searchCapabilityRefetchTimer = setTimeout(() => {
|
|
42929
|
+
this.searchCapabilityRefetchTimer = null;
|
|
42930
|
+
if (this.searchCapabilityRefetchNeeded()) void this.refetchRuntimeConfig();
|
|
42931
|
+
}, delay);
|
|
42932
|
+
this.searchCapabilityRefetchTimer.unref?.();
|
|
42933
|
+
}
|
|
42934
|
+
/**
|
|
42935
|
+
* A refetch re-renders skills.json (owner_dm_search_cap counts web_extract only with the capability, D39) and posts
|
|
42936
|
+
* the report. Needed under a manifest in force, or when the reported value is stale. Never before the boot fetch.
|
|
42937
|
+
*/
|
|
42938
|
+
searchCapabilityRefetchNeeded() {
|
|
42939
|
+
if (!this.bootConfigSettled || this.closingDown) return false;
|
|
42940
|
+
const inForce = (this.cfg.ownerGrantPin?.pin ?? null) !== null && this.toolGrantManifest !== null;
|
|
42941
|
+
return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable();
|
|
42942
|
+
}
|
|
42943
|
+
/**
|
|
42944
|
+
* D35: the local recompute. Widening (web_extract back in) only while the binding for the in-force manifest stands,
|
|
42945
|
+
* so a policy is never written ahead of its skills.json. Narrowing never waits: with no binding in force, a file that
|
|
42946
|
+
* lists web_extract loses it at once, so an older runtime never reads it as unreadable (which drops every grant).
|
|
42947
|
+
* Content-free; a write failure is logged and the debounced refetch tries again.
|
|
42948
|
+
*/
|
|
42949
|
+
recomputeManifestPolicy(capable) {
|
|
42950
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
42951
|
+
const manifest = pin !== null ? this.toolGrantManifest : null;
|
|
42952
|
+
try {
|
|
42953
|
+
if (manifest !== null && this.manifestBinding !== null && this.manifestBinding.counter === manifest.counter) {
|
|
42954
|
+
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, capable));
|
|
42955
|
+
return;
|
|
42956
|
+
}
|
|
42957
|
+
if (capable) return;
|
|
42958
|
+
const path = join17(runtimeConfigDir(this.home, this.cfg.identityId), MANIFEST_POLICY_FILE);
|
|
42959
|
+
if (!existsSync5(path)) return;
|
|
42960
|
+
let doc;
|
|
42961
|
+
try {
|
|
42962
|
+
doc = JSON.parse(readFileSync10(path, "utf8"));
|
|
42963
|
+
} catch {
|
|
42964
|
+
return;
|
|
42965
|
+
}
|
|
42966
|
+
const tools = doc?.manifest_tools;
|
|
42967
|
+
if (!Array.isArray(tools) || !tools.includes("web_extract")) return;
|
|
42968
|
+
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, {
|
|
42969
|
+
v: 1,
|
|
42970
|
+
manifest_tools: tools.filter((t) => typeof t === "string" && t !== "web_extract"),
|
|
42971
|
+
secret_path_mode: "enforce"
|
|
42972
|
+
});
|
|
42973
|
+
} catch (e) {
|
|
42974
|
+
writeServiceLog2("warn", "authz.search_capability", { state: ServiceLogEnum.of("policy_write_failed", ["policy_write_failed"]) }, e);
|
|
42975
|
+
}
|
|
42976
|
+
}
|
|
42527
42977
|
noteConsumerCapability(state) {
|
|
42528
42978
|
this.lastConsumerCapability = state;
|
|
42529
42979
|
if (state !== "ok") {
|
|
@@ -42715,6 +43165,8 @@ ${channelId}`;
|
|
|
42715
43165
|
this.supersedeRecheckTimers.clear();
|
|
42716
43166
|
if (this.redeliveryTimer !== null) clearTimeout(this.redeliveryTimer);
|
|
42717
43167
|
this.redeliveryTimer = null;
|
|
43168
|
+
if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
|
|
43169
|
+
this.searchCapabilityRefetchTimer = null;
|
|
42718
43170
|
this.pingHandle?.close();
|
|
42719
43171
|
if (this.safetyTimer) clearInterval(this.safetyTimer);
|
|
42720
43172
|
if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
|
|
@@ -42824,7 +43276,7 @@ ${channelId}`;
|
|
|
42824
43276
|
};
|
|
42825
43277
|
|
|
42826
43278
|
// src/config.ts
|
|
42827
|
-
import { readFileSync as
|
|
43279
|
+
import { readFileSync as readFileSync11, statSync as statSync5 } from "node:fs";
|
|
42828
43280
|
import { dirname as dirname6, join as join18 } from "node:path";
|
|
42829
43281
|
var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
|
|
42830
43282
|
var CONFIG_ERROR_MESSAGE = {
|
|
@@ -42846,7 +43298,7 @@ var ConfigError = class extends Error {
|
|
|
42846
43298
|
function loadConfig(opts) {
|
|
42847
43299
|
let raw;
|
|
42848
43300
|
try {
|
|
42849
|
-
raw =
|
|
43301
|
+
raw = readFileSync11(opts.credentialPath, "utf8");
|
|
42850
43302
|
} catch (e) {
|
|
42851
43303
|
const code = e?.code;
|
|
42852
43304
|
throw new ConfigError(code === "ENOENT" ? "credential_not_found" : "credential_unreadable");
|
|
@@ -42976,11 +43428,11 @@ import { z as z3 } from "zod";
|
|
|
42976
43428
|
|
|
42977
43429
|
// src/auth.ts
|
|
42978
43430
|
import { randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
42979
|
-
import { existsSync as existsSync6, readFileSync as
|
|
43431
|
+
import { existsSync as existsSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync2 } from "node:fs";
|
|
42980
43432
|
function resolveToken(opts) {
|
|
42981
43433
|
if (opts.token) return opts.token;
|
|
42982
43434
|
const f = opts.tokenFile;
|
|
42983
|
-
if (f && existsSync6(f)) return
|
|
43435
|
+
if (f && existsSync6(f)) return readFileSync12(f, "utf8").trim();
|
|
42984
43436
|
const tok = randomBytes11(32).toString("base64url");
|
|
42985
43437
|
if (f) writeFileSync2(f, tok, { mode: 384 });
|
|
42986
43438
|
return tok;
|
|
@@ -43076,6 +43528,7 @@ function withConsumerGrants(m, perToolConsumer) {
|
|
|
43076
43528
|
return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
|
|
43077
43529
|
}
|
|
43078
43530
|
var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
|
|
43531
|
+
var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
|
|
43079
43532
|
var CONSTANT_TOOL_NAMES = Object.freeze({
|
|
43080
43533
|
owner_dm: Object.freeze(["current_time", "web_search"]),
|
|
43081
43534
|
team_room: Object.freeze(["current_time"])
|
|
@@ -43098,6 +43551,15 @@ function withHarnessGrants(m, withheld) {
|
|
|
43098
43551
|
if (!a || a.tool_grants.length === 0) return m;
|
|
43099
43552
|
return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
|
|
43100
43553
|
}
|
|
43554
|
+
function withSearchCapability(m, consumerHasFeature, signedProvider) {
|
|
43555
|
+
if (consumerHasFeature) return m;
|
|
43556
|
+
const a = m.authorization;
|
|
43557
|
+
if (!a) return m;
|
|
43558
|
+
const stripSearch = signedProvider !== void 0 && signedProvider !== "brave";
|
|
43559
|
+
const kept = a.tool_grants.filter((g) => g.tool !== "web_extract" && !(stripSearch && g.tool === "web_search"));
|
|
43560
|
+
if (kept.length === a.tool_grants.length) return m;
|
|
43561
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
43562
|
+
}
|
|
43101
43563
|
|
|
43102
43564
|
// src/http-server.ts
|
|
43103
43565
|
function writeSse(res, msgs, onWriteError) {
|
|
@@ -43120,7 +43582,8 @@ data: ${JSON.stringify(msgs[i])}
|
|
|
43120
43582
|
}
|
|
43121
43583
|
function deliverSseBatch(res, batch, agent, onWriteError) {
|
|
43122
43584
|
const withheld = agent.harnessGrantsWithheld();
|
|
43123
|
-
const
|
|
43585
|
+
const provider = agent.signedSearchProvider();
|
|
43586
|
+
const left3 = writeSse(res, batch.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)), onWriteError);
|
|
43124
43587
|
const leftIds = new Set(left3.map((message) => message.message_id));
|
|
43125
43588
|
agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
|
|
43126
43589
|
if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
|
|
@@ -43391,7 +43854,10 @@ function createHttpServer(agent, opts) {
|
|
|
43391
43854
|
if (method === "GET" && path === "/v1/messages") {
|
|
43392
43855
|
const channel = url.searchParams.get("channel_id") ?? void 0;
|
|
43393
43856
|
const wait = Math.max(0, Math.min(50, Number(url.searchParams.get("wait") ?? 0)));
|
|
43394
|
-
|
|
43857
|
+
const drained = await agent.drain(channel, wait);
|
|
43858
|
+
const withheld = agent.harnessGrantsWithheld();
|
|
43859
|
+
const provider = agent.signedSearchProvider();
|
|
43860
|
+
return json(res, 200, drained.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)));
|
|
43395
43861
|
}
|
|
43396
43862
|
if (method === "GET" && path === "/v1/events") {
|
|
43397
43863
|
const channel = url.searchParams.get("channel_id") ?? void 0;
|
|
@@ -43565,6 +44031,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
43565
44031
|
consumer: "unknown",
|
|
43566
44032
|
perToolGrants: false,
|
|
43567
44033
|
manifestGrants: false,
|
|
44034
|
+
searchProviders: false,
|
|
43568
44035
|
// helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
|
|
43569
44036
|
deliveryOpen: helloWaitMs <= 0,
|
|
43570
44037
|
deliveryToken: /* @__PURE__ */ new Map(),
|
|
@@ -43618,9 +44085,13 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
43618
44085
|
continue;
|
|
43619
44086
|
}
|
|
43620
44087
|
const out = withConsumerNotes(
|
|
43621
|
-
|
|
43622
|
-
|
|
43623
|
-
|
|
44088
|
+
withSearchCapability(
|
|
44089
|
+
withManifestCapability(
|
|
44090
|
+
withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
|
|
44091
|
+
session.manifestGrants
|
|
44092
|
+
),
|
|
44093
|
+
session.searchProviders,
|
|
44094
|
+
agent.signedSearchProvider()
|
|
43624
44095
|
),
|
|
43625
44096
|
batch[i],
|
|
43626
44097
|
session,
|
|
@@ -43656,6 +44127,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
43656
44127
|
const cleanup = () => {
|
|
43657
44128
|
if (cleaned) return;
|
|
43658
44129
|
cleaned = true;
|
|
44130
|
+
session.closed = true;
|
|
43659
44131
|
if (helloTimer) clearTimeout(helloTimer);
|
|
43660
44132
|
agent.noteSessionClose();
|
|
43661
44133
|
unsubscribe();
|
|
@@ -43676,6 +44148,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
43676
44148
|
if (session.harness) agent.clearHarnessReport(session.harness);
|
|
43677
44149
|
if (session.perToolGrants) agent.clearPerToolConsumer(session);
|
|
43678
44150
|
if (session.manifestGrants) agent.clearManifestConsumer(session);
|
|
44151
|
+
agent.clearSearchProvidersSocket(session);
|
|
43679
44152
|
};
|
|
43680
44153
|
current = { sock, cleanup, sweepStale };
|
|
43681
44154
|
sock.on("close", cleanup);
|
|
@@ -43751,6 +44224,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
43751
44224
|
return;
|
|
43752
44225
|
}
|
|
43753
44226
|
if (m.type === "hello") {
|
|
44227
|
+
if (session.closed) return;
|
|
43754
44228
|
const report = m.harness === void 0 ? null : parseHarnessReport(m.harness);
|
|
43755
44229
|
if (report) {
|
|
43756
44230
|
session.harness = report;
|
|
@@ -43774,6 +44248,8 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
43774
44248
|
agent.clearManifestConsumer(session);
|
|
43775
44249
|
}
|
|
43776
44250
|
agent.noteConsumerCapability(manifest ? "ok" : session.harnessClaimed ? "harness_consumer" : "missing");
|
|
44251
|
+
session.searchProviders = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(SEARCH_PROVIDERS_FEATURE);
|
|
44252
|
+
agent.noteSearchProvidersSocket(session, session.searchProviders);
|
|
43777
44253
|
if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
|
|
43778
44254
|
agent.noteNotesConsumer();
|
|
43779
44255
|
}
|
|
@@ -43781,7 +44257,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
43781
44257
|
if (session.recallConsumer) agent.noteRecallConsumer();
|
|
43782
44258
|
session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
|
|
43783
44259
|
if (session.unifiedConsumer) agent.noteUnifiedConsumer();
|
|
43784
|
-
sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE] });
|
|
44260
|
+
sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE, SEARCH_PROVIDERS_FEATURE] });
|
|
43785
44261
|
if (!session.deliveryOpen) {
|
|
43786
44262
|
session.deliveryOpen = true;
|
|
43787
44263
|
flush();
|
|
@@ -43833,10 +44309,13 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
43833
44309
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
43834
44310
|
return;
|
|
43835
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);
|
|
43836
44314
|
const sendOpts = {
|
|
43837
44315
|
agentTurn: isAgentTurn(agent, m),
|
|
43838
44316
|
...typeof m.in_reply_to === "string" ? { inReplyTo: m.in_reply_to } : {},
|
|
43839
|
-
...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {}
|
|
44317
|
+
...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {},
|
|
44318
|
+
...callBufferReply ? { callBufferReply: true } : {}
|
|
43840
44319
|
};
|
|
43841
44320
|
if (m.a2a && typeof m.a2a === "object") {
|
|
43842
44321
|
const a = m.a2a;
|