@livx.cc/agentx 0.99.100 → 0.99.102

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/cli.js CHANGED
@@ -709,16 +709,16 @@ ${entries.map((s) => " " + s).join("\n")}`);
709
709
  return blocks.length ? blocks.join("\n") : `(no ${label} found)`;
710
710
  }
711
711
  function fuzzyLineReplace(content, oldStr, newStr) {
712
- const norm3 = (s) => s.trim();
712
+ const norm4 = (s) => s.trim();
713
713
  const cl = content.split("\n");
714
- const ol = oldStr.split("\n").map(norm3);
714
+ const ol = oldStr.split("\n").map(norm4);
715
715
  while (ol.length && ol[ol.length - 1] === "") ol.pop();
716
716
  while (ol.length && ol[0] === "") ol.shift();
717
717
  if (!ol.length) return null;
718
718
  const matches = [];
719
719
  for (let i2 = 0; i2 + ol.length <= cl.length; i2++) {
720
720
  let ok = true;
721
- for (let j = 0; j < ol.length; j++) if (norm3(cl[i2 + j]) !== ol[j]) {
721
+ for (let j = 0; j < ol.length; j++) if (norm4(cl[i2 + j]) !== ol[j]) {
722
722
  ok = false;
723
723
  break;
724
724
  }
@@ -2954,7 +2954,7 @@ async function psGroupCpuSeconds(pgid) {
2954
2954
  try {
2955
2955
  return await winTreeCpuSeconds(pgid);
2956
2956
  } catch (e) {
2957
- log24.debug(`cpu probe failed for ${pgid}: ${e}`);
2957
+ log25.debug(`cpu probe failed for ${pgid}: ${e}`);
2958
2958
  return void 0;
2959
2959
  }
2960
2960
  }
@@ -2963,7 +2963,7 @@ async function psGroupCpuSeconds(pgid) {
2963
2963
  const out = await new Promise((res, rej) => execFile5("ps", ["-A", "-o", "pid=,ppid=,pgid=,time="], { timeout: 5e3, env: { ...process.env, LC_ALL: "C" } }, (e, so) => e ? rej(e) : res(String(so))));
2964
2964
  return sumTreeCpu(out, pgid);
2965
2965
  } catch (e) {
2966
- log24.debug(`ps cpu probe failed for group ${pgid}: ${e}`);
2966
+ log25.debug(`ps cpu probe failed for group ${pgid}: ${e}`);
2967
2967
  return void 0;
2968
2968
  }
2969
2969
  }
@@ -3112,7 +3112,7 @@ function makeRealShellTool(options) {
3112
3112
  proc.stderr?.on("data", collect);
3113
3113
  proc.on("error", (err2) => {
3114
3114
  if (err2?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean2(out)));
3115
- log24.debug("shell spawn error", err2);
3115
+ log25.debug("shell spawn error", err2);
3116
3116
  finish(`[exit 1] ${err2?.message ?? err2}${out ? "\n" + clean2(out) : ""}`);
3117
3117
  });
3118
3118
  proc.on("close", (code) => {
@@ -3191,7 +3191,7 @@ ${clean2(out) || "(no output yet)"}`;
3191
3191
  }
3192
3192
  ];
3193
3193
  }
3194
- var log24, clean2, DETACHED, shellArgv, SECRET_ENV_RE, _spawn, ShellJobRegistry, shellStatus, NO_JOB2;
3194
+ var log25, clean2, DETACHED, shellArgv, SECRET_ENV_RE, _spawn, ShellJobRegistry, shellStatus, NO_JOB2;
3195
3195
  var init_tools_shell = __esm({
3196
3196
  "src/tools.shell.ts"() {
3197
3197
  "use strict";
@@ -3201,7 +3201,7 @@ var init_tools_shell = __esm({
3201
3201
  init_shell_sandbox();
3202
3202
  init_tools_jobs();
3203
3203
  init_platform();
3204
- log24 = forComponent("shell");
3204
+ log25 = forComponent("shell");
3205
3205
  clean2 = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
3206
3206
  DETACHED = { stdio: ["ignore", "pipe", "pipe"], ...DETACH_OPTIONS };
3207
3207
  shellArgv = (sh, command) => ({ bin: sh.bin, args: sh.args(command), ...sh.spawnOptions ? { spawnOptions: sh.spawnOptions } : {} });
@@ -7697,9 +7697,9 @@ var VoiceEngine = class _VoiceEngine {
7697
7697
  this.ackProbe = null;
7698
7698
  this.adaptiveAckFired = false;
7699
7699
  const lead2 = m ? m[1] : bare;
7700
- const norm3 = lead2.replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
7700
+ const norm4 = lead2.replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
7701
7701
  const same = this.ackPickedFired ? bare.match(_VoiceEngine.leadingPhrase(this.lastAck)) : null;
7702
- if (_VoiceEngine.ACK_LEADING.test(norm3)) {
7702
+ if (_VoiceEngine.ACK_LEADING.test(norm4)) {
7703
7703
  const rest = m ? m[2] : "";
7704
7704
  this.diag("reflex_ack_suppressed", { lead: lead2.trim() });
7705
7705
  if (!rest.trim()) return "";
@@ -7813,8 +7813,8 @@ var VoiceEngine = class _VoiceEngine {
7813
7813
  this.adaptiveAckFired = false;
7814
7814
  this.ackProbe = null;
7815
7815
  if (heldProbe) {
7816
- const norm3 = heldProbe.replace(/\[emotion[^\]]*\]/gi, "").replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
7817
- if (_VoiceEngine.ACK_LEADING.test(norm3)) this.diag("reflex_ack_suppressed", { lead: heldProbe.trim() });
7816
+ const norm4 = heldProbe.replace(/\[emotion[^\]]*\]/gi, "").replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
7817
+ if (_VoiceEngine.ACK_LEADING.test(norm4)) this.diag("reflex_ack_suppressed", { lead: heldProbe.trim() });
7818
7818
  else this.speakDelta(heldProbe);
7819
7819
  }
7820
7820
  if (!this.speaking) return;
@@ -8352,14 +8352,14 @@ var VoiceEngine = class _VoiceEngine {
8352
8352
  if (!prev) return next;
8353
8353
  if (!next) return prev;
8354
8354
  const pw = prev.split(/\s+/), nw = next.split(/\s+/);
8355
- const norm3 = _VoiceEngine.normWord;
8355
+ const norm4 = _VoiceEngine.normWord;
8356
8356
  if (pw.length >= 2 && pw.length <= nw.length) {
8357
8357
  let pfx = true;
8358
- for (let i = 0; i < pw.length - 1; i++) if (norm3(pw[i]) !== norm3(nw[i])) {
8358
+ for (let i = 0; i < pw.length - 1; i++) if (norm4(pw[i]) !== norm4(nw[i])) {
8359
8359
  pfx = false;
8360
8360
  break;
8361
8361
  }
8362
- if (pfx && norm3(nw[pw.length - 1]).startsWith(norm3(pw[pw.length - 1]))) return next;
8362
+ if (pfx && norm4(nw[pw.length - 1]).startsWith(norm4(pw[pw.length - 1]))) return next;
8363
8363
  }
8364
8364
  if (_VoiceEngine.restates(prev, next)) {
8365
8365
  this.diag("merge_restated", { dropped: prev.slice(0, 60) });
@@ -8368,7 +8368,7 @@ var VoiceEngine = class _VoiceEngine {
8368
8368
  const max = Math.min(pw.length, nw.length);
8369
8369
  for (let k = max; k > 0; k--) {
8370
8370
  let match = true;
8371
- for (let i = 0; i < k; i++) if (norm3(pw[pw.length - k + i]) !== norm3(nw[i])) {
8371
+ for (let i = 0; i < k; i++) if (norm4(pw[pw.length - k + i]) !== norm4(nw[i])) {
8372
8372
  match = false;
8373
8373
  break;
8374
8374
  }
@@ -8420,11 +8420,11 @@ var VoiceEngine = class _VoiceEngine {
8420
8420
  * race), never worse. Fillers ("mhm") deliberately don't count as replies — see speakFiller. */
8421
8421
  refinalizes(next) {
8422
8422
  const pw = this.lastDispatchWords, nw = next.trim().split(/\s+/);
8423
- const norm3 = _VoiceEngine.normWord;
8423
+ const norm4 = _VoiceEngine.normWord;
8424
8424
  if (pw.length < 3 || nw.length < pw.length) return false;
8425
8425
  if (this.clock.now() - this.lastDispatchAt >= _VoiceEngine.DUP_FINAL_MS) return false;
8426
- for (let i = 0; i < pw.length - 1; i++) if (norm3(pw[i]) !== norm3(nw[i])) return false;
8427
- const pLast = norm3(pw[pw.length - 1]), nSame = norm3(nw[pw.length - 1]);
8426
+ for (let i = 0; i < pw.length - 1; i++) if (norm4(pw[i]) !== norm4(nw[i])) return false;
8427
+ const pLast = norm4(pw[pw.length - 1]), nSame = norm4(nw[pw.length - 1]);
8428
8428
  if (!nSame.startsWith(pLast)) return false;
8429
8429
  return nw.length > pw.length || nSame.length > pLast.length;
8430
8430
  }
@@ -9252,7 +9252,91 @@ configureLogging((name) => {
9252
9252
  // src/voice/instinct.ts
9253
9253
  init_logging();
9254
9254
  import { AIClient, DecideClient, JevAdapter, LlmDecideAdapter } from "ai.libx.js";
9255
- var log19 = forComponent("Instinct");
9255
+
9256
+ // src/voice/overlap.ts
9257
+ init_logging();
9258
+ var log19 = forComponent("Overlap");
9259
+ var norm2 = (s) => s.toLowerCase().replace(/[’']/g, "'").replace(/[^\p{L}\p{N}' -]+/gu, " ").replace(/\s+/g, " ").trim();
9260
+ var HARD = /\b(?:stop|wait|no|nope|nah|hold on|hang on|hold up|pause|cancel|quiet|enough|shut up|never ?mind|actually|but|sorry what|what|why|how|who|when|where|which|can you|could you|tell me|i mean|excuse me)\b/;
9261
+ var ACK = new Set("yeah yea yes yep yup ya ok okay k right sure true totally exactly definitely absolutely indeed mm mmm hmm mhm mm-hm uh-huh uh huh aha ah oh ooh wow nice cool great awesome perfect good fine interesting gotcha got it i see makes sense fair agreed correct of course for sure lol ha haha hahaha heh hehe yeah yeah alright all right sweet neat oh nice oh wow oh cool oh okay oh yeah oh right that's right that's true thats right thats true love it i know me too same".split(" "));
9262
+ var PHRASES = ["got it", "i see", "makes sense", "of course", "for sure", "all right", "that's right", "that's true", "thats right", "thats true", "love it", "i know", "me too", "uh huh"];
9263
+ function overlapRule(text) {
9264
+ if (/\?/.test(text)) return "cut";
9265
+ let t = norm2(text);
9266
+ if (!t) return void 0;
9267
+ if (HARD.test(t)) return "cut";
9268
+ for (const p of PHRASES) t = t.split(p).join(" ");
9269
+ const words3 = t.split(/[\s-]+/).filter(Boolean);
9270
+ if (words3.every((w) => ACK.has(w) || /^(?:ha)+h?$|^(?:he)+h?$|^m+(?:hm+)?$|^h?m+$/.test(w))) return "keep";
9271
+ return void 0;
9272
+ }
9273
+ function fastOverlapModel(ai, model = "groq/qwen/qwen3.8-27b") {
9274
+ return async (heard, signal) => {
9275
+ const prompt = 'Live voice call. The assistant is mid-sentence and the user just said this over it: "' + heard + '"\nIs it ONLY a listener cue \u2014 agreement, acknowledgement, a short reaction or laugh \u2014 so the assistant should keep talking? Answer no if they want the floor: a question, objection, correction, request, or new information.\nAnswer yes or no.';
9276
+ const reasoning = /qwen/.test(model) ? { providerOptions: { reasoning_effort: "none" } } : /gpt-oss/.test(model) ? { providerOptions: { reasoning_effort: "low" } } : {};
9277
+ const r = await ai.chat({ model, messages: [{ role: "user", content: prompt }], stream: false, signal, temperature: 0, maxTokens: /gpt-oss/.test(model) ? 200 : 3, ...reasoning });
9278
+ const a = (r.content ?? "").trim().toLowerCase();
9279
+ return /^yes/.test(a) ? "keep" : /^no/.test(a) ? "cut" : null;
9280
+ };
9281
+ }
9282
+ var OverlapJudgeOptions = class {
9283
+ /** The fast model for ambiguous overlaps; absent → ambiguous cuts. */
9284
+ model;
9285
+ /** A model verdict later than this is dropped → cut. */
9286
+ budgetMs = 300;
9287
+ onDecision = () => {
9288
+ };
9289
+ now = () => performance.now();
9290
+ };
9291
+ var OverlapJudge = class {
9292
+ constructor(options) {
9293
+ this.options = options;
9294
+ this.options = { ...new OverlapJudgeOptions(), ...options };
9295
+ }
9296
+ options;
9297
+ get o() {
9298
+ return this.options;
9299
+ }
9300
+ async judge(heard) {
9301
+ const t0 = this.o.now();
9302
+ const done = (verdict, by) => {
9303
+ const d = { verdict, by, ms: Math.round(this.o.now() - t0) };
9304
+ if (by === "late" || by === "error" || by === "none") log19.warn(`overlap "${heard.slice(0, 60)}": no verdict (${by}, ${d.ms}ms) \u2192 cut`);
9305
+ else log19.debug(`overlap "${heard.slice(0, 60)}": ${verdict} by ${by} ${d.ms}ms`);
9306
+ this.o.onDecision(heard, d);
9307
+ return d;
9308
+ };
9309
+ const rule = overlapRule(heard);
9310
+ if (rule) return done(rule, "rule");
9311
+ if (!this.o.model) return done("cut", "none");
9312
+ const ctl = new AbortController();
9313
+ let timer;
9314
+ const late = new Promise((r) => {
9315
+ timer = setTimeout(() => {
9316
+ ctl.abort();
9317
+ r("late");
9318
+ }, this.o.budgetMs);
9319
+ });
9320
+ try {
9321
+ const v = await Promise.race([this.o.model(heard, ctl.signal).catch((e) => {
9322
+ log19.warn(`overlap model failed: ${e.message}`);
9323
+ return "error";
9324
+ }), late]);
9325
+ if (v === "late" || v === "error") return done("cut", v);
9326
+ return v ? done(v, "model") : done("cut", "error");
9327
+ } finally {
9328
+ clearTimeout(timer);
9329
+ }
9330
+ }
9331
+ };
9332
+ function overlapModelFor(ai, env = process.env) {
9333
+ const model = env.AGENTX_CHANNEL_DECIDER_MODEL || void 0;
9334
+ return model || env.GROQ_API_KEY ? fastOverlapModel(ai, model) : void 0;
9335
+ }
9336
+ var softBarge = (mode) => mode !== "turn";
9337
+
9338
+ // src/voice/instinct.ts
9339
+ var log20 = forComponent("Instinct");
9256
9340
  function voiceDeciderOptions(env = process.env) {
9257
9341
  if (env.TYPESAFE_AI_KEY) return { apiKey: env.TYPESAFE_AI_KEY };
9258
9342
  if (env.GROQ_API_KEY) return { provider: "llm", model: "groq/openai/gpt-oss-120b", apiKey: env.GROQ_API_KEY };
@@ -9285,6 +9369,10 @@ var InstinctOptions = class {
9285
9369
  * (~300-400ms) and can never act. A hold that survives this window is the undecided case (a 1-word overlap
9286
9370
  * the engine would RESUME over at overlapResumeMs=700) — the one where a yield actually changes the outcome. */
9287
9371
  overlapSettleMs = 250;
9372
+ /** Soft barge-in's fast yes/no model (overlap.ts fastOverlapModel) for overlaps the 0ms rules leave open, inside
9373
+ * `overlapBudgetMs`; late/failed → yield. Absent → the generic Choice decides them (slower; no verdict → engine default). */
9374
+ overlapModel;
9375
+ overlapBudgetMs = 300;
9288
9376
  /** ackFor waits this long for the host's ackOutcome before treating the ack as unspoken. Hosts set it to the engine's
9289
9377
  * last chance to fire (bod-voice: 2×adaptiveAckMs — past that a pending pick is dropped as pick_late). */
9290
9378
  ackSettleMs = 1500;
@@ -9457,7 +9545,8 @@ var Instinct = class {
9457
9545
  const gen = ++this.overlapGen;
9458
9546
  if (this.o.overlapSettleMs > 0) await new Promise((r) => setTimeout(r, this.o.overlapSettleMs));
9459
9547
  if (gen !== this.overlapGen) return;
9460
- const d = await this.ask(
9548
+ const fast = overlapRule(heard) ?? this.o.overlapModel;
9549
+ const d = fast ? await this.judgeOverlap(heard) : await this.ask(
9461
9550
  "overlap",
9462
9551
  heard,
9463
9552
  { user_said_over_us: heard },
@@ -9474,6 +9563,12 @@ var Instinct = class {
9474
9563
  this.o.onDecision({ ...d, acted: acted("keep_talking", this.o.onKeepTalking(heard)) });
9475
9564
  } else if (d) this.o.onDecision(d);
9476
9565
  }
9566
+ /** Rules + fast model (OverlapJudge) as an overlap decision; undecided → yield (today's hard barge). */
9567
+ async judgeOverlap(heard) {
9568
+ const j = await new OverlapJudge({ model: this.o.overlapModel, budgetMs: this.o.overlapBudgetMs, now: this.o.now }).judge(heard);
9569
+ const choice = j.verdict === "keep" ? "keep_talking" : "yield";
9570
+ return { kind: "overlap", heard, choice, probabilities: { [choice]: 1 }, ms: j.ms };
9571
+ }
9477
9572
  /** The engine's hold ended (it ceded the floor, resumed over a stalled overlap, began a new turn, or was
9478
9573
  * interrupted) — drop any pending/in-flight overlap consult. Hosts call this on every hold exit. */
9479
9574
  overlapEnded() {
@@ -9512,13 +9607,13 @@ var Instinct = class {
9512
9607
  * unfinished partial is no turn to react to — the interjection itself is the reply. */
9513
9608
  pickAck(ctx) {
9514
9609
  if (this.interjecting) {
9515
- log19.debug(`ack: cut-in turn \u2192 silent (engine context "${ctx.text.slice(0, 40)}" is the previous final)`);
9610
+ log20.debug(`ack: cut-in turn \u2192 silent (engine context "${ctx.text.slice(0, 40)}" is the previous final)`);
9516
9611
  return Promise.resolve(null);
9517
9612
  }
9518
9613
  const text = ctx.text.trim();
9519
9614
  const p = (async () => {
9520
9615
  const intentP = ctx.barged ? this.bargeIntent(text).catch((e) => {
9521
- log19.warn(`barge intent for the ack failed \u2014 situational ack: ${e.message}`);
9616
+ log20.warn(`barge intent for the ack failed \u2014 situational ack: ${e.message}`);
9522
9617
  return null;
9523
9618
  }) : null;
9524
9619
  const d = await this.ask(
@@ -9540,7 +9635,7 @@ var Instinct = class {
9540
9635
  settle = r;
9541
9636
  });
9542
9637
  const timer = setTimeout(() => {
9543
- log19.debug(`ack: no engine outcome in ${this.o.ackSettleMs}ms \u2014 not claimed`);
9638
+ log20.debug(`ack: no engine outcome in ${this.o.ackSettleMs}ms \u2014 not claimed`);
9544
9639
  settle(null);
9545
9640
  }, this.o.ackSettleMs);
9546
9641
  void spoken.then(() => clearTimeout(timer));
@@ -9589,7 +9684,7 @@ var Instinct = class {
9589
9684
  });
9590
9685
  return { kind, heard, choice: answers.act.choice, probabilities: answers.act.probabilities, ms: Math.round(this.o.now() - t0) };
9591
9686
  } catch (e) {
9592
- log19.warn(`${kind} decision failed \u2014 falling back to engine defaults: ${e.message}`);
9687
+ log20.warn(`${kind} decision failed \u2014 falling back to engine defaults: ${e.message}`);
9593
9688
  if (rethrow) throw e;
9594
9689
  return null;
9595
9690
  }
@@ -9598,7 +9693,7 @@ var Instinct = class {
9598
9693
 
9599
9694
  // src/voice/unsaid.ts
9600
9695
  init_logging();
9601
- var log20 = forComponent("Unsaid");
9696
+ var log21 = forComponent("Unsaid");
9602
9697
  var STOP2 = new Set("that this with have from they them their there then than what when were will would your about into just also been some only over more most very which while after before because these those does here".split(" "));
9603
9698
  var contentStems = (s) => (s.toLowerCase().match(/[a-z0-9']+/g) ?? []).filter((w) => w.length >= 4 && !STOP2.has(w)).map((w) => w.slice(0, 4));
9604
9699
  var SAID_OVERLAP = 0.6;
@@ -9638,7 +9733,7 @@ async function unsaidClauses(pieces, spoken, decider, timeoutMs = 5e3) {
9638
9733
  return p < CONVEYED_MIN;
9639
9734
  }).map((x) => x.c);
9640
9735
  } catch (e) {
9641
- log20.warn(`unsaid judge failed \u2014 only near-zero-overlap clauses count as unsaid: ${e.message}`);
9736
+ log21.warn(`unsaid judge failed \u2014 only near-zero-overlap clauses count as unsaid: ${e.message}`);
9642
9737
  return fallback();
9643
9738
  } finally {
9644
9739
  clearTimeout(timer);
@@ -9722,13 +9817,13 @@ function scrubSpokenJunk(text, final = false) {
9722
9817
  return s;
9723
9818
  }
9724
9819
  function isRepeatOf(sentence, spoken) {
9725
- const norm3 = (x) => x.replace(/\[[a-z_ ]+\]/gi, "").replace(/[^\p{L}\p{N}]+/gu, " ").trim().toLowerCase();
9726
- const s = norm3(sentence);
9727
- return s.replace(/ /g, "").length >= 16 && spoken.split(SENTENCE_SPLIT).some((x) => norm3(x) === s);
9820
+ const norm4 = (x) => x.replace(/\[[a-z_ ]+\]/gi, "").replace(/[^\p{L}\p{N}]+/gu, " ").trim().toLowerCase();
9821
+ const s = norm4(sentence);
9822
+ return s.replace(/ /g, "").length >= 16 && spoken.split(SENTENCE_SPLIT).some((x) => norm4(x) === s);
9728
9823
  }
9729
9824
 
9730
9825
  // src/duplex.ts
9731
- var log21 = forComponent("DuplexAgent");
9826
+ var log22 = forComponent("DuplexAgent");
9732
9827
  function describeCall(call) {
9733
9828
  const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
9734
9829
  const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
@@ -9916,7 +10011,7 @@ function isOfferAcceptance(text) {
9916
10011
  }
9917
10012
  var isQuestion = (s) => /\?["'”’)\]]*$/.test(s.trim());
9918
10013
  var OFFER_OVERRUN = 150;
9919
- var VOICE_SYSTEM_PROMPT = 'You are a spoken voice assistant \u2014 the user HEARS everything you say. Use short sentences. One idea per sentence. No markdown, no bullet lists, no code blocks, no headings, no emoji. Never emit stage directions or parenthetical asides about your own process \u2014 nothing like "(waiting for the result...)" or "(checking)"; while work runs, either say it as plain speech or end your turn.\nThis holds even when asked to "print", "list", "show", or "make a table" \u2014 there is no screen for the spoken channel. Speak it as flowing prose ("Tuesday is half a meter, Wednesday a bit less\u2026"), or if they truly need it on screen, route it to Act to render. Never emit dashes or pipes into speech.\nKeep turns SHORT \u2014 one to three sentences, then stop. Never lecture, enumerate cases, or add caveats unprompted. Conversation is a fast exchange: give the one thing asked, and let the user pull more if they want it.\nYou have three cognitive tiers \u2014 like a human brain:\n\u2022 YOU (reflex) \u2014 instant, lightweight. Handle greetings, simple questions, status checks, QuickLook.\n\u2022 `Act` \u2014 your hands. A background worker with its own configured tools and access to the user\'s environment (files and shell{{WORKER_WEB}}). Use for reading, editing, searching, running tasks, building \u2014 any real work.\n{{THINK_SLOT}}\nWhen you are unsure whether you can do or access something, do NOT assume and do NOT claim a capability you have not confirmed. To check what you can do, QuickLook `capabilities` (instant \u2014 it lists your worker\'s real tools) and answer from that. Never promise an ability that is not in your capabilities; if it is not there, tell the user plainly you can\'t. To actually DO real work, call `Act`. When the user mentions their project, folder, files, or environment ("this project", "the current folder", "my code"), call `Act` IMMEDIATELY \u2014 do not ask for paths or details the worker can discover itself. Never pretend to have done the work or invent results \u2014 the worker\'s report is your only source.\nYou cannot mute the microphone or stop voice capture yourself \u2014 no tool does it. If the user asks you to stop listening or turn the voice off, never claim you did: tell them to say exactly "voice off" (handled by the app directly), or type /voice.\nYou are NOT a knowledge base. For any question whose answer needs SPECIFIC verifiable facts you do not already have in hand \u2014 how to build/configure/implement something, exact API, library, entitlement, command or option names, current events, or particular numbers, dates, or names \u2014 do NOT answer from your own memory: you will confidently make things up (a fake API, a wrong entitlement, an event that did not happen). Route it to `Act`, which can search and verify, and speak only what its report says. DELEGATION RULE \u2014 decide for yourself, the user never has to push: if you cannot answer confidently from the conversation plus trivial well-known knowledge, do NOT refuse and do NOT guess \u2014 dispatch `Act` immediately with a clear brief and say you are checking. Anything needing CURRENT data (weather, news, prices, dates, sky/astronomy, "right now"), real computation, or verification is an automatic dispatch \u2014 never a refusal. The user should never need to say "search the web" or "think harder" to make you act; needing fresh or verified information IS the trigger. Refuse only what your worker genuinely cannot do (check `capabilities`), and say why. Answer inline ONLY for general conversation, chit-chat, and trivia you are sure of, or facts you can see via QuickLook. When elaborating on a completed task ("tell me more", "the gist"), stay strictly within what that result actually said \u2014 if the user asks for something the result did not cover, that is NEW information: dispatch `Act`, do not improvise.\nALWAYS react before you work: the FIRST thing in your turn is a brief spoken acknowledgement of what you heard and what you are about to do ("got it \u2014 opening that now", "sure, let me pull it up", "okay, checking"). NEVER call a tool (Act, Think, QuickLook) silently \u2014 the user must hear you react before you go quiet to work. After dispatching Act or Think, that same one short sentence IS your turn \u2014 end it and do not wait for the result. Exactly ONE short line: never stack a second acknowledgement, and never narrate your own presence or status while waiting ("I\'m here", "let me see", "still checking" right after you already acked). One clean ack then silence reads as competent; repeated check-ins read as nervous.\nA completed task speaks its OWN result to the user (the worker voices what matters as it finishes) \u2014 you do NOT re-voice clean task results. A FAILED or INCOMPLETE task still arrives as a "[task t1 failed] \u2026" event for you to handle. The completed result stays in YOUR context \u2014 it is yours to draw on. When the user follows up ("tell me more", "what else", "and?"), answer FROM that result first: you already have the detail, so elaborate on what you have. Do NOT spawn a fresh worker to re-search or re-gather what you were just handed. Re-dispatch ONLY when genuinely new information is needed \u2014 e.g. the user wants the full contents of a SPECIFIC source, which is one WebFetch of that URL, not a brand-new search. "[task t1 progress] \u2026" events are interim status, NOT results \u2014 on a genuinely LONG wait you MAY give one brief half-sentence aside, but only occasionally; silence while working is normal and fine (the user knows you are on it). Do NOT narrate every step or re-announce yourself. Never present progress as a finished result.\nCRITICAL: while a task is still running you have NO answer yet \u2014 never state a specific result of any kind (a number, size, count, name, path, or value). The real answer exists ONLY once the task finishes (the worker speaks it, and `TaskStatus` then returns it); inventing one meanwhile (a made-up disk size, commit count, etc.) is a serious error. Until then, only acknowledge and wait.\nNever read raw file paths, diffs, or code aloud verbatim.\nDo NOT end every turn with the same canned offer ("want a rundown?", "want the steps?"). Offer once at most; if the user pushes back, repeats themselves, or sounds unsatisfied ("you know what I mean?", "think deeper", "are you sure?"), do NOT re-offer the same thing \u2014 change approach: dispatch `Act`/`Think` to actually dig in, or ask one concrete clarifying question. Repeating a non-answer is worse than silence. When the user says you looked in the wrong place, or doubts a "not found", do not defend the search you did: dispatch `Act` with their hint (the place, the id, the tool they name). Only say you checked or searched something you actually checked with a tool this conversation \u2014 name exactly what you looked at.\n"[task t1 asks] \u2026" events are QUESTIONS from a background task \u2014 relay to the user in your own words, short, then end your turn. When the user answers, call `AnswerTask` with that id and their answer. NEVER answer on the user\'s behalf for permissions or risky operations; if their reply is ambiguous, confirm first.\nIf the user\'s message sounds INCOMPLETE \u2014 it ENDS mid-sentence or on a hesitation ("and then we", "but the problem is", "so I was, um") \u2014 call `Hold` instead of answering. A hesitation INSIDE a finished message ("can you, um, open it?") does not make it incomplete, and an unclear or odd but finished message is not a fragment: answer it, or ask one short question \u2014 never Hold on it. This keeps listening for the rest of their thought. Only respond with substance when you have a complete question or request.\nDispatch discipline: send ONE self-contained task per request \u2014 a single worker with the full brief beats several workers with fragments (each worker starts fresh and re-discovers context). NEVER dispatch a worker just to read files or gather information \u2014 workers explore and discover context themselves; pass on what you already know and let one worker do the whole job. Split into parallel tasks only when the user asks for genuinely independent things. When a task completes, report its result and stop \u2014 do NOT dispatch follow-up work (verification, polish, extras) the user did not ask for, unless the report itself signals failure or doubt.\nDo not fire a second Act/Think for work already in flight, and NEVER spawn a second task to re-count, cross-check, or verify a result a worker already gave you \u2014 trust its answer; a single question gets ONE task. Call `TaskStatus` at most ONCE per turn; if a task is still running, give one brief status aside and end the turn \u2014 never poll it again and again in a loop. "Still on it" is ONLY for a repeat check on a task you already acknowledged earlier \u2014 NEVER as the ack when you first dispatch work ("still" is wrong when nothing was running yet; use a fresh "on it"/"got it, checking" instead). Use `CancelTask` when the user asks to stop something \u2014 to stop everything ("stop all", "cancel everything") pass id "all". When the user only ADDS to or CORRECTS a request whose task is still running (a detail, a path, a name, a spelling), call `Act` with `refines` set to that task id \u2014 it updates the running worker in place; never cancel and re-dispatch for a refinement.\nPRIORITY: when the user says goodbye or wants to end/finish/wrap up the session ("ok bye", "that\'s all", "let\'s finish", "let\'s end", "goodnight", "exit", "wrap up"), call `ExitSession` IMMEDIATELY \u2014 do not act, do not check status, just exit.\nFor TRIVIAL instant lookups only \u2014 current time, git branch, listing a folder, peeking at a small file, or checking your own `capabilities`/tools \u2014 use `QuickLook` (instant, no task). Whenever the user asks what you can do or whether you have some ability, QuickLook `capabilities` and answer from that \u2014 never guess. Anything requiring searching, reasoning, running commands, or editing goes through `Act`.\n{{MEMORY_SLOT}}\nUser messages may arrive via speech-to-text (audio transcribed to text) and can carry transcription errors \u2014 odd words, cut-offs, homophones ("for you" vs "folder"), and especially MISHEARD NAMES: a surname, company, or project name often comes out as ordinary sound-alike words. Read for INTENT, not surface text, and do not silently rewrite what they said. If a message seems garbled, surprising, or only half-parses, or a word sounds like a name you already know (from memory or this conversation) but is spelled differently, treat it with suspicion \u2014 do NOT act on the odd word blindly or improvise content from it; briefly confirm what they meant ("did you mean\u2026?") and wait. A one-line confirm beats a confident wrong answer. NEVER save to memory a name, spelling, or term you only HEARD (it may be a transcription error) \u2014 when the user spells something aloud, read the spelling back and get a yes before storing it.';
10014
+ var VOICE_SYSTEM_PROMPT = 'You are a spoken voice assistant \u2014 the user HEARS everything you say. Use short sentences. One idea per sentence. No markdown, no bullet lists, no code blocks, no headings, no emoji. Never emit stage directions or parenthetical asides about your own process \u2014 nothing like "(waiting for the result...)" or "(checking)"; while work runs, either say it as plain speech or end your turn.\nThis holds even when asked to "print", "list", "show", or "make a table" \u2014 there is no screen for the spoken channel. Speak it as flowing prose ("Tuesday is half a meter, Wednesday a bit less\u2026"), or if they truly need it on screen, route it to Act to render. Never emit dashes or pipes into speech.\nKeep turns SHORT \u2014 one to three sentences, then stop. Never lecture, enumerate cases, or add caveats unprompted. Conversation is a fast exchange: give the one thing asked, and let the user pull more if they want it.\nYou have three cognitive tiers \u2014 like a human brain:\n\u2022 YOU (reflex) \u2014 instant, lightweight. Handle greetings, simple questions, status checks, QuickLook.\n\u2022 `Act` \u2014 your hands. A background worker with its own configured tools and access to the user\'s environment (files and shell{{WORKER_WEB}}). Use for reading, editing, searching, running tasks, building \u2014 any real work.\n{{THINK_SLOT}}\nWhen you are unsure whether you can do or access something, do NOT assume and do NOT claim a capability you have not confirmed. To check what you can do, QuickLook `capabilities` (instant \u2014 it lists your worker\'s real tools) and answer from that. Never promise an ability that is not in your capabilities; if it is not there, tell the user plainly you can\'t. To actually DO real work, call `Act`. When the user mentions their project, folder, files, or environment ("this project", "the current folder", "my code"), call `Act` IMMEDIATELY \u2014 do not ask for paths or details the worker can discover itself. Never pretend to have done the work or invent results \u2014 the worker\'s report is your only source.\nYou cannot mute the microphone or stop voice capture yourself \u2014 no tool does it. If the user asks you to stop listening or turn the voice off, never claim you did: tell them to say exactly "voice off" (handled by the app directly), or type /voice.\nYou are NOT a knowledge base. For any question whose answer needs SPECIFIC verifiable facts you do not already have in hand \u2014 how to build/configure/implement something, exact API, library, entitlement, command or option names, current events, or particular numbers, dates, or names \u2014 do NOT answer from your own memory: you will confidently make things up (a fake API, a wrong entitlement, an event that did not happen). Route it to `Act`, which can search and verify, and speak only what its report says. DELEGATION RULE \u2014 decide for yourself, the user never has to push: if you cannot answer confidently from the conversation plus trivial well-known knowledge, do NOT refuse and do NOT guess \u2014 dispatch `Act` immediately with a clear brief and say you are checking. Anything needing CURRENT data (weather, news, prices, dates, sky/astronomy, "right now"), real computation, or verification is an automatic dispatch \u2014 never a refusal. The user should never need to say "search the web" or "think harder" to make you act; needing fresh or verified information IS the trigger. Refuse only what your worker genuinely cannot do (check `capabilities`), and say why. Answer inline ONLY for general conversation, chit-chat, and trivia you are sure of, or facts you can see via QuickLook. When elaborating on a completed task ("tell me more", "the gist"), stay strictly within what that result actually said \u2014 if the user asks for something the result did not cover, that is NEW information: dispatch `Act`, do not improvise.\nALWAYS react before you work: the FIRST thing in your turn is a brief spoken acknowledgement of what you heard and what you are about to do ("got it \u2014 opening that now", "sure, let me pull it up", "okay, checking"). NEVER call a tool (Act, Think, QuickLook) silently \u2014 the user must hear you react before you go quiet to work. After dispatching Act or Think, that same one short sentence IS your turn \u2014 end it and do not wait for the result. Exactly ONE short line: never stack a second acknowledgement, and never narrate your own presence or status while waiting ("I\'m here", "let me see", "still checking" right after you already acked). One clean ack then silence reads as competent; repeated check-ins read as nervous.\nA completed task speaks its OWN result to the user (the worker voices what matters as it finishes) \u2014 you do NOT re-voice clean task results. A FAILED or INCOMPLETE task still arrives as a "[task t1 failed] \u2026" event for you to handle. The completed result stays in YOUR context \u2014 it is yours to draw on. When the user follows up ("tell me more", "what else", "and?"), answer FROM that result first: you already have the detail, so elaborate on what you have. Do NOT spawn a fresh worker to re-search or re-gather what you were just handed. Re-dispatch ONLY when genuinely new information is needed \u2014 e.g. the user wants the full contents of a SPECIFIC source, which is one WebFetch of that URL, not a brand-new search. "[task t1 progress] \u2026" events are interim status, NOT results \u2014 on a genuinely LONG wait you MAY give one brief half-sentence aside, but only occasionally; silence while working is normal and fine (the user knows you are on it). Do NOT narrate every step or re-announce yourself. Never present progress as a finished result.\nCRITICAL: while a task is still running you have NO answer yet \u2014 never state a specific result of any kind (a number, size, count, name, path, or value). The real answer exists ONLY once the task finishes (the worker speaks it, and `TaskStatus` then returns it); inventing one meanwhile (a made-up disk size, commit count, etc.) is a serious error. Until then, only acknowledge and wait.\nNever read raw file paths, diffs, or code aloud verbatim.\nDo NOT end every turn with the same canned offer ("want a rundown?", "want the steps?"). Offer once at most; if the user pushes back, repeats themselves, or sounds unsatisfied ("you know what I mean?", "think deeper", "are you sure?"), do NOT re-offer the same thing \u2014 change approach: dispatch `Act`/`Think` to actually dig in, or ask one concrete clarifying question. Repeating a non-answer is worse than silence. A short yes or agreement right after you asked or offered something answers THAT question: act on it now (dispatch the work it needs) \u2014 never re-ask it, rephrase it, or narrow it into a new question. When the user says you looked in the wrong place, or doubts a "not found", do not defend the search you did: dispatch `Act` with their hint (the place, the id, the tool they name). Only say you checked or searched something you actually checked with a tool this conversation \u2014 name exactly what you looked at.\n"[task t1 asks] \u2026" events are QUESTIONS from a background task \u2014 relay to the user in your own words, short, then end your turn. When the user answers, call `AnswerTask` with that id and their answer. NEVER answer on the user\'s behalf for permissions or risky operations; if their reply is ambiguous, confirm first.\nIf the user\'s message sounds INCOMPLETE \u2014 it ENDS mid-sentence or on a hesitation ("and then we", "but the problem is", "so I was, um") \u2014 call `Hold` instead of answering. A hesitation INSIDE a finished message ("can you, um, open it?") does not make it incomplete, and an unclear or odd but finished message is not a fragment: answer it, or ask one short question \u2014 never Hold on it. This keeps listening for the rest of their thought. Only respond with substance when you have a complete question or request.\nDispatch discipline: send ONE self-contained task per request \u2014 a single worker with the full brief beats several workers with fragments (each worker starts fresh and re-discovers context). NEVER dispatch a worker just to read files or gather information \u2014 workers explore and discover context themselves; pass on what you already know and let one worker do the whole job. Split into parallel tasks only when the user asks for genuinely independent things. When a task completes, report its result and stop \u2014 do NOT dispatch follow-up work (verification, polish, extras) the user did not ask for, unless the report itself signals failure or doubt.\nDo not fire a second Act/Think for work already in flight, and NEVER spawn a second task to re-count, cross-check, or verify a result a worker already gave you \u2014 trust its answer; a single question gets ONE task. Call `TaskStatus` at most ONCE per turn; if a task is still running, give one brief status aside and end the turn \u2014 never poll it again and again in a loop. "Still on it" is ONLY for a repeat check on a task you already acknowledged earlier \u2014 NEVER as the ack when you first dispatch work ("still" is wrong when nothing was running yet; use a fresh "on it"/"got it, checking" instead). Use `CancelTask` when the user asks to stop something \u2014 to stop everything ("stop all", "cancel everything") pass id "all". When the user only ADDS to or CORRECTS a request whose task is still running (a detail, a path, a name, a spelling), call `Act` with `refines` set to that task id \u2014 it updates the running worker in place; never cancel and re-dispatch for a refinement.\nPRIORITY: when the user says goodbye or wants to end/finish/wrap up the session ("ok bye", "that\'s all", "let\'s finish", "let\'s end", "goodnight", "exit", "wrap up"), call `ExitSession` IMMEDIATELY \u2014 do not act, do not check status, just exit.\nFor TRIVIAL instant lookups only \u2014 current time, git branch, listing a folder, peeking at a small file, or checking your own `capabilities`/tools \u2014 use `QuickLook` (instant, no task). Whenever the user asks what you can do or whether you have some ability, QuickLook `capabilities` and answer from that \u2014 never guess. Anything requiring searching, reasoning, running commands, or editing goes through `Act`.\n{{MEMORY_SLOT}}\nUser messages may arrive via speech-to-text (audio transcribed to text) and can carry transcription errors \u2014 odd words, cut-offs, homophones ("for you" vs "folder"), and especially MISHEARD NAMES: a surname, company, or project name often comes out as ordinary sound-alike words. Read for INTENT, not surface text, and do not silently rewrite what they said. If a message seems garbled, surprising, or only half-parses, or a word sounds like a name you already know (from memory or this conversation) but is spelled differently, treat it with suspicion \u2014 do NOT act on the odd word blindly or improvise content from it; briefly confirm what they meant ("did you mean\u2026?") and wait. A one-line confirm beats a confident wrong answer. NEVER save to memory a name, spelling, or term you only HEARD (it may be a transcription error) \u2014 when the user spells something aloud, read the spelling back and get a yes before storing it.';
9920
10015
  var THINK_GUIDANCE = "\u2022 `Think` \u2014 your brain. A premium reasoning model, FAR more expensive than Act. Reserve it for open-ended architecture/design questions, or a problem Act already FAILED at. ALL implementation work \u2014 coding, refactoring, debugging, edge cases, tests \u2014 goes to Act; Act is highly capable. Never send the same work to both.";
9921
10016
  var THINK_DISABLED_GUIDANCE = "(Think tier is not available \u2014 use Act for all escalations.)";
9922
10017
  var VOICE_STYLE_CONVERSATIONAL = `Speak like a person in a live conversation, not an assistant reading a script. React first, then deliver: a quick impulsive beat ("oh nice", "hmm, hold on", "ah, got it") before the substance. Use contractions always. Vary sentence length \u2014 some very short. Light fillers and backchannels are fine ("mm-hm", "right", "let's see") but at most one per reply \u2014 never stack them. When you escalate to Act or Think, say it like a human would ("hang on, let me actually dig into that \u2014 gimme a minute") instead of announcing a task. When a result comes back, react to it like you just found out ("okay so \u2014 turns out\u2026"). Match the user's energy: a quick question gets a quick answer \u2014 a few words is a perfectly good turn. Prefer a short answer plus an offer ("want the details?") over covering everything. Never narrate your own mechanics (no "I will now act", no task ids out loud).`;
@@ -10083,7 +10178,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10083
10178
  const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
10084
10179
  if (m) {
10085
10180
  this.fabricationCut = true;
10086
- log21.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
10181
+ log22.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
10087
10182
  this.reflexBuf = this.reflexBuf.slice(0, Math.max(m.index, this.reflexForwarded));
10088
10183
  this.pumpReflex(true);
10089
10184
  return;
@@ -10207,7 +10302,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10207
10302
  this.leakHit = true;
10208
10303
  this.leakCuts.push(leak.trim().slice(0, 40));
10209
10304
  const why = this.cutKind === "claim" ? "stated a result no task has given yet" : this.cutKind === "restart" ? "started a second, restated reply" : "spoke its own analysis";
10210
- log21.warn(`reflex ${why} ("${leak.trim().slice(0, 40)}\u2026") \u2014 cut before TTS`);
10305
+ log22.warn(`reflex ${why} ("${leak.trim().slice(0, 40)}\u2026") \u2014 cut before TTS`);
10211
10306
  this.notify("diag", this.cutKind === "claim" ? "result_claim" : this.cutKind === "restart" ? "reply_restart" : "analysis_leak", { text: leak.trim().slice(0, 80), at: this.turnSpoken.trim() ? "mid_reply" : "before_speech" });
10212
10307
  const next = this.stepMarks.find((k) => k > at);
10213
10308
  if (next === void 0) {
@@ -10224,6 +10319,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10224
10319
  if (!out) return;
10225
10320
  if (out.trim()) this.spokeThisTurn = true;
10226
10321
  this.turnSpoken += out;
10322
+ this.noteVoiced(this.turnSpoken);
10227
10323
  this.emitHost({ kind: "text_delta", message: out });
10228
10324
  }
10229
10325
  /** Leak verdict for one complete sentence: false = speak, true = cut, undefined = the judge is still out.
@@ -10247,7 +10343,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10247
10343
  this.notify("diag", "leak_judge", { text: s.slice(0, 80), p: Math.round(p * 100) / 100, ms: Math.round(performance.now() - t0) });
10248
10344
  return p >= LEAK_JUDGE_THRESHOLD;
10249
10345
  }, (e) => {
10250
- log21.warn(`leak judge failed \u2014 speaking the sentence: ${e.message}`);
10346
+ log22.warn(`leak judge failed \u2014 speaking the sentence: ${e.message}`);
10251
10347
  return false;
10252
10348
  }).then((leak) => {
10253
10349
  clearTimeout(timer);
@@ -10319,7 +10415,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10319
10415
  let i = t.length - 1;
10320
10416
  while (i >= this.turnBase && !(t[i].role === "assistant" && typeof t[i].content === "string" && t[i].content.includes(key))) i--;
10321
10417
  if (i < this.turnBase) {
10322
- log21.warn("analysis leak cut, but its text was not found in the transcript to scrub");
10418
+ log22.warn("analysis leak cut, but its text was not found in the transcript to scrub");
10323
10419
  continue;
10324
10420
  }
10325
10421
  const m = t[i];
@@ -10374,7 +10470,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10374
10470
  const res = await this.voice.send(content);
10375
10471
  const fb = this.options.reflexFallbackModel, model = this.voice.options.model;
10376
10472
  if (res.finishReason !== "error" || !rateLimited(res.error) || !fb || fb === model || fb.includes("/") && model.includes("/") && fb.split("/")[0] !== model.split("/")[0] || this.turnDispatched || this.turnSpoken.trim()) return res;
10377
- log21.warn(`reflex ${model} rate-limited \u2014 re-running the turn on ${fb}`);
10473
+ log22.warn(`reflex ${model} rate-limited \u2014 re-running the turn on ${fb}`);
10378
10474
  this.notify("diag", "reflex_fallback", { from: model, to: fb });
10379
10475
  this.voice.transcript.length = base;
10380
10476
  this.voice.options.model = fb;
@@ -10407,7 +10503,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10407
10503
  try {
10408
10504
  await this.voice.send(reminder);
10409
10505
  } catch (e) {
10410
- log21.warn(`follow-through nudge failed: ${e instanceof Error ? e.message : e}`);
10506
+ log22.warn(`follow-through nudge failed: ${e instanceof Error ? e.message : e}`);
10411
10507
  } finally {
10412
10508
  const i = this.voice.transcript.findIndex((m, k) => k >= base && m.role === "user" && m.content === reminder);
10413
10509
  if (i >= 0) this.voice.transcript.splice(i, 1);
@@ -10468,7 +10564,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10468
10564
  return {
10469
10565
  preToolUse: (call) => {
10470
10566
  if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
10471
- log21.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
10567
+ log22.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
10472
10568
  this.abortSpeculation();
10473
10569
  return { block: true, reason: "Speculative turn aborted." };
10474
10570
  }
@@ -10542,7 +10638,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10542
10638
  try {
10543
10639
  await this.voice.send(reminder);
10544
10640
  } catch (e) {
10545
- log21.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
10641
+ log22.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
10546
10642
  } finally {
10547
10643
  this.nudging = false;
10548
10644
  const i = this.voice.transcript.findIndex((m, k) => k >= base && m.role === "user" && m.content === reminder);
@@ -10597,7 +10693,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10597
10693
  const accepted = this.offerOpen && this.parkedRedeliver.length > 0 && this.parkedRedeliver.every((p) => p.task === this.offer.task) && (isOfferAcceptance(content) || isTrivialBarge(content));
10598
10694
  const judge = accepted ? void 0 : this.options.bargeIntent;
10599
10695
  return (accepted ? Promise.resolve("resume") : judge ? judge(content).catch((e) => {
10600
- log21.warn(`bargeIntent judge failed \u2014 using the allowlist: ${e.message}`);
10696
+ log22.warn(`bargeIntent judge failed \u2014 using the allowlist: ${e.message}`);
10601
10697
  return fallback();
10602
10698
  }) : Promise.resolve(fallback())).then((intent) => {
10603
10699
  this.notify("diag", "barge_intent", { intent, text: content.slice(0, 80) });
@@ -10622,7 +10718,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10622
10718
  return this.sendTurn(content, false, rec, ext);
10623
10719
  });
10624
10720
  }
10625
- return this.sendTurn(content, false, rec, ext);
10721
+ return this.sendTurn(typeof content === "string" ? content + this.acceptNote(content) : content, false, rec, ext);
10626
10722
  }
10627
10723
  /** Host-named user turns (send opts.id), so a later send can replace one. Bounded; a replace that arrives
10628
10724
  * BEFORE its target (the host's dispatch hops can reorder) leaves a pre-superseded record behind. */
@@ -10793,7 +10889,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10793
10889
  try {
10794
10890
  res = await this.voice.send(spec.text);
10795
10891
  } catch (e) {
10796
- log21.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
10892
+ log22.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
10797
10893
  } finally {
10798
10894
  this.voice.options.signal = prevSignal;
10799
10895
  }
@@ -10807,7 +10903,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10807
10903
  if (d === "abort") {
10808
10904
  if (this.voice.transcript.length > base) this.voice.transcript.length = base;
10809
10905
  this.speculativeAbortedCalls++;
10810
- log21.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
10906
+ log22.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
10811
10907
  if (this.spec === spec) this.spec = void 0;
10812
10908
  return res ?? empty;
10813
10909
  }
@@ -10875,6 +10971,7 @@ Update: ${brief}`;
10875
10971
  this.bargeMark = this.parkedRedeliver.length;
10876
10972
  this.reflexCut = false;
10877
10973
  this.offerOpen = false;
10974
+ this.asked = "";
10878
10975
  }
10879
10976
  const parked = [];
10880
10977
  for (const rec of this.tasks.values())
@@ -10918,6 +11015,7 @@ Update: ${brief}`;
10918
11015
  if (say) {
10919
11016
  this.offerOpen = false;
10920
11017
  this.options.host?.notify?.({ kind: "speak_utterance", message: say });
11018
+ this.noteVoiced(say);
10921
11019
  if (rec) {
10922
11020
  this.voicedBy.set(say.trim(), rec.id);
10923
11021
  if (this.voicedBy.size > 64) this.voicedBy.delete(this.voicedBy.keys().next().value);
@@ -10930,6 +11028,7 @@ Update: ${brief}`;
10930
11028
  this.offer = { text: rec.capOffer = this.options.spokenOffer, task: rec.id };
10931
11029
  this.offerOpen = true;
10932
11030
  this.options.host?.notify?.({ kind: "speak_utterance", message: this.offer.text });
11031
+ this.noteVoiced(this.offer.text);
10933
11032
  }
10934
11033
  this.notify("diag", "delivery_budget", { task: rec.id, spoke: rec.voicedChars, parked: rest.length });
10935
11034
  } else if (rec.deliveryParked === "barge") this.parkedRedeliver.push({ text: seg, task: rec.id });
@@ -10956,6 +11055,20 @@ Update: ${brief}`;
10956
11055
  offer = { text: "", task: "" };
10957
11056
  /** The cap offer was the last thing voiced (no delivery or turn has spoken since) — a plain yes accepts it (send). */
10958
11057
  offerOpen = false;
11058
+ /** The last thing voiced (reflex reply or delivery), when it ended on a question — a plain yes answers it (acceptNote). */
11059
+ asked = "";
11060
+ noteVoiced(text) {
11061
+ const last = text.trim().split(SENTENCE_SPLIT).at(-1) ?? "";
11062
+ this.asked = isQuestion(last) ? last.trim() : "";
11063
+ }
11064
+ /** A plain yes to the question just voiced, tied to it in the user's line: left to the reflex, a bare "Oh, yeah." after
11065
+ * an offer read as ambiguous and the offer was asked again instead of done (Para phone call 2026-10-13). */
11066
+ acceptNote(content) {
11067
+ const q2 = this.asked;
11068
+ this.asked = "";
11069
+ return q2 && isOfferAcceptance(content) ? `
11070
+ [this answers the question you just asked \u2014 "${q2}" \u2014 with yes: act on it now (do what it offered, or pass the answer on to the task that asked); do not ask it again.]` : "";
11071
+ }
10959
11072
  /** The task's outcome now belongs to its integration turn (failed / stopped early): take back its parked pieces and
10960
11073
  * keep it from being revived — a "go on" must not replay raw segments of a result the reflex is about to explain. */
10961
11074
  dropParked(rec) {
@@ -10982,6 +11095,7 @@ Update: ${brief}`;
10982
11095
  if (!text) return;
10983
11096
  this.offerOpen = false;
10984
11097
  this.options.host?.notify?.({ kind: "speak_utterance", message: text });
11098
+ this.noteVoiced(text);
10985
11099
  this.notify("diag", "parked_redelivered", { chars: text.length });
10986
11100
  }
10987
11101
  /** Resolve when all queued voice turns AND all in-flight worker tasks have settled (tests, graceful shutdown). */
@@ -11172,7 +11286,7 @@ Another agent just implemented the above. Independently check the CURRENT state
11172
11286
  this.notify("task_verify", `task ${id}: verifying`, { id });
11173
11287
  const cres = await new Agent(checkerOpts).run(checkBrief);
11174
11288
  if (cres.finishReason !== "stop") {
11175
- log21.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
11289
+ log22.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
11176
11290
  this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
11177
11291
  }
11178
11292
  const sum = (a = 0, b = 0) => a + b;
@@ -11308,7 +11422,7 @@ Another agent just implemented the above. Independently check the CURRENT state
11308
11422
  rec.status = "done";
11309
11423
  rec.result = res.text;
11310
11424
  const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
11311
- log21.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
11425
+ log22.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
11312
11426
  this.notify("task_done", `task ${id} (${rec.label}) completed`, {
11313
11427
  id,
11314
11428
  text: res.text,
@@ -11342,14 +11456,14 @@ Another agent just implemented the above. Independently check the CURRENT state
11342
11456
  foldIfSuperseded(rec, note) {
11343
11457
  if (rec.deliveryParked !== "moved_on") return false;
11344
11458
  if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
11345
- log21.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
11459
+ log22.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
11346
11460
  return true;
11347
11461
  }
11348
11462
  failTask(rec, msg) {
11349
11463
  this.dropAsk(rec.id);
11350
11464
  rec.status = "error";
11351
11465
  rec.result = msg;
11352
- log21.warn(`task ${rec.id} failed: ${msg}`);
11466
+ log22.warn(`task ${rec.id} failed: ${msg}`);
11353
11467
  this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`, { id: rec.id });
11354
11468
  if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
11355
11469
  this.dropParked(rec);
@@ -11647,7 +11761,7 @@ Another agent just implemented the above. Independently check the CURRENT state
11647
11761
  init_llm();
11648
11762
  init_logging();
11649
11763
  init_models();
11650
- var log22 = forComponent("ModelRouter");
11764
+ var log23 = forComponent("ModelRouter");
11651
11765
  var ModelRouterOptions = class {
11652
11766
  classifier = jevClassifier();
11653
11767
  /** Capability tiers → model id (any provider). basic/standard run the main session; premium is only ever an offload. */
@@ -11731,10 +11845,10 @@ var ModelRouter = class {
11731
11845
  if (tier === "premium") d.offload = this.state.offload && (c.sameThread ?? 0) >= o.sameThreadAt ? "continue" : "fresh";
11732
11846
  } catch (e) {
11733
11847
  const msg = e instanceof Error ? e.message : String(e);
11734
- log22.warn(`classifier failed, falling back to standard: ${msg}`);
11848
+ log23.warn(`classifier failed, falling back to standard: ${msg}`);
11735
11849
  d = { tier: "standard", model: o.models.standard, probs: {}, fallback: msg, classifyMs: Math.round(performance.now() - t0) };
11736
11850
  }
11737
- log22.info(`route ${d.tier}${d.offload ? `(${d.offload})` : ""} \xB7 argmax=${d.choice ?? "-"} \xB7 p=${JSON.stringify(d.probs)}${d.sameThread != null ? ` \xB7 same=${d.sameThread.toFixed(2)}` : ""} \xB7 ${d.classifyMs}ms`);
11851
+ log23.info(`route ${d.tier}${d.offload ? `(${d.offload})` : ""} \xB7 argmax=${d.choice ?? "-"} \xB7 p=${JSON.stringify(d.probs)}${d.sameThread != null ? ` \xB7 same=${d.sameThread.toFixed(2)}` : ""} \xB7 ${d.classifyMs}ms`);
11738
11852
  return d;
11739
11853
  }
11740
11854
  /** Run one user turn on `agent` with routing. Returns a RunResult carrying `costUsd` + `routing`. */
@@ -11786,7 +11900,7 @@ ${text}`;
11786
11900
  const answer = res.text || `(premium sub-agent finished with no reply; finishReason=${res.finishReason})`;
11787
11901
  agent.transcript.push({ role: "user", content }, { role: "assistant", content: answer });
11788
11902
  this.state.offload = { messages: child.transcript, mainLen: agent.transcript.length, question: text };
11789
- log22.info(`offload ${cont ? "continue" : "fresh"} done \xB7 ${res.finishReason} \xB7 ${res.steps} steps`);
11903
+ log23.info(`offload ${cont ? "continue" : "fresh"} done \xB7 ${res.finishReason} \xB7 ${res.steps} steps`);
11790
11904
  const costUsd = o.costOf(main2, briefUsage) + o.costOf(o.models.premium, res.usage);
11791
11905
  return { ...res, text: answer, messages: agent.transcript, usage: addUsage(briefUsage, res.usage), costUsd, routing: d };
11792
11906
  }
@@ -11802,7 +11916,7 @@ The request:
11802
11916
  ${text}`);
11803
11917
  return { text: r.text.trim() || text, usage: r.usage };
11804
11918
  } catch (e) {
11805
- log22.warn(`brief failed, sending the raw request: ${e instanceof Error ? e.message : String(e)}`);
11919
+ log23.warn(`brief failed, sending the raw request: ${e instanceof Error ? e.message : String(e)}`);
11806
11920
  return { text };
11807
11921
  } finally {
11808
11922
  agent.transcript = snap;
@@ -11847,7 +11961,7 @@ async function jevAvailable(opts = {}) {
11847
11961
  const { env } = await loadJev(opts);
11848
11962
  return JEV_KEYS.some((k) => env[k]?.trim());
11849
11963
  } catch (e) {
11850
- log22.debug(`jev unavailable: ${e instanceof Error ? e.message : String(e)}`);
11964
+ log23.debug(`jev unavailable: ${e instanceof Error ? e.message : String(e)}`);
11851
11965
  return false;
11852
11966
  }
11853
11967
  }
@@ -12076,7 +12190,7 @@ init_logging();
12076
12190
  init_platform();
12077
12191
  import { spawn } from "child_process";
12078
12192
  import { createHash } from "crypto";
12079
- var log23 = forComponent("mcp");
12193
+ var log24 = forComponent("mcp");
12080
12194
  var PROTOCOL_VERSION = "2025-06-18";
12081
12195
  var DEFAULT_TIMEOUT_MS = 3e4;
12082
12196
  var DEFAULT_CALL_TIMEOUT_MS = 30 * 6e4;
@@ -12108,7 +12222,7 @@ var StdioTransport = class {
12108
12222
  proc.stdout.setEncoding("utf8");
12109
12223
  proc.stdout.on("data", (chunk) => this.onData(chunk));
12110
12224
  proc.stderr.setEncoding("utf8");
12111
- proc.stderr.on("data", (chunk) => log23.debug(`[${command}] stderr:`, chunk.trimEnd()));
12225
+ proc.stderr.on("data", (chunk) => log24.debug(`[${command}] stderr:`, chunk.trimEnd()));
12112
12226
  proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
12113
12227
  proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
12114
12228
  }
@@ -12122,7 +12236,7 @@ var StdioTransport = class {
12122
12236
  try {
12123
12237
  this.dispatch(JSON.parse(line));
12124
12238
  } catch (e) {
12125
- log23.debug("dropping non-JSON line from MCP server:", line, e);
12239
+ log24.debug("dropping non-JSON line from MCP server:", line, e);
12126
12240
  }
12127
12241
  }
12128
12242
  }
@@ -12132,7 +12246,7 @@ var StdioTransport = class {
12132
12246
  try {
12133
12247
  p2?.onProgress?.(msg.params ?? {});
12134
12248
  } catch (e) {
12135
- log23.debug("onProgress handler threw", e);
12249
+ log24.debug("onProgress handler threw", e);
12136
12250
  }
12137
12251
  return;
12138
12252
  }
@@ -12170,7 +12284,7 @@ var StdioTransport = class {
12170
12284
  const onAbort = () => {
12171
12285
  if (!this.pending.delete(id)) return;
12172
12286
  clearTimeout(timer);
12173
- this.notify("notifications/cancelled", { requestId: id, reason: String(signal?.reason?.message ?? "cancelled") }).catch((e) => log23.debug("cancel notify failed", e));
12287
+ this.notify("notifications/cancelled", { requestId: id, reason: String(signal?.reason?.message ?? "cancelled") }).catch((e) => log24.debug("cancel notify failed", e));
12174
12288
  reject(abortError(signal));
12175
12289
  };
12176
12290
  const done = () => signal?.removeEventListener("abort", onAbort);
@@ -12194,7 +12308,7 @@ var StdioTransport = class {
12194
12308
  try {
12195
12309
  this.proc?.stdin?.end();
12196
12310
  } catch (e) {
12197
- log23.debug("stdin end failed", e);
12311
+ log24.debug("stdin end failed", e);
12198
12312
  }
12199
12313
  if (!(IS_WIN && killTree(this.proc?.pid, "SIGTERM"))) this.proc?.kill();
12200
12314
  }
@@ -12229,7 +12343,7 @@ var HttpTransport = class {
12229
12343
  return await this.post({ jsonrpc: "2.0", id, method, params: opts.onProgress ? withProgressToken(params, id) : params }, false, signal, opts.onProgress);
12230
12344
  } catch (e) {
12231
12345
  if (opts.signal?.aborted) {
12232
- void this.notify("notifications/cancelled", { requestId: id, reason: String(opts.signal.reason?.message ?? "cancelled") }).catch((err2) => log23.debug("cancel notify failed", err2));
12346
+ void this.notify("notifications/cancelled", { requestId: id, reason: String(opts.signal.reason?.message ?? "cancelled") }).catch((err2) => log24.debug("cancel notify failed", err2));
12233
12347
  throw abortError(opts.signal);
12234
12348
  }
12235
12349
  throw e;
@@ -12272,7 +12386,7 @@ function sseMessage(line) {
12272
12386
  try {
12273
12387
  return JSON.parse(trimmed.slice(5).trim());
12274
12388
  } catch (e) {
12275
- log23.debug("skipping unparseable SSE data line", e);
12389
+ log24.debug("skipping unparseable SSE data line", e);
12276
12390
  return void 0;
12277
12391
  }
12278
12392
  }
@@ -12282,7 +12396,7 @@ async function readSseResponse(resp, id, onProgress) {
12282
12396
  try {
12283
12397
  onProgress?.(obj.params ?? {});
12284
12398
  } catch (e) {
12285
- log23.debug("onProgress handler threw", e);
12399
+ log24.debug("onProgress handler threw", e);
12286
12400
  }
12287
12401
  return void 0;
12288
12402
  }
@@ -12381,7 +12495,7 @@ async function mountWithDeadline(name, cfg, mountTimeoutMs) {
12381
12495
  return { name, client, tools, specs, serverInfo: init?.serverInfo, config: cfg };
12382
12496
  })(), mountTimeoutMs, name);
12383
12497
  } catch (e) {
12384
- await client.close().catch((err2) => log23.debug(`close after failed mount of "${name}": ${err2}`));
12498
+ await client.close().catch((err2) => log24.debug(`close after failed mount of "${name}": ${err2}`));
12385
12499
  throw e;
12386
12500
  }
12387
12501
  }
@@ -12392,15 +12506,15 @@ function validEntries(servers) {
12392
12506
  return Object.entries(servers).filter(([name, cfg]) => {
12393
12507
  if (!cfg || cfg.disabled) return false;
12394
12508
  if (!cfg.command && !cfg.url) {
12395
- log23.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
12509
+ log24.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
12396
12510
  return false;
12397
12511
  }
12398
12512
  return true;
12399
12513
  });
12400
12514
  }
12401
12515
  function logMountFailure(name, e) {
12402
- if (e instanceof McpAuthError) log23.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
12403
- else log23.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
12516
+ if (e instanceof McpAuthError) log24.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
12517
+ else log24.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
12404
12518
  }
12405
12519
  async function mountMcpServers(servers = {}, opts = {}) {
12406
12520
  const entries = validEntries(servers);
@@ -12410,7 +12524,7 @@ async function mountMcpServers(servers = {}, opts = {}) {
12410
12524
  const name = entries[i][0];
12411
12525
  if (r.status === "fulfilled") {
12412
12526
  out.push(r.value);
12413
- log23.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
12527
+ log24.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
12414
12528
  } else logMountFailure(name, r.reason);
12415
12529
  });
12416
12530
  return out;
@@ -12450,7 +12564,7 @@ var McpPool = class {
12450
12564
  const prev = this.warm.get(key);
12451
12565
  if (prev) {
12452
12566
  clearTimeout(prev.timer);
12453
- if (prev.client !== client) void prev.client.close().catch((err2) => log23.debug(`warm-pool replace close failed: ${err2}`));
12567
+ if (prev.client !== client) void prev.client.close().catch((err2) => log24.debug(`warm-pool replace close failed: ${err2}`));
12454
12568
  }
12455
12569
  const e = { client, timer: void 0 };
12456
12570
  this.warm.set(key, e);
@@ -12467,7 +12581,7 @@ var McpPool = class {
12467
12581
  const e = this.warm.get(key);
12468
12582
  if (!e) return;
12469
12583
  this.warm.delete(key);
12470
- await e.client.close().catch((err2) => log23.debug(`warm-pool evict close failed: ${err2}`));
12584
+ await e.client.close().catch((err2) => log24.debug(`warm-pool evict close failed: ${err2}`));
12471
12585
  }
12472
12586
  async closeAll() {
12473
12587
  for (const e of this.warm.values()) {
@@ -12686,7 +12800,7 @@ init_tools_shell();
12686
12800
  // src/tools.notify.ts
12687
12801
  init_logging();
12688
12802
  import { execFile as execFile2 } from "child_process";
12689
- var log25 = forComponent("notify");
12803
+ var log26 = forComponent("notify");
12690
12804
  var WIN_TOAST = [
12691
12805
  "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null",
12692
12806
  "$t = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)",
@@ -12717,7 +12831,7 @@ function makeNotifyTool(opts = {}) {
12717
12831
  return new Promise((resolve6) => {
12718
12832
  run(argv[0], argv[1], { timeout: 5e3, ...extra }, (e) => {
12719
12833
  if (e) {
12720
- log25.debug("notification failed", e);
12834
+ log26.debug("notification failed", e);
12721
12835
  resolve6(`Notification failed: ${e.message}`);
12722
12836
  } else resolve6("Notification shown.");
12723
12837
  });
@@ -12733,7 +12847,7 @@ import { BodDB as BodDB2 } from "@bod.ee/db";
12733
12847
  init_logging();
12734
12848
  import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
12735
12849
  import { homedir } from "os";
12736
- var log26 = forComponent("cli-util");
12850
+ var log27 = forComponent("cli-util");
12737
12851
  function dotDirs(base, sub, opts = {}) {
12738
12852
  const home = opts.home ?? homedir();
12739
12853
  const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
@@ -12750,7 +12864,7 @@ function parseJson(text, fallback, what = "json") {
12750
12864
  try {
12751
12865
  return JSON.parse(text);
12752
12866
  } catch (e) {
12753
- log26.debug(`parseJson(${what}) failed: ${e.message}`);
12867
+ log27.debug(`parseJson(${what}) failed: ${e.message}`);
12754
12868
  return fallback;
12755
12869
  }
12756
12870
  }
@@ -12760,7 +12874,7 @@ function readJsonFile(path, fallback) {
12760
12874
  try {
12761
12875
  text = readFileSync5(path, "utf8");
12762
12876
  } catch (e) {
12763
- log26.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
12877
+ log27.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
12764
12878
  return fallback;
12765
12879
  }
12766
12880
  return parseJson(text, fallback, path);
@@ -13064,7 +13178,7 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as rea
13064
13178
  import { homedir as homedir3 } from "os";
13065
13179
  import { dirname as dirname4, join as join7 } from "path";
13066
13180
  import { fileURLToPath } from "url";
13067
- var log27 = forComponent("VoiceIO");
13181
+ var log28 = forComponent("VoiceIO");
13068
13182
  var now3 = () => performance.now();
13069
13183
  var Player = class {
13070
13184
  proc = null;
@@ -13078,7 +13192,7 @@ var Player = class {
13078
13192
  ["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
13079
13193
  { stdio: ["pipe", "ignore", "ignore"] }
13080
13194
  );
13081
- this.proc.on("error", (e) => log27.warn(`ffplay error: ${e.message}`));
13195
+ this.proc.on("error", (e) => log28.warn(`ffplay error: ${e.message}`));
13082
13196
  this.proc.stdin.on("error", () => {
13083
13197
  });
13084
13198
  this.bytesWritten = 0;
@@ -13116,7 +13230,7 @@ function detectFfmpegMic() {
13116
13230
  const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
13117
13231
  const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
13118
13232
  if (!mic) throw new Error("no audio input device found");
13119
- log27.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
13233
+ log28.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
13120
13234
  return `:${mic.idx}`;
13121
13235
  }
13122
13236
  function detectDshowMic() {
@@ -13127,7 +13241,7 @@ function detectDshowMic() {
13127
13241
  const devices = [...(r.stderr ?? "").matchAll(/"([^"]+)"\s*\(audio\)/g)].map((m) => m[1]);
13128
13242
  const mic = devices.find((d) => !/stereo mix|loopback|virtual|cable|teams/i.test(d)) ?? devices[0];
13129
13243
  if (!mic) throw new Error("no audio input device found (set MIC_DEVICE to the DirectShow device name)");
13130
- log27.debug(`ffmpeg dshow mic: ${mic}`);
13244
+ log28.debug(`ffmpeg dshow mic: ${mic}`);
13131
13245
  return `audio=${mic}`;
13132
13246
  }
13133
13247
  function detectedInputDevice() {
@@ -13163,15 +13277,15 @@ function resolveAecBinary() {
13163
13277
  if (existsSync4(bin) && statSync6(bin).mtimeMs >= statSync6(src).mtimeMs) return bin;
13164
13278
  if (spawnSync3("which", ["swiftc"]).status !== 0) return null;
13165
13279
  mkdirSync5(cacheDir, { recursive: true });
13166
- log27.info("compiling AEC mic helper (first run)\u2026");
13280
+ log28.info("compiling AEC mic helper (first run)\u2026");
13167
13281
  const build = spawnSync3("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
13168
13282
  if (build.status !== 0) {
13169
- log27.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
13283
+ log28.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
13170
13284
  return null;
13171
13285
  }
13172
13286
  const sign = spawnSync3("codesign", ["-fs", "-", bin], { encoding: "utf8" });
13173
13287
  if (sign.status !== 0) {
13174
- log27.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
13288
+ log28.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
13175
13289
  return null;
13176
13290
  }
13177
13291
  return bin;
@@ -13213,17 +13327,17 @@ var NodeMicSource = class {
13213
13327
  this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
13214
13328
  } else {
13215
13329
  if (!hasCommand("ffmpeg")) throw new Error("voice I/O unavailable: no AEC helper and no ffmpeg on PATH");
13216
- log27.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
13330
+ log28.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
13217
13331
  const input = process.platform === "win32" ? ["-f", "dshow", "-i", detectDshowMic()] : ["-f", "avfoundation", "-i", detectFfmpegMic()];
13218
13332
  this.proc = spawn2(
13219
13333
  "ffmpeg",
13220
13334
  ["-loglevel", "error", ...input, "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
13221
13335
  { stdio: ["ignore", "pipe", "pipe"], ...process.platform === "win32" ? { windowsHide: true } : {} }
13222
13336
  );
13223
- this.proc.stderr.on("data", (d) => log27.warn(`ffmpeg: ${String(d).trim()}`));
13337
+ this.proc.stderr.on("data", (d) => log28.warn(`ffmpeg: ${String(d).trim()}`));
13224
13338
  }
13225
13339
  this.proc.on("exit", (c) => {
13226
- if (c && !this.stopped) log27.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
13340
+ if (c && !this.stopped) log28.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
13227
13341
  });
13228
13342
  this.proc.stdout.on("data", (chunk) => onChunk(chunk));
13229
13343
  }
@@ -13289,7 +13403,7 @@ var AecDuplexAudio = class {
13289
13403
  this.proc.stdin.on("error", () => {
13290
13404
  });
13291
13405
  this.proc.on("exit", (c) => {
13292
- if (c && !this.stopped) log27.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
13406
+ if (c && !this.stopped) log28.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
13293
13407
  });
13294
13408
  this.proc.stdout.on("data", (chunk) => {
13295
13409
  this.gotChunk = true;
@@ -13305,7 +13419,7 @@ var AecDuplexAudio = class {
13305
13419
  openMicSettings();
13306
13420
  this.onFatal?.("microphone permission denied \u2014 enable it in System Settings \u2192 Privacy & Security \u2192 Microphone for your terminal, then restart it");
13307
13421
  }
13308
- } else log27.debug(`mic-aec: ${s}`);
13422
+ } else log28.debug(`mic-aec: ${s}`);
13309
13423
  }
13310
13424
  });
13311
13425
  if (!this.noVpio && !this.triedFallback) {
@@ -13314,7 +13428,7 @@ var AecDuplexAudio = class {
13314
13428
  this.triedFallback = true;
13315
13429
  this.noVpio = true;
13316
13430
  this._aec = false;
13317
- log27.warn("mic-aec: VPIO delivered no audio in 2.5s \u2014 falling back to non-VPIO capture (no AEC \u2192 half-duplex, no barge-in)");
13431
+ log28.warn("mic-aec: VPIO delivered no audio in 2.5s \u2014 falling back to non-VPIO capture (no AEC \u2192 half-duplex, no barge-in)");
13318
13432
  this.onDegrade?.();
13319
13433
  this.killProc();
13320
13434
  this.spawnHelper();
@@ -13485,7 +13599,7 @@ function bindAckInstinct(v, instinct) {
13485
13599
  if (ev.kind === "ack_fired") instinct.ackOutcome(String(ev.phrase));
13486
13600
  else if (ev.kind === "ack_skipped" || ev.kind === "ack_cancelled") instinct.ackOutcome(null);
13487
13601
  } catch (e) {
13488
- log27.warn(`ack outcome tap failed: ${e instanceof Error ? e.message : e}`);
13602
+ log28.warn(`ack outcome tap failed: ${e instanceof Error ? e.message : e}`);
13489
13603
  }
13490
13604
  onDiag(ev);
13491
13605
  };
@@ -13562,7 +13676,7 @@ function channelInput(sc, m) {
13562
13676
  }
13563
13677
  if (m.type === "agent_state") {
13564
13678
  if (typeof m.state === "string" && MAIN_STATES.has(m.state)) sc.onAgentState(m.state);
13565
- else log27.warn(`agent_state: unknown state ${JSON.stringify(m.state)} \u2014 ignored`);
13679
+ else log28.warn(`agent_state: unknown state ${JSON.stringify(m.state)} \u2014 ignored`);
13566
13680
  return true;
13567
13681
  }
13568
13682
  return false;
@@ -13686,7 +13800,7 @@ function fakeVoiceParts(uttFile) {
13686
13800
  init_logging();
13687
13801
  import { appendFile, mkdirSync as mkdirSync6 } from "fs";
13688
13802
  import { dirname as dirname5 } from "path";
13689
- var log28 = forComponent("VoiceDiag");
13803
+ var log29 = forComponent("VoiceDiag");
13690
13804
  var JsonlDiagSink = class {
13691
13805
  constructor(path) {
13692
13806
  this.path = path;
@@ -13727,7 +13841,7 @@ var JsonlDiagSink = class {
13727
13841
  disable(e) {
13728
13842
  this.disabled = true;
13729
13843
  this.queue.length = 0;
13730
- log28.debug(`voice diag sink disabled (${this.path}): ${e instanceof Error ? e.message : e}`);
13844
+ log29.debug(`voice diag sink disabled (${this.path}): ${e instanceof Error ? e.message : e}`);
13731
13845
  }
13732
13846
  /** True once a failure permanently disabled the sink (tests/inspection). */
13733
13847
  get isDisabled() {
@@ -13749,7 +13863,7 @@ init_logging();
13749
13863
  init_logging();
13750
13864
  import { mkdir, readFile, writeFile } from "fs/promises";
13751
13865
  import { join as join8 } from "path";
13752
- var log29 = forComponent("ChannelBanks");
13866
+ var log30 = forComponent("ChannelBanks");
13753
13867
  var LINE_KINDS = ["backchannel", "filler", "bridge"];
13754
13868
  var BANK_KEYS = [...LINE_KINDS, "phatic"];
13755
13869
  var EN_PHATIC = [
@@ -13782,9 +13896,9 @@ var EN_PHATIC = [
13782
13896
  var EN_BANK = { ...CHANNEL_BANK, phatic: EN_PHATIC };
13783
13897
  var primaryLang = (tag) => tag.trim().toLowerCase().split(/[-_]/)[0];
13784
13898
  function isPhatic(text, bank) {
13785
- const t = norm2(text);
13899
+ const t = norm3(text);
13786
13900
  return !!t && !!bank?.phatic?.some((p) => {
13787
- const q2 = norm2(p);
13901
+ const q2 = norm3(p);
13788
13902
  return !!q2 && (t === q2 || t.startsWith(q2 + " "));
13789
13903
  });
13790
13904
  }
@@ -13862,16 +13976,16 @@ function fitsLanguage(line, lang) {
13862
13976
  const ws = line.toLowerCase().match(new RegExp("\\p{L}+", "gu")) ?? [];
13863
13977
  return !ws.some((w) => !mine?.has(w) && Object.entries(STOP3).some(([l, s]) => l !== lang && s.has(w)));
13864
13978
  }
13865
- var norm2 = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}' ]/gu, "").replace(/\s+/g, " ").trim();
13979
+ var norm3 = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}' ]/gu, "").replace(/\s+/g, " ").trim();
13866
13980
  function cleanLine(s, lang, maxWords = 6) {
13867
13981
  const raw = s.trim().split("\n")[0];
13868
13982
  const l = raw.replace(/^[-*•\d.)\s]+/, "").replace(/^["'“”«»]+|["'“”«»]+$/g, "").replace(new RegExp("^\\p{L}+:\\s*", "u"), "").trim();
13869
13983
  if (!l || isLeakSuspect(raw) || isLeakSuspect(l)) {
13870
- if (l) log29.debug(`line "${raw}" reads as meta \u2014 rejected`);
13984
+ if (l) log30.debug(`line "${raw}" reads as meta \u2014 rejected`);
13871
13985
  return "";
13872
13986
  }
13873
13987
  if (lang && !fitsLanguage(l, lang)) {
13874
- log29.debug(`line "${l}" is not ${lang} \u2014 rejected`);
13988
+ log30.debug(`line "${l}" is not ${lang} \u2014 rejected`);
13875
13989
  return "";
13876
13990
  }
13877
13991
  return l.split(/\s+/).length > maxWords || l.length > 60 ? "" : l;
@@ -13883,9 +13997,9 @@ function validateBank(raw, lang, minPerKind = 3) {
13883
13997
  for (const k of BANK_KEYS) {
13884
13998
  const v = raw[k];
13885
13999
  const seen = /* @__PURE__ */ new Set();
13886
- out[k] = (Array.isArray(v) ? v : []).filter((x) => typeof x === "string").map((x) => cleanLine(x, lang)).filter((x) => x && !(k === "filler" && SPEAKER_FILLER.test(norm2(x))) && !seen.has(norm2(x)) && seen.add(norm2(x)));
14000
+ out[k] = (Array.isArray(v) ? v : []).filter((x) => typeof x === "string").map((x) => cleanLine(x, lang)).filter((x) => x && !(k === "filler" && SPEAKER_FILLER.test(norm3(x))) && !seen.has(norm3(x)) && seen.add(norm3(x)));
13887
14001
  if (out[k].length < minPerKind) {
13888
- log29.warn(`bank ${lang}: only ${out[k].length} valid ${k} lines \u2014 rejected`);
14002
+ log30.warn(`bank ${lang}: only ${out[k].length} valid ${k} lines \u2014 rejected`);
13889
14003
  return null;
13890
14004
  }
13891
14005
  }
@@ -13955,11 +14069,11 @@ var ChannelBanks = class {
13955
14069
  const f = this.file(lang);
13956
14070
  let cached = null;
13957
14071
  if (f) cached = await readFile(f, "utf8").then((s) => JSON.parse(s)).catch((e) => {
13958
- if (e.code !== "ENOENT") log29.warn(`bank cache ${f} unreadable \u2014 regenerating: ${e.message}`);
14072
+ if (e.code !== "ENOENT") log30.warn(`bank cache ${f} unreadable \u2014 regenerating: ${e.message}`);
13959
14073
  return null;
13960
14074
  });
13961
14075
  if (cached && cached.v !== BANK_VERSION) {
13962
- log29.debug(`bank ${lang}: cache v${cached.v ?? 1} predates v${BANK_VERSION} \u2014 regenerating`);
14076
+ log30.debug(`bank ${lang}: cache v${cached.v ?? 1} predates v${BANK_VERSION} \u2014 regenerating`);
13963
14077
  cached = null;
13964
14078
  }
13965
14079
  const valid = cached && validateBank(cached.bank, lang, 0);
@@ -13967,7 +14081,7 @@ var ChannelBanks = class {
13967
14081
  if (valid) {
13968
14082
  for (const k of BANK_KEYS) if (!valid[k].length) valid[k] = [...this.o.fallback[lang]?.[k] ?? []];
13969
14083
  this.banks.set(lang, valid);
13970
- log29.debug(`bank ${lang}: cache hit (${BANK_KEYS.map((k) => valid[k].length).join("/")})${complete ? "" : " \u2014 missing kinds, refreshing"}`);
14084
+ log30.debug(`bank ${lang}: cache hit (${BANK_KEYS.map((k) => valid[k].length).join("/")})${complete ? "" : " \u2014 missing kinds, refreshing"}`);
13971
14085
  }
13972
14086
  if (complete && this.o.now() - cached.at < this.o.refreshMs) return;
13973
14087
  await this.generate(lang, valid);
@@ -13983,14 +14097,14 @@ var ChannelBanks = class {
13983
14097
  const bank = {};
13984
14098
  for (const k of BANK_KEYS) {
13985
14099
  const seen = /* @__PURE__ */ new Set();
13986
- bank[k] = [...fresh[k], ...have?.[k] ?? []].filter((x) => !seen.has(norm2(x)) && seen.add(norm2(x))).slice(0, this.o.maxPerKind);
14100
+ bank[k] = [...fresh[k], ...have?.[k] ?? []].filter((x) => !seen.has(norm3(x)) && seen.add(norm3(x))).slice(0, this.o.maxPerKind);
13987
14101
  }
13988
14102
  this.banks.set(lang, bank);
13989
- log29.debug(`bank ${lang}: ${have ? "refreshed" : "generated"} (${BANK_KEYS.map((k) => bank[k].length).join("/")}) in ${Math.round(this.o.now() - t0)}ms`);
14103
+ log30.debug(`bank ${lang}: ${have ? "refreshed" : "generated"} (${BANK_KEYS.map((k) => bank[k].length).join("/")}) in ${Math.round(this.o.now() - t0)}ms`);
13990
14104
  const f = this.file(lang);
13991
- if (f) await mkdir(this.o.cacheDir, { recursive: true }).then(() => writeFile(f, JSON.stringify({ at: this.o.now(), v: BANK_VERSION, bank }))).catch((e) => log29.warn(`bank cache write ${f} failed: ${e.message}`));
14105
+ if (f) await mkdir(this.o.cacheDir, { recursive: true }).then(() => writeFile(f, JSON.stringify({ at: this.o.now(), v: BANK_VERSION, bank }))).catch((e) => log30.warn(`bank cache write ${f} failed: ${e.message}`));
13992
14106
  } catch (e) {
13993
- log29.warn(`bank ${lang}: generation failed \u2014 ${have ? "keeping the cached bank" : lang in this.o.fallback ? "fallback bank" : "silent"}: ${e.message}`);
14107
+ log30.warn(`bank ${lang}: generation failed \u2014 ${have ? "keeping the cached bank" : lang in this.o.fallback ? "fallback bank" : "silent"}: ${e.message}`);
13994
14108
  } finally {
13995
14109
  clearTimeout(t);
13996
14110
  if (!this.banks.has(lang)) setTimeout(() => this.pending.delete(lang), this.o.retryMs).unref?.();
@@ -13999,7 +14113,7 @@ var ChannelBanks = class {
13999
14113
  };
14000
14114
 
14001
14115
  // src/voice/backchannelPlan.ts
14002
- var log30 = forComponent("BackchannelPlan");
14116
+ var log31 = forComponent("BackchannelPlan");
14003
14117
  var TURN_TYPES = ["story", "list", "complaint", "explanation", "short_command", "question", "other"];
14004
14118
  var DENSITIES = ["none", "low", "normal", "high"];
14005
14119
  var silentTurn = (p) => p.density === "none" || p.turn === "short_command" || p.turn === "question";
@@ -14064,14 +14178,41 @@ Reply JSON only: {"turn":"...","density":"...","cues":[{"when":["phrase",...],"l
14064
14178
  const reasoning = /gpt-oss/.test(o.model) ? { providerOptions: { reasoning_effort: "low" } } : {};
14065
14179
  const r = await this.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false, signal, maxTokens: o.maxTokens, ...reasoning });
14066
14180
  const p = parsePlan(r.content ?? "", c.lang);
14067
- if (!p) log30.debug(`unusable plan: ${(r.content ?? "").slice(0, 120)}`);
14181
+ if (!p) log31.debug(`unusable plan: ${(r.content ?? "").slice(0, 120)}`);
14068
14182
  return p;
14069
14183
  }
14070
14184
  };
14071
14185
 
14072
14186
  // src/voice/secondChannel.ts
14073
- var log31 = forComponent("SecondChannel");
14187
+ var log32 = forComponent("SecondChannel");
14188
+ var CHANNEL_MODES = {
14189
+ turn: { kinds: ["bridge"], bridgeDelayMs: 1500 },
14190
+ normal: {},
14191
+ chatty: {
14192
+ anticipate: true,
14193
+ cueOnly: false,
14194
+ generateFor: ["backchannel", "bridge"],
14195
+ reactionGapMs: 6e3,
14196
+ generateBudgetMs: 400,
14197
+ backchannelMinWords: 5,
14198
+ backchannelGapMs: 1200,
14199
+ minGapMs: 1200,
14200
+ backchannelPauseMs: 250
14201
+ }
14202
+ };
14203
+ var CHANNEL_MODE_NAMES = Object.keys(CHANNEL_MODES);
14204
+ function parseChannelMode(v) {
14205
+ if (!v) return "normal";
14206
+ if (!(v in CHANNEL_MODES)) throw new Error(`unknown channel mode "${v}" (${CHANNEL_MODE_NAMES.join("|")})`);
14207
+ return v;
14208
+ }
14074
14209
  var SecondChannelOptions = class {
14210
+ /** Preset (CHANNEL_MODES) under the explicit options. */
14211
+ mode = "normal";
14212
+ /** Line kinds this channel may speak. */
14213
+ kinds = ["backchannel", "filler", "bridge"];
14214
+ /** A generated (content) backchannel needs this long since the last one; in between, canned light cues. 0 = always. */
14215
+ reactionGapMs = 0;
14075
14216
  /** Refiner (Jev/fast LLM): the choice over CHANNEL_ACTIONS, or null on failure. Absent → the rule alone decides.
14076
14217
  * `state.rule` is the kind the rule proposed (fastChannelDecider answers yes/no on it). `signal` aborts a late call. */
14077
14218
  decide;
@@ -14153,20 +14294,23 @@ function withBudget(p, ms, what, ac) {
14153
14294
  let t;
14154
14295
  const late = new Promise((r) => {
14155
14296
  t = setTimeout(() => {
14156
- log31.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
14297
+ log32.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
14157
14298
  ac?.abort();
14158
14299
  r(null);
14159
14300
  }, ms);
14160
14301
  });
14161
14302
  return Promise.race([p.catch((e) => {
14162
- log31.warn(`${what} failed \u2014 falling back: ${e?.message ?? e}`);
14303
+ log32.warn(`${what} failed \u2014 falling back: ${e?.message ?? e}`);
14163
14304
  return null;
14164
14305
  }), late]).finally(() => clearTimeout(t));
14165
14306
  }
14166
14307
  var SecondChannel = class {
14167
14308
  constructor(options) {
14168
14309
  this.options = options;
14169
- this.options = { ...new SecondChannelOptions(), ...options };
14310
+ const preset = CHANNEL_MODES[options?.mode ?? "normal"] ?? {};
14311
+ const base = new SecondChannelOptions();
14312
+ this.options = { ...base, ...preset, ...options };
14313
+ for (const k of Object.keys(preset)) if (!options || !(k in options)) this.normalTiming[k] = base[k];
14170
14314
  this.startedAt = this.o.now();
14171
14315
  if (this.o.lang) this.setLanguage(this.o.lang, true);
14172
14316
  }
@@ -14213,6 +14357,10 @@ var SecondChannel = class {
14213
14357
  reply = "";
14214
14358
  // channel-1's text this turn (the plan's context)
14215
14359
  lastUser = "";
14360
+ lastReactAt = -Infinity;
14361
+ normalTiming = {};
14362
+ // chatty without aec: the preset's keys back at their defaults
14363
+ degradedLogged = false;
14216
14364
  /** The language lines are spoken in (detected from the user's words, or set by the host). */
14217
14365
  get language() {
14218
14366
  return this.lang;
@@ -14224,7 +14372,7 @@ var SecondChannel = class {
14224
14372
  if (host) this.pinned = true;
14225
14373
  const lang = primaryLang(tag);
14226
14374
  if (!lang || lang === this.lang) return;
14227
- log31.debug(`language ${this.lang} \u2192 ${lang}`);
14375
+ log32.debug(`language ${this.lang} \u2192 ${lang}`);
14228
14376
  this.lang = lang;
14229
14377
  this.recent = [];
14230
14378
  this.picks.clear();
@@ -14241,7 +14389,13 @@ var SecondChannel = class {
14241
14389
  return this.lang ? this.o.banks ? this.o.banks.get(this.lang) : this.lang === "en" ? EN_BANK : null : null;
14242
14390
  }
14243
14391
  get o() {
14244
- return this.options;
14392
+ const o = this.options;
14393
+ if (o.mode !== "chatty" || o.aec) return o;
14394
+ if (!this.degradedLogged) {
14395
+ this.degradedLogged = true;
14396
+ log32.info("chatty mode without echo cancellation \u2014 normal timing (our lines would re-trigger the mic)");
14397
+ }
14398
+ return { ...o, ...this.normalTiming };
14245
14399
  }
14246
14400
  /** The user's live partial for the current turn (the whole partial, not a delta). */
14247
14401
  onInterim(text) {
@@ -14354,7 +14508,7 @@ var SecondChannel = class {
14354
14508
  try {
14355
14509
  this.o.cancel();
14356
14510
  } catch (e) {
14357
- log31.warn(`cancel threw: ${e.message}`);
14511
+ log32.warn(`cancel threw: ${e.message}`);
14358
14512
  }
14359
14513
  }
14360
14514
  }
@@ -14377,6 +14531,7 @@ var SecondChannel = class {
14377
14531
  }
14378
14532
  /** Hard gates every line passes, whoever proposed it. */
14379
14533
  allowed(k) {
14534
+ if (!this.o.kinds.includes(k)) return false;
14380
14535
  if (this.audioLocked || this.inflight || this.o.now() - this.lastAt < this.o.minGapMs || k === this.lastKind && (k !== "backchannel" || this.o.now() - this.lastAt < this.o.backchannelGapMs)) return false;
14381
14536
  if (k === "backchannel") return this.o.aec && this.userSpeaking && this.main === "idle";
14382
14537
  if (k === "filler") return !this.userSpeaking && this.main === "idle";
@@ -14438,17 +14593,19 @@ var SecondChannel = class {
14438
14593
  const d = await withBudget(this.o.decide(this.tail(), { ...this.state(pauseMs), rule }, ac.signal), this.o.decisionBudgetMs, "channel decision", ac);
14439
14594
  choice = d?.choice;
14440
14595
  if (choice && choice in CHANNEL_ACTIONS && (choice === "silence" || this.allowed(choice))) kind = choice;
14441
- else if (choice && choice in CHANNEL_ACTIONS) log31.debug(`decider chose ${choice} for a ${rule} moment \u2014 the rule stands`);
14596
+ else if (choice && choice in CHANNEL_ACTIONS) log32.debug(`decider chose ${choice} for a ${rule} moment \u2014 the rule stands`);
14442
14597
  }
14443
14598
  if (kind === "silence") return ev({ choice, acted: "silence" });
14444
14599
  if (stale()) return ev({ choice, kind, acted: "stale" });
14445
14600
  if (!this.allowed(kind)) return ev({ choice, kind, acted: "gated" });
14446
- const text = cue && !this.recent.some((r) => norm2(r) === norm2(cue.line)) ? cue.line : await this.line(kind);
14601
+ const react = kind === "backchannel" && this.o.now() - this.lastReactAt >= this.o.reactionGapMs;
14602
+ const text = cue && react && !this.recent.some((r) => norm3(r) === norm3(cue.line)) ? cue.line : await this.line(kind, react);
14447
14603
  if (!text) return ev({ choice, kind, acted: "no_bank" });
14448
14604
  if (stale()) return ev({ choice, kind, text, acted: "stale" });
14449
14605
  if (!this.allowed(kind)) return ev({ choice, kind, text, acted: "gated" });
14450
14606
  const said = this.say(kind, text);
14451
14607
  if (said && cue) this.cueFrom = words2(this.heard).length;
14608
+ if (said && kind === "backchannel" && this.o.reactionGapMs && !this.bank?.backchannel.some((b) => norm3(b) === norm3(text))) this.lastReactAt = this.o.now();
14452
14609
  ev({ choice, kind, text, acted: said ? "spoken" : "skipped" });
14453
14610
  } finally {
14454
14611
  this.busy = false;
@@ -14465,15 +14622,15 @@ var SecondChannel = class {
14465
14622
  call_ms: Math.round(now4 - this.startedAt)
14466
14623
  };
14467
14624
  }
14468
- async line(kind) {
14469
- if (this.o.generate && this.o.generateFor.includes(kind)) {
14625
+ async line(kind, react = true) {
14626
+ if (this.o.generate && this.o.generateFor.includes(kind) && (kind !== "backchannel" || react)) {
14470
14627
  const ac = new AbortController();
14471
- const ctx = { kind, heard: this.tail(), state: this.main, avoid: [...this.recent], lang: this.lang };
14628
+ const ctx = { kind, heard: this.tail(), state: this.main, avoid: [...this.recent], lang: this.lang, ...kind === "backchannel" && this.o.reactionGapMs ? { react: true } : {} };
14472
14629
  const raw = await withBudget(this.o.generate(ctx, ac.signal), this.o.generateBudgetMs, `${kind} generate`);
14473
14630
  if (raw === null) ac.abort();
14474
14631
  const text = cleanLine(raw ?? "", this.lang);
14475
- if (text && !this.recent.some((r) => norm2(r) === norm2(text))) return text;
14476
- if (text) log31.debug(`generated "${text}" repeats a recent line \u2014 canned`);
14632
+ if (text && !this.recent.some((r) => norm3(r) === norm3(text))) return text;
14633
+ if (text) log32.debug(`generated "${text}" repeats a recent line \u2014 canned`);
14477
14634
  }
14478
14635
  return this.pick(kind);
14479
14636
  }
@@ -14483,8 +14640,8 @@ var SecondChannel = class {
14483
14640
  const pool = this.bank?.[kind] ?? [];
14484
14641
  if (!pool.length) return "";
14485
14642
  const hist = this.picks.get(kind) ?? [];
14486
- const recent = new Set([...hist.slice(-this.o.bankExclude), ...this.recent].map(norm2));
14487
- const fresh = pool.filter((x) => !recent.has(norm2(x)));
14643
+ const recent = new Set([...hist.slice(-this.o.bankExclude), ...this.recent].map(norm3));
14644
+ const fresh = pool.filter((x) => !recent.has(norm3(x)));
14488
14645
  const from = fresh.length ? fresh : pool;
14489
14646
  const p = from[Math.floor(this.o.random() * from.length)];
14490
14647
  this.picks.set(kind, [...hist, p].slice(-this.o.bankExclude));
@@ -14495,7 +14652,7 @@ var SecondChannel = class {
14495
14652
  try {
14496
14653
  r = this.o.speak(text, kind);
14497
14654
  } catch (e) {
14498
- log31.warn(`speak threw \u2014 line dropped: ${e.message}`);
14655
+ log32.warn(`speak threw \u2014 line dropped: ${e.message}`);
14499
14656
  return false;
14500
14657
  }
14501
14658
  if (r === false) return false;
@@ -14507,7 +14664,7 @@ var SecondChannel = class {
14507
14664
  this.o.show(text, kind);
14508
14665
  const p = Promise.resolve(r).then(() => {
14509
14666
  }, (e) => {
14510
- log31.warn(`channel line playback failed: ${e?.message ?? e}`);
14667
+ log32.warn(`channel line playback failed: ${e?.message ?? e}`);
14511
14668
  }).finally(() => {
14512
14669
  if (this.inflight === p) this.inflight = null;
14513
14670
  });
@@ -14543,10 +14700,12 @@ var KIND_HINT2 = {
14543
14700
  filler: 'a soft listener sound while THEY search for words ("mm\u2026", "mhm", "yeah\u2026") \u2014 never a word that continues their sentence or starts yours',
14544
14701
  bridge: 'a neutral one-moment cue while the answer is prepared ("let me see\u2026", "one sec\u2026") \u2014 never the start of a statement or opinion ("the thing is", "well actually")'
14545
14702
  };
14703
+ var REACT_HINT = 'a short reaction to their LAST words instead: feeling ("oh no", "ha, nice", "ugh"), surprise ("wait, really?", "huh"), a light laugh if they joke or laugh ("ha!"), or an echo of their key word ("the deploy?") \u2014 still never asks for an answer or takes the turn; if nothing there deserves a reaction, a plain "mm-hm"';
14546
14704
  function fastLineGenerator(ai, model) {
14547
14705
  return async (c, signal) => {
14548
14706
  const prompt = `You voice the tiny side-sounds of a live spoken conversation. Write ONE line of 1-5 words: ${KIND_HINT2[c.kind]}.
14549
- The user just said: "${c.heard}"
14707
+ ` + (c.react ? `Here the line may be ${REACT_HINT}.
14708
+ ` : "") + `The user just said: "${c.heard}"
14550
14709
  The assistant is: ${c.state}.
14551
14710
  ` + (c.avoid.length ? `Do not reuse any of: ${c.avoid.map((a) => `"${a}"`).join(", ")}.
14552
14711
  ` : "") + `React to their actual words, casually, in ${languageName(c.lang)}. Output only the line.`;
@@ -14702,7 +14861,7 @@ function applyOverlay(cfg, file) {
14702
14861
  // cli/hooks-config.ts
14703
14862
  import { spawnSync as spawnSync4 } from "child_process";
14704
14863
  init_platform();
14705
- var log32 = forComponent("hooks");
14864
+ var log33 = forComponent("hooks");
14706
14865
  var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
14707
14866
  function ruleMatches(rule, toolName) {
14708
14867
  if (!rule.tool || rule.tool === "*") return true;
@@ -14721,7 +14880,7 @@ function runCmd(rule, env) {
14721
14880
  });
14722
14881
  return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
14723
14882
  } catch (e) {
14724
- log32.debug(`hook command failed: ${rule.command}`, e);
14883
+ log33.debug(`hook command failed: ${rule.command}`, e);
14725
14884
  return { code: 1, out: String(e?.message ?? e) };
14726
14885
  }
14727
14886
  }
@@ -14829,7 +14988,7 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as rea
14829
14988
  import { homedir as homedir6 } from "os";
14830
14989
  import { join as join11 } from "path";
14831
14990
  init_platform();
14832
- var log33 = forComponent("session");
14991
+ var log34 = forComponent("session");
14833
14992
  var globalDir = () => join11(homedir6(), ".agent", "sessions");
14834
14993
  function indexLink(target, link2) {
14835
14994
  try {
@@ -14892,7 +15051,7 @@ var SessionStore = class {
14892
15051
  }
14893
15052
  load(id) {
14894
15053
  if (!this.safeId(id)) {
14895
- log33.debug(`rejecting unsafe session id: ${id}`);
15054
+ log34.debug(`rejecting unsafe session id: ${id}`);
14896
15055
  return void 0;
14897
15056
  }
14898
15057
  const path = join11(this.dir, `${id}.json`);
@@ -14900,7 +15059,7 @@ var SessionStore = class {
14900
15059
  try {
14901
15060
  return JSON.parse(readFileSync8(path, "utf8"));
14902
15061
  } catch (e) {
14903
- log33.debug(`unreadable session ${id} \u2014 ignoring`, e);
15062
+ log34.debug(`unreadable session ${id} \u2014 ignoring`, e);
14904
15063
  return void 0;
14905
15064
  }
14906
15065
  }
@@ -14913,7 +15072,7 @@ var SessionStore = class {
14913
15072
  try {
14914
15073
  metas.push(JSON.parse(readFileSync8(join11(this.dir, f), "utf8")).meta);
14915
15074
  } catch (e) {
14916
- log33.debug(`skipping unreadable session file ${f}`, e);
15075
+ log34.debug(`skipping unreadable session file ${f}`, e);
14917
15076
  }
14918
15077
  }
14919
15078
  return metas.sort((a, b) => b.updated - a.updated);
@@ -15152,7 +15311,7 @@ import { promisify } from "util";
15152
15311
  import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync9 } from "fs";
15153
15312
  import { homedir as homedir7 } from "os";
15154
15313
  import { join as join12, resolve as resolve4, sep as sep3, parse as parsePath } from "path";
15155
- var log34 = forComponent("checkpoints");
15314
+ var log35 = forComponent("checkpoints");
15156
15315
  var exec = promisify(execFile4);
15157
15316
  var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
15158
15317
  function isBroadRoot(p) {
@@ -15198,7 +15357,7 @@ var ShadowRepo = class {
15198
15357
  writeFileSync6(join12(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
15199
15358
  this.ready = true;
15200
15359
  } catch (e) {
15201
- log34.debug(`git checkpoints unavailable for ${this.workTree}`, e);
15360
+ log35.debug(`git checkpoints unavailable for ${this.workTree}`, e);
15202
15361
  this.ready = false;
15203
15362
  }
15204
15363
  return this.ready;
@@ -15209,7 +15368,7 @@ var ShadowRepo = class {
15209
15368
  }
15210
15369
  async commit(label, forced = []) {
15211
15370
  await this.run("add", "-A");
15212
- for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log34.debug(`force-add failed: ${p}`, e));
15371
+ for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log35.debug(`force-add failed: ${p}`, e));
15213
15372
  await this.run("commit", "--allow-empty", "-q", "-m", label);
15214
15373
  }
15215
15374
  /** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
@@ -15217,8 +15376,8 @@ var ShadowRepo = class {
15217
15376
  * turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
15218
15377
  * point per turn, so the REPL's turn↔frame mapping stays intact. */
15219
15378
  async amendForced(paths) {
15220
- for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log34.debug(`force-capture failed: ${p}`, e));
15221
- await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log34.debug("amend failed", e));
15379
+ for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log35.debug(`force-capture failed: ${p}`, e));
15380
+ await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log35.debug("amend failed", e));
15222
15381
  }
15223
15382
  /** Commits on `ref`, oldest-first (canonical index space). */
15224
15383
  async log(ref) {
@@ -15278,7 +15437,7 @@ var ShadowRepo = class {
15278
15437
  await this.run("gc", "--auto", "-q").catch(() => {
15279
15438
  });
15280
15439
  } catch (e) {
15281
- log34.debug("checkpoint prune failed", e);
15440
+ log35.debug("checkpoint prune failed", e);
15282
15441
  }
15283
15442
  }
15284
15443
  };
@@ -15346,7 +15505,7 @@ var GitCheckpoints = class {
15346
15505
  use(sessionId) {
15347
15506
  if (sessionId === this.session) return;
15348
15507
  this.session = sessionId;
15349
- if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log34.debug("re-point failed", e));
15508
+ if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log35.debug("re-point failed", e));
15350
15509
  }
15351
15510
  async begin(label) {
15352
15511
  if (!await this.start()) return;
@@ -15366,7 +15525,7 @@ var GitCheckpoints = class {
15366
15525
  process.stderr.write(`\x1B[2m checkpoints: '${this.repos[i].root}' too large to snapshot in ${Math.round(this.options.opTimeoutMs / 1e3)}s \u2014 file rewind disabled here\x1B[0m
15367
15526
  `);
15368
15527
  } else {
15369
- log34.debug("checkpoint commit failed", e);
15528
+ log35.debug("checkpoint commit failed", e);
15370
15529
  survivors.push(this.repos[i]);
15371
15530
  survivorCaches.push(this.caches[i] ?? []);
15372
15531
  }
@@ -15401,7 +15560,7 @@ var GitCheckpoints = class {
15401
15560
  if (this.forced.has(abs)) continue;
15402
15561
  this.forced.add(abs);
15403
15562
  const i = this.repoIndexFor(abs);
15404
- if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log34.debug("amendForced failed", e));
15563
+ if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log35.debug("amendForced failed", e));
15405
15564
  }
15406
15565
  }
15407
15566
  };
@@ -17186,7 +17345,7 @@ import { spawnSync as spawnSync6 } from "child_process";
17186
17345
  import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
17187
17346
  import { homedir as homedir8 } from "os";
17188
17347
  import { join as join14 } from "path";
17189
- var log35 = forComponent("os-sched");
17348
+ var log36 = forComponent("os-sched");
17190
17349
  var OsScheduler = class {
17191
17350
  options;
17192
17351
  constructor(options) {
@@ -17248,7 +17407,7 @@ var OsScheduler = class {
17248
17407
  }
17249
17408
  }
17250
17409
  } catch (e) {
17251
- log35.debug(`cancel ${id}`, e);
17410
+ log36.debug(`cancel ${id}`, e);
17252
17411
  }
17253
17412
  for (const f of [`${id}.json`, `${id}.sh`]) {
17254
17413
  try {
@@ -17366,7 +17525,7 @@ import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unli
17366
17525
  import { homedir as homedir9 } from "os";
17367
17526
  import { join as join15 } from "path";
17368
17527
  init_platform();
17369
- var log36 = forComponent("remote-trigger");
17528
+ var log37 = forComponent("remote-trigger");
17370
17529
  var TRIGGER_DIR = () => join15(homedir9(), ".agent", "triggers");
17371
17530
  var sockPath = (sessionId, dir = TRIGGER_DIR()) => join15(dir, `${sessionId}.sock`);
17372
17531
  var TriggerServer = class {
@@ -17404,13 +17563,13 @@ var TriggerServer = class {
17404
17563
  conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
17405
17564
  }
17406
17565
  });
17407
- conn.on("error", (e) => log36.debug("trigger conn error", e));
17566
+ conn.on("error", (e) => log37.debug("trigger conn error", e));
17408
17567
  });
17409
- this.server.on("error", (e) => log36.debug("trigger server error", e));
17568
+ this.server.on("error", (e) => log37.debug("trigger server error", e));
17410
17569
  this.server.listen(p);
17411
17570
  this.path = p;
17412
17571
  } catch (e) {
17413
- log36.debug("trigger server unavailable", e);
17572
+ log37.debug("trigger server unavailable", e);
17414
17573
  }
17415
17574
  }
17416
17575
  /** Re-bind on /resume (the session id changed). */
@@ -17542,7 +17701,7 @@ var strike = C("9");
17542
17701
  var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
17543
17702
  var mdPalette = { bold, dim, cyan, italic, strike, green, link };
17544
17703
  var err = (s) => process.stderr.write(s);
17545
- var log37 = forComponent("cli");
17704
+ var log38 = forComponent("cli");
17546
17705
  var VERSION = (() => {
17547
17706
  try {
17548
17707
  return JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
@@ -17687,6 +17846,7 @@ function parseArgs(argv) {
17687
17846
  a.duplex = true;
17688
17847
  a.duplexV2 = true;
17689
17848
  } else if (x === "--channel-only") a.channelOnly = true;
17849
+ else if (x === "--channel-mode") a.channelMode = parseChannelMode(val(++i, x));
17690
17850
  else if (x === "--conversational" || x === "--convo" || x === "--voice") {
17691
17851
  a.voice = true;
17692
17852
  a.duplex = true;
@@ -17730,6 +17890,8 @@ function parseArgs(argv) {
17730
17890
  if (a.channelOnly && !a.inputFormat) throw new Error("--channel-only needs --input-format stream-json --output-format stream-json");
17731
17891
  if (a.duplex && a.plan) throw new Error("--plan is not supported in --duplex (workers are non-interactive; a plan could never be approved)");
17732
17892
  if (a.voiceSystemPrompt && !a.duplex) throw new Error("--voice-system-prompt only applies with --duplex");
17893
+ if (a.channelMode && !a.duplexV2 && !a.channelOnly) throw new Error("--channel-mode only applies with --duplex-v2 or --channel-only");
17894
+ a.channelMode ??= parseChannelMode(process.env.AGENTX_CHANNEL_MODE);
17733
17895
  if (a.voiceModel && !a.duplex && !a.channelOnly) throw new Error("--voice-model only applies with --duplex or --channel-only");
17734
17896
  if (a.thinkModel !== void 0 && !a.duplex) throw new Error("--think-model/--no-think only apply with --duplex");
17735
17897
  if (!canPrompt && (a.ask || a.plan))
@@ -17776,6 +17938,9 @@ Flags:
17776
17938
  enter the agent's history (headless: stdin interim/pause/agent_state, stdout channel)
17777
17939
  --channel-only headless (--input-format stream-json): ONLY the second channel, for a host whose main
17778
17940
  answer comes from elsewhere (stdin user/text/barge/interim/pause/agent_state, stdout channel)
17941
+ --channel-mode <m> second-channel talkativeness: turn (only a bridge when the answer is slow) | normal
17942
+ (default) | chatty (frequent cues + content reactions; needs echo cancellation)
17943
+ env fallback: AGENTX_CHANNEL_MODE
17779
17944
  --conversational duplex with a conversation-native register \u2014 short fast turns, fillers,
17780
17945
  impulsive reactions, human pacing (implies --duplex; aliases: --convo, --voice)
17781
17946
  with SONIOX_API_KEY + CARTESIA_API_KEY(+VOICE_ID) set: real voice I/O \u2014 mic in,
@@ -18385,7 +18550,7 @@ function turnCost(model, usage) {
18385
18550
  const priced = model.startsWith("claude-code/") ? `anthropic/${resolveModelAlias(model.slice("claude-code/".length))}` : model;
18386
18551
  return costOf(getModelInfo(priced)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, priced);
18387
18552
  }
18388
- async function evaluateGoal(ai, condition, transcript, log38) {
18553
+ async function evaluateGoal(ai, condition, transcript, log39) {
18389
18554
  const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
18390
18555
  const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
18391
18556
  return text.slice(0, 600);
@@ -18405,7 +18570,7 @@ ${recent}` }
18405
18570
  const match = r.content.match(/\{[\s\S]*\}/);
18406
18571
  if (match) return JSON.parse(match[0]);
18407
18572
  } catch (e) {
18408
- log38(dim(` (goal evaluator error: ${e?.message ?? e})
18573
+ log39(dim(` (goal evaluator error: ${e?.message ?? e})
18409
18574
  `));
18410
18575
  }
18411
18576
  return { met: false, reason: "evaluation unclear" };
@@ -18631,7 +18796,7 @@ function mcpAgentTools(mounted, opts) {
18631
18796
  return tools;
18632
18797
  }
18633
18798
  async function closeMcp(mounted) {
18634
- await Promise.all(mounted.map((m) => m.client.close().catch((e) => log37.debug("mcp close failed", e))));
18799
+ await Promise.all(mounted.map((m) => m.client.close().catch((e) => log38.debug("mcp close failed", e))));
18635
18800
  }
18636
18801
  var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
18637
18802
  function mentionRefs(line) {
@@ -18690,7 +18855,7 @@ async function expandMentions(fs, line) {
18690
18855
  if (loaded.includes(ref) || missing.includes(ref)) continue;
18691
18856
  if (ref.includes(":") && mcpMentionResolver) {
18692
18857
  const body = await mcpMentionResolver(ref).catch((e) => {
18693
- log37.debug("mcp mention resolve failed", e);
18858
+ log38.debug("mcp mention resolve failed", e);
18694
18859
  return null;
18695
18860
  });
18696
18861
  if (body != null) {
@@ -18781,7 +18946,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
18781
18946
  try {
18782
18947
  store.save(session);
18783
18948
  } catch (ex) {
18784
- log37.debug("mid-turn session flush failed", ex);
18949
+ log38.debug("mid-turn session flush failed", ex);
18785
18950
  }
18786
18951
  }
18787
18952
  return origNotify(e);
@@ -18945,14 +19110,14 @@ var isCancelTeardown = (e) => {
18945
19110
  function installCancelGuards(mounted) {
18946
19111
  process.on("unhandledRejection", (e) => {
18947
19112
  if (isCancelTeardown(e)) {
18948
- log37.debug("suppressed unhandledRejection (cursor stream cancel)", e);
19113
+ log38.debug("suppressed unhandledRejection (cursor stream cancel)", e);
18949
19114
  return;
18950
19115
  }
18951
- log37.error("unhandledRejection", e);
19116
+ log38.error("unhandledRejection", e);
18952
19117
  });
18953
19118
  process.on("uncaughtException", (e) => {
18954
19119
  if (isCancelTeardown(e)) {
18955
- log37.debug("suppressed uncaughtException (cursor stream cancel)", e);
19120
+ log38.debug("suppressed uncaughtException (cursor stream cancel)", e);
18956
19121
  return;
18957
19122
  }
18958
19123
  console.error(e);
@@ -19016,6 +19181,7 @@ async function repl(args, ai, cfg, cwd) {
19016
19181
  const decider = duplex ? leakDecider() : void 0;
19017
19182
  const instinct = decider && process.env.VOICE_INSTINCT === "1" ? new Instinct({
19018
19183
  decider,
19184
+ overlapModel: overlapModelFor(ai),
19019
19185
  context: () => ({ agentSaid: dx?.recentExcerpt(1).replace(/^\w+: /, ""), recent: dx?.recentExcerpt(4) }),
19020
19186
  onDecision: ({ kind, ...d }) => {
19021
19187
  voiceDiag?.push({ t: performance.now(), kind: `instinct_${kind}`, ...d });
@@ -19381,7 +19547,7 @@ async function repl(args, ai, cfg, cwd) {
19381
19547
  try {
19382
19548
  mkdirSync12(dir, { recursive: true });
19383
19549
  } catch (e) {
19384
- log37.debug("paste image dir create failed", e);
19550
+ log38.debug("paste image dir create failed", e);
19385
19551
  }
19386
19552
  const img = grabClipboardImage(dir, String(Date.now()));
19387
19553
  return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
@@ -19494,7 +19660,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
19494
19660
  mkdirSync12(join16(cwd, ".agent"), { recursive: true });
19495
19661
  appendFileSync(histPath, line + "\n");
19496
19662
  } catch (e) {
19497
- log37.debug("history write failed", e);
19663
+ log38.debug("history write failed", e);
19498
19664
  }
19499
19665
  };
19500
19666
  const ago = (t) => {
@@ -19566,7 +19732,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
19566
19732
  try {
19567
19733
  store.save(session);
19568
19734
  } catch (e) {
19569
- log37.debug("session save after rewind failed", e);
19735
+ log38.debug("session save after rewind failed", e);
19570
19736
  }
19571
19737
  err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
19572
19738
  `));
@@ -19601,7 +19767,7 @@ ${task}`;
19601
19767
  bangContext.length = 0;
19602
19768
  }
19603
19769
  const delta = await refreshCatalogs().catch((e) => {
19604
- log37.debug("catalog refresh failed", e);
19770
+ log38.debug("catalog refresh failed", e);
19605
19771
  return "";
19606
19772
  });
19607
19773
  if (delta) {
@@ -19864,7 +20030,7 @@ ${task}`;
19864
20030
  desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
19865
20031
  run: async () => {
19866
20032
  await refreshCatalogs().catch((e) => {
19867
- log37.debug("catalog refresh failed", e);
20033
+ log38.debug("catalog refresh failed", e);
19868
20034
  });
19869
20035
  face.reprepare();
19870
20036
  err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
@@ -20367,7 +20533,7 @@ ${task}`;
20367
20533
  try {
20368
20534
  for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
20369
20535
  } catch (e) {
20370
- log37.debug(`loadAgents(${d}) failed`, e);
20536
+ log38.debug(`loadAgents(${d}) failed`, e);
20371
20537
  }
20372
20538
  }
20373
20539
  if (!seen.size) {
@@ -20455,7 +20621,7 @@ ${task}`;
20455
20621
  }
20456
20622
  if (idx >= 0) {
20457
20623
  const old = mounted.splice(idx, 1)[0];
20458
- await old.client.close().catch((e) => log37.debug("mcp close failed", e));
20624
+ await old.client.close().catch((e) => log38.debug("mcp close failed", e));
20459
20625
  }
20460
20626
  try {
20461
20627
  const m = await mountMcpServer(name, conf);
@@ -20483,7 +20649,7 @@ ${task}`;
20483
20649
  }
20484
20650
  const m = mounted.splice(idx, 1)[0];
20485
20651
  remountMcpTools();
20486
- await m.client.close().catch((e) => log37.debug("mcp close failed", e));
20652
+ await m.client.close().catch((e) => log38.debug("mcp close failed", e));
20487
20653
  err(dim(` removed "${name}"
20488
20654
  `));
20489
20655
  return;
@@ -20686,7 +20852,7 @@ ${task}`;
20686
20852
  try {
20687
20853
  return readdirSync4(join16(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
20688
20854
  } catch (e) {
20689
- log37.debug("completion readdir failed", absDir, e);
20855
+ log38.debug("completion readdir failed", absDir, e);
20690
20856
  return null;
20691
20857
  }
20692
20858
  };
@@ -20823,7 +20989,7 @@ ${out}
20823
20989
  return;
20824
20990
  }
20825
20991
  await refreshCatalogs().catch((e) => {
20826
- log37.debug("catalog refresh failed", e);
20992
+ log38.debug("catalog refresh failed", e);
20827
20993
  });
20828
20994
  const sk = skills.find((x) => x.name === name);
20829
20995
  if (sk) {
@@ -20942,7 +21108,7 @@ ${out}
20942
21108
  if (delivery || cut) dx?.parkCutDelivery(delivery, !!cut, heard);
20943
21109
  const cutReply = cut && cut.full.length - cut.heard.length > 40 ? cut : null;
20944
21110
  const noteP = replaces ? replaces.note : !cutReply ? Promise.resolve("") : (instinct ? instinct.bargeIntent(text).catch((e) => {
20945
- log37.warn(`barge intent failed \u2014 model decides: ${e.message}`);
21111
+ log38.warn(`barge intent failed \u2014 model decides: ${e.message}`);
20946
21112
  return void 0;
20947
21113
  }) : Promise.resolve(void 0)).then(async (intent) => interruptionNote(cutReply, intent) + ackedNote(await instinct?.ackFor(text)));
20948
21114
  const cutIn = replaces ? replaces.cutIn : interjectNote;
@@ -20962,6 +21128,10 @@ ${out}
20962
21128
  }).finally(() => editorRef?.redrawNow());
20963
21129
  }
20964
21130
  });
21131
+ if (!instinct && decider && args.duplexV2 && softBarge(args.channelMode)) {
21132
+ const v = voiceIO;
21133
+ bindOverlapInstinct(v, new Instinct({ decider, overlapModel: overlapModelFor(ai) }), () => voiceIO === v);
21134
+ }
20965
21135
  if (instinct) {
20966
21136
  const v = voiceIO, o = v.options;
20967
21137
  const { onPartial } = o;
@@ -20972,7 +21142,7 @@ ${out}
20972
21142
  if (listening()) instinct.hear(text);
20973
21143
  onPartial(text);
20974
21144
  };
20975
- bindOverlapInstinct(v, instinct, () => voiceIO === v);
21145
+ if (!args.duplexV2 || softBarge(args.channelMode)) bindOverlapInstinct(v, instinct, () => voiceIO === v);
20976
21146
  bindAckInstinct(v, instinct);
20977
21147
  o.onUtterance = (text, meta) => {
20978
21148
  instinct.reset();
@@ -21040,7 +21210,7 @@ ${out}
21040
21210
  if (dx) dx.options.holdResumeMs = HOLD_RESUME_MS;
21041
21211
  if (args.duplexV2 && dx) {
21042
21212
  const v = voiceIO;
21043
- bindSecondChannel(v, makeSecondChannel(ai, dx.options.reflexModel, { show: (t) => {
21213
+ bindSecondChannel(v, makeSecondChannel(ai, dx.options.reflexModel, args.channelMode, { show: (t) => {
21044
21214
  err("\r\x1B[0J" + dim(` \u21B3 ${t}
21045
21215
  `));
21046
21216
  editorRef?.redrawNow();
@@ -21297,7 +21467,7 @@ function duplexError(e, reflexModel) {
21297
21467
  }
21298
21468
  var HOLD_RESUME_MS = 4e3;
21299
21469
  var leakDecider = () => voiceDecider();
21300
- function makeSecondChannel(ai, reflexModel, opts) {
21470
+ function makeSecondChannel(ai, reflexModel, mode = "normal", opts) {
21301
21471
  const decider = leakDecider();
21302
21472
  const instinct = decider ? new Instinct({ decider }) : void 0;
21303
21473
  const model = process.env.AGENTX_CHANNEL_MODEL || reflexModel;
@@ -21307,12 +21477,21 @@ function makeSecondChannel(ai, reflexModel, opts) {
21307
21477
  banks: new ChannelBanks({ generate: fastBankGenerator(ai, model), cacheDir: join16(homedir10(), ".agent", "cache", "channel-banks") }),
21308
21478
  decide: channelDecide(ai, instinct),
21309
21479
  generate: fastLineGenerator(ai, model),
21310
- // Anticipated backchannels (off until the offline replay proves them — tests/voice-bc-anticipate-eval.ts)
21311
- ...process.env.AGENTX_CHANNEL_ANTICIPATE === "1" ? { anticipate: true, plan: (c, s) => planner.plan(c, s) } : {},
21312
- onEvent: (e) => log37.debug(`channel ${e.trigger}: rule=${e.rule} choice=${e.choice ?? "-"} \u2192 ${e.acted}${e.text ? ` "${e.text}"` : ""}${e.cue ? " (planned cue)" : ""} ${e.ms}ms`),
21480
+ mode,
21481
+ // Anticipated backchannels: on in chatty mode (its preset), else off until the offline replay proves them (tests/voice-bc-anticipate-eval.ts)
21482
+ plan: (c, s) => planner.plan(c, s),
21483
+ ...process.env.AGENTX_CHANNEL_ANTICIPATE === "1" ? { anticipate: true } : {},
21484
+ onEvent: (e) => log38.debug(`channel ${e.trigger}: rule=${e.rule} choice=${e.choice ?? "-"} \u2192 ${e.acted}${e.text ? ` "${e.text}"` : ""}${e.cue ? " (planned cue)" : ""} ${e.ms}ms`),
21313
21485
  ...opts
21314
21486
  });
21315
21487
  }
21488
+ function overlapResponder(ai, mode, out) {
21489
+ const judge = softBarge(mode) ? new OverlapJudge({ model: overlapModelFor(ai) }) : void 0;
21490
+ return (m) => {
21491
+ const text = String(m.text ?? "");
21492
+ void (judge ? judge.judge(text) : Promise.resolve({ verdict: "cut", by: "mode", ms: 0 })).then((d) => out({ type: "overlap", ...m.id !== void 0 ? { id: m.id } : {}, verdict: d.verdict, by: d.by, ms: d.ms }));
21493
+ };
21494
+ }
21316
21495
  async function duplexServe(args, ai, cfg, cwd) {
21317
21496
  const out = (o) => process.stdout.write(JSON.stringify(o) + "\n");
21318
21497
  const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join16(cwd, ".agent", "mcp-auth.json") }));
@@ -21362,7 +21541,7 @@ async function duplexServe(args, ai, cfg, cwd) {
21362
21541
  });
21363
21542
  if (args.voiceSystemPrompt) dx.voice.options.systemPrompt += "\n\n" + args.voiceSystemPrompt;
21364
21543
  let scTurn = 0;
21365
- const sc = args.duplexV2 ? makeSecondChannel(ai, dx.options.reflexModel, { show: (text, kind) => out({ type: "channel", kind, text }) }) : void 0;
21544
+ const sc = args.duplexV2 ? makeSecondChannel(ai, dx.options.reflexModel, args.channelMode, { show: (text, kind) => out({ type: "channel", kind, text }) }) : void 0;
21366
21545
  const store = new SessionStore(cwd);
21367
21546
  const session = startSession(args, store, dx.voice, cwd);
21368
21547
  persist = () => {
@@ -21371,10 +21550,11 @@ async function duplexServe(args, ai, cfg, cwd) {
21371
21550
  try {
21372
21551
  store.save(session);
21373
21552
  } catch (e) {
21374
- log37.warn("duplex session save failed", e);
21553
+ log38.warn("duplex session save failed", e);
21375
21554
  }
21376
21555
  };
21377
- out({ type: "ready", sessionId: session.meta.id, reflexModel: dx.options.reflexModel, actModel: dx.options.actModel, ...notes.length ? { notes } : {} });
21556
+ const overlap2 = overlapResponder(ai, args.duplexV2 ? args.channelMode : void 0, out);
21557
+ out({ type: "ready", sessionId: session.meta.id, reflexModel: dx.options.reflexModel, actModel: dx.options.actModel, softBarge: softBarge(args.duplexV2 ? args.channelMode : void 0), ...notes.length ? { notes } : {} });
21378
21558
  const done = new Promise((resolveDone) => {
21379
21559
  const rl = createInterface({ input: process.stdin });
21380
21560
  rl.on("line", (line) => {
@@ -21389,7 +21569,8 @@ async function duplexServe(args, ai, cfg, cwd) {
21389
21569
  dx.userActive();
21390
21570
  activeTurn?.abort();
21391
21571
  dx.parkInFlightDeliveries();
21392
- } else if (sc && channelInput(sc, m)) {
21572
+ } else if (m.type === "overlap") overlap2(m);
21573
+ else if (sc && channelInput(sc, m)) {
21393
21574
  if (m.type === "interim" && String(m.text ?? "").trim()) dx.userActive();
21394
21575
  } else if (m.type === "cancel") out({ type: "task", kind: "cancel_result", id: m.task, message: dx.cancelTask(String(m.task)) });
21395
21576
  else if (m.type === "exit") rl.close();
@@ -21409,7 +21590,7 @@ async function duplexServe(args, ai, cfg, cwd) {
21409
21590
  done2();
21410
21591
  out({ type: "turn_done", id, finishReason: res?.finishReason ?? "error", ...why ? { error: why } : {} });
21411
21592
  }).catch((e) => {
21412
- log37.error("duplex turn failed", e);
21593
+ log38.error("duplex turn failed", e);
21413
21594
  done2();
21414
21595
  out({ type: "turn_done", id, finishReason: "error", error: String(e?.message ?? e) });
21415
21596
  });
@@ -21427,8 +21608,9 @@ async function channelServe(args, ai, cfg) {
21427
21608
  const out = (o) => process.stdout.write(JSON.stringify(o) + "\n");
21428
21609
  const { reflexModel, notes } = duplexModels({ voiceModel: args.voiceModel, reflexModel: cfg.reflexModel, thinkModel: false, actModel: String(args.model ?? cfg.model ?? ""), apiKeys: { ...cfg.apiKeys, ...apiKeysFromEnv() } });
21429
21610
  const model = reflexModel ?? new DuplexAgentOptions().reflexModel;
21430
- const sc = makeSecondChannel(ai, model, { show: (text, kind) => out({ type: "channel", kind, text }) });
21431
- out({ type: "ready", model, ...notes.length ? { notes } : {} });
21611
+ const sc = makeSecondChannel(ai, model, args.channelMode, { show: (text, kind) => out({ type: "channel", kind, text }) });
21612
+ const overlap2 = overlapResponder(ai, args.channelMode, out);
21613
+ out({ type: "ready", model, channelMode: args.channelMode, softBarge: softBarge(args.channelMode), ...notes.length ? { notes } : {} });
21432
21614
  const rl = createInterface({ input: process.stdin });
21433
21615
  rl.on("line", (line) => {
21434
21616
  let m;
@@ -21441,6 +21623,7 @@ async function channelServe(args, ai, cfg) {
21441
21623
  if (m.type === "user" && typeof m.text === "string") sc.onUserTurn(m.text);
21442
21624
  else if (m.type === "text" && typeof m.text === "string") sc.onMainTokens(m.text);
21443
21625
  else if (m.type === "barge") sc.onBargeIn();
21626
+ else if (m.type === "overlap") overlap2(m);
21444
21627
  else if (m.type === "exit") rl.close();
21445
21628
  });
21446
21629
  await new Promise((r) => rl.on("close", () => r()));