@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/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
- headers: { "content-type": "application/json", ...this.key ? { authorization: `Bearer ${this.key}` } : {} },
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} ${CONTINUITY_CLAUSE}`;
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.channelId, turn.messageId, plan.line) ? "deferred" : "failed" : await this.runFastReply(turn, fastText, guidance, fastSpeaker, greeting);
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
- /** Send a line the voice fast path speaks: its memory record is "voice" (H1). */
39932
- async sendSpoken(channelId, text, messageId) {
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(channelId, messageId, line) {
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 (memory lookup, the LLM call,
39958
- * normalization, or the send itself) is caught, counted in
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.channelId, m.messageId, ackOnlyGreeting(this.voiceAgentName(), speaker?.displayName));
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
- if (!this.mayReadHistoryForTurn(m.messageId, m.authorization)) {
39979
- memory = { recent: [], facts: [] };
39980
- } else {
39981
- try {
39982
- memory = await this.memory.context(m.channelId, FAST_PATH_MEMORY_TURNS);
39983
- } catch {
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): OWNER_DM_DEFAULT_READ_GRANTS only when
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); OR
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 sender gets nothing.
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 || ownerRelayed) tool_grants = OWNER_DM_DEFAULT_READ_GRANTS;
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 && (this.cfg.memoryProvider ?? "securecoms") === "securecoms" && // Final review M-4: the live payload flag too, so with notes off the
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: OWNER_DM_DEFAULT_READ_GRANTS,
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
- guidance: ackSent ? `${voiceGuidance} ${FAST_PATH_SPOKEN_GUIDANCE}` : voiceGuidance
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 merged = { ...held, text };
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;