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