@livx.cc/agentx 0.99.101 → 0.99.103

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,98 @@ 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
+ if (!model && !env.GROQ_API_KEY) return void 0;
9335
+ const m = fastOverlapModel(ai, model);
9336
+ const t0 = performance.now();
9337
+ void m("yeah", AbortSignal.timeout(1e4)).then(
9338
+ () => log19.debug(`fast model warm in ${Math.round(performance.now() - t0)}ms`),
9339
+ (e) => log19.warn(`fast model warm-up failed (the first overlap verdict may be late): ${e?.message ?? e}`)
9340
+ );
9341
+ return m;
9342
+ }
9343
+ var softBarge = (mode) => mode !== "turn";
9344
+
9345
+ // src/voice/instinct.ts
9346
+ var log20 = forComponent("Instinct");
9256
9347
  function voiceDeciderOptions(env = process.env) {
9257
9348
  if (env.TYPESAFE_AI_KEY) return { apiKey: env.TYPESAFE_AI_KEY };
9258
9349
  if (env.GROQ_API_KEY) return { provider: "llm", model: "groq/openai/gpt-oss-120b", apiKey: env.GROQ_API_KEY };
@@ -9285,6 +9376,10 @@ var InstinctOptions = class {
9285
9376
  * (~300-400ms) and can never act. A hold that survives this window is the undecided case (a 1-word overlap
9286
9377
  * the engine would RESUME over at overlapResumeMs=700) — the one where a yield actually changes the outcome. */
9287
9378
  overlapSettleMs = 250;
9379
+ /** Soft barge-in's fast yes/no model (overlap.ts fastOverlapModel) for overlaps the 0ms rules leave open, inside
9380
+ * `overlapBudgetMs`; late/failed → yield. Absent → the generic Choice decides them (slower; no verdict → engine default). */
9381
+ overlapModel;
9382
+ overlapBudgetMs = 300;
9288
9383
  /** ackFor waits this long for the host's ackOutcome before treating the ack as unspoken. Hosts set it to the engine's
9289
9384
  * last chance to fire (bod-voice: 2×adaptiveAckMs — past that a pending pick is dropped as pick_late). */
9290
9385
  ackSettleMs = 1500;
@@ -9457,7 +9552,8 @@ var Instinct = class {
9457
9552
  const gen = ++this.overlapGen;
9458
9553
  if (this.o.overlapSettleMs > 0) await new Promise((r) => setTimeout(r, this.o.overlapSettleMs));
9459
9554
  if (gen !== this.overlapGen) return;
9460
- const d = await this.ask(
9555
+ const fast = overlapRule(heard) ?? this.o.overlapModel;
9556
+ const d = fast ? await this.judgeOverlap(heard) : await this.ask(
9461
9557
  "overlap",
9462
9558
  heard,
9463
9559
  { user_said_over_us: heard },
@@ -9474,6 +9570,12 @@ var Instinct = class {
9474
9570
  this.o.onDecision({ ...d, acted: acted("keep_talking", this.o.onKeepTalking(heard)) });
9475
9571
  } else if (d) this.o.onDecision(d);
9476
9572
  }
9573
+ /** Rules + fast model (OverlapJudge) as an overlap decision; undecided → yield (today's hard barge). */
9574
+ async judgeOverlap(heard) {
9575
+ const j = await new OverlapJudge({ model: this.o.overlapModel, budgetMs: this.o.overlapBudgetMs, now: this.o.now }).judge(heard);
9576
+ const choice = j.verdict === "keep" ? "keep_talking" : "yield";
9577
+ return { kind: "overlap", heard, choice, probabilities: { [choice]: 1 }, ms: j.ms };
9578
+ }
9477
9579
  /** The engine's hold ended (it ceded the floor, resumed over a stalled overlap, began a new turn, or was
9478
9580
  * interrupted) — drop any pending/in-flight overlap consult. Hosts call this on every hold exit. */
9479
9581
  overlapEnded() {
@@ -9512,13 +9614,13 @@ var Instinct = class {
9512
9614
  * unfinished partial is no turn to react to — the interjection itself is the reply. */
9513
9615
  pickAck(ctx) {
9514
9616
  if (this.interjecting) {
9515
- log19.debug(`ack: cut-in turn \u2192 silent (engine context "${ctx.text.slice(0, 40)}" is the previous final)`);
9617
+ log20.debug(`ack: cut-in turn \u2192 silent (engine context "${ctx.text.slice(0, 40)}" is the previous final)`);
9516
9618
  return Promise.resolve(null);
9517
9619
  }
9518
9620
  const text = ctx.text.trim();
9519
9621
  const p = (async () => {
9520
9622
  const intentP = ctx.barged ? this.bargeIntent(text).catch((e) => {
9521
- log19.warn(`barge intent for the ack failed \u2014 situational ack: ${e.message}`);
9623
+ log20.warn(`barge intent for the ack failed \u2014 situational ack: ${e.message}`);
9522
9624
  return null;
9523
9625
  }) : null;
9524
9626
  const d = await this.ask(
@@ -9540,7 +9642,7 @@ var Instinct = class {
9540
9642
  settle = r;
9541
9643
  });
9542
9644
  const timer = setTimeout(() => {
9543
- log19.debug(`ack: no engine outcome in ${this.o.ackSettleMs}ms \u2014 not claimed`);
9645
+ log20.debug(`ack: no engine outcome in ${this.o.ackSettleMs}ms \u2014 not claimed`);
9544
9646
  settle(null);
9545
9647
  }, this.o.ackSettleMs);
9546
9648
  void spoken.then(() => clearTimeout(timer));
@@ -9589,7 +9691,7 @@ var Instinct = class {
9589
9691
  });
9590
9692
  return { kind, heard, choice: answers.act.choice, probabilities: answers.act.probabilities, ms: Math.round(this.o.now() - t0) };
9591
9693
  } catch (e) {
9592
- log19.warn(`${kind} decision failed \u2014 falling back to engine defaults: ${e.message}`);
9694
+ log20.warn(`${kind} decision failed \u2014 falling back to engine defaults: ${e.message}`);
9593
9695
  if (rethrow) throw e;
9594
9696
  return null;
9595
9697
  }
@@ -9598,7 +9700,7 @@ var Instinct = class {
9598
9700
 
9599
9701
  // src/voice/unsaid.ts
9600
9702
  init_logging();
9601
- var log20 = forComponent("Unsaid");
9703
+ var log21 = forComponent("Unsaid");
9602
9704
  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
9705
  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
9706
  var SAID_OVERLAP = 0.6;
@@ -9638,7 +9740,7 @@ async function unsaidClauses(pieces, spoken, decider, timeoutMs = 5e3) {
9638
9740
  return p < CONVEYED_MIN;
9639
9741
  }).map((x) => x.c);
9640
9742
  } catch (e) {
9641
- log20.warn(`unsaid judge failed \u2014 only near-zero-overlap clauses count as unsaid: ${e.message}`);
9743
+ log21.warn(`unsaid judge failed \u2014 only near-zero-overlap clauses count as unsaid: ${e.message}`);
9642
9744
  return fallback();
9643
9745
  } finally {
9644
9746
  clearTimeout(timer);
@@ -9722,13 +9824,13 @@ function scrubSpokenJunk(text, final = false) {
9722
9824
  return s;
9723
9825
  }
9724
9826
  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);
9827
+ const norm4 = (x) => x.replace(/\[[a-z_ ]+\]/gi, "").replace(/[^\p{L}\p{N}]+/gu, " ").trim().toLowerCase();
9828
+ const s = norm4(sentence);
9829
+ return s.replace(/ /g, "").length >= 16 && spoken.split(SENTENCE_SPLIT).some((x) => norm4(x) === s);
9728
9830
  }
9729
9831
 
9730
9832
  // src/duplex.ts
9731
- var log21 = forComponent("DuplexAgent");
9833
+ var log22 = forComponent("DuplexAgent");
9732
9834
  function describeCall(call) {
9733
9835
  const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
9734
9836
  const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
@@ -9916,7 +10018,7 @@ function isOfferAcceptance(text) {
9916
10018
  }
9917
10019
  var isQuestion = (s) => /\?["'”’)\]]*$/.test(s.trim());
9918
10020
  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.';
10021
+ 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
10022
  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
10023
  var THINK_DISABLED_GUIDANCE = "(Think tier is not available \u2014 use Act for all escalations.)";
9922
10024
  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 +10185,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10083
10185
  const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
10084
10186
  if (m) {
10085
10187
  this.fabricationCut = true;
10086
- log21.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
10188
+ log22.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
10087
10189
  this.reflexBuf = this.reflexBuf.slice(0, Math.max(m.index, this.reflexForwarded));
10088
10190
  this.pumpReflex(true);
10089
10191
  return;
@@ -10207,7 +10309,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10207
10309
  this.leakHit = true;
10208
10310
  this.leakCuts.push(leak.trim().slice(0, 40));
10209
10311
  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`);
10312
+ log22.warn(`reflex ${why} ("${leak.trim().slice(0, 40)}\u2026") \u2014 cut before TTS`);
10211
10313
  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
10314
  const next = this.stepMarks.find((k) => k > at);
10213
10315
  if (next === void 0) {
@@ -10224,6 +10326,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10224
10326
  if (!out) return;
10225
10327
  if (out.trim()) this.spokeThisTurn = true;
10226
10328
  this.turnSpoken += out;
10329
+ this.noteVoiced(this.turnSpoken);
10227
10330
  this.emitHost({ kind: "text_delta", message: out });
10228
10331
  }
10229
10332
  /** Leak verdict for one complete sentence: false = speak, true = cut, undefined = the judge is still out.
@@ -10247,7 +10350,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10247
10350
  this.notify("diag", "leak_judge", { text: s.slice(0, 80), p: Math.round(p * 100) / 100, ms: Math.round(performance.now() - t0) });
10248
10351
  return p >= LEAK_JUDGE_THRESHOLD;
10249
10352
  }, (e) => {
10250
- log21.warn(`leak judge failed \u2014 speaking the sentence: ${e.message}`);
10353
+ log22.warn(`leak judge failed \u2014 speaking the sentence: ${e.message}`);
10251
10354
  return false;
10252
10355
  }).then((leak) => {
10253
10356
  clearTimeout(timer);
@@ -10319,7 +10422,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10319
10422
  let i = t.length - 1;
10320
10423
  while (i >= this.turnBase && !(t[i].role === "assistant" && typeof t[i].content === "string" && t[i].content.includes(key))) i--;
10321
10424
  if (i < this.turnBase) {
10322
- log21.warn("analysis leak cut, but its text was not found in the transcript to scrub");
10425
+ log22.warn("analysis leak cut, but its text was not found in the transcript to scrub");
10323
10426
  continue;
10324
10427
  }
10325
10428
  const m = t[i];
@@ -10374,7 +10477,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10374
10477
  const res = await this.voice.send(content);
10375
10478
  const fb = this.options.reflexFallbackModel, model = this.voice.options.model;
10376
10479
  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}`);
10480
+ log22.warn(`reflex ${model} rate-limited \u2014 re-running the turn on ${fb}`);
10378
10481
  this.notify("diag", "reflex_fallback", { from: model, to: fb });
10379
10482
  this.voice.transcript.length = base;
10380
10483
  this.voice.options.model = fb;
@@ -10407,7 +10510,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10407
10510
  try {
10408
10511
  await this.voice.send(reminder);
10409
10512
  } catch (e) {
10410
- log21.warn(`follow-through nudge failed: ${e instanceof Error ? e.message : e}`);
10513
+ log22.warn(`follow-through nudge failed: ${e instanceof Error ? e.message : e}`);
10411
10514
  } finally {
10412
10515
  const i = this.voice.transcript.findIndex((m, k) => k >= base && m.role === "user" && m.content === reminder);
10413
10516
  if (i >= 0) this.voice.transcript.splice(i, 1);
@@ -10468,7 +10571,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10468
10571
  return {
10469
10572
  preToolUse: (call) => {
10470
10573
  if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
10471
- log21.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
10574
+ log22.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
10472
10575
  this.abortSpeculation();
10473
10576
  return { block: true, reason: "Speculative turn aborted." };
10474
10577
  }
@@ -10542,7 +10645,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10542
10645
  try {
10543
10646
  await this.voice.send(reminder);
10544
10647
  } catch (e) {
10545
- log21.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
10648
+ log22.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
10546
10649
  } finally {
10547
10650
  this.nudging = false;
10548
10651
  const i = this.voice.transcript.findIndex((m, k) => k >= base && m.role === "user" && m.content === reminder);
@@ -10597,7 +10700,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10597
10700
  const accepted = this.offerOpen && this.parkedRedeliver.length > 0 && this.parkedRedeliver.every((p) => p.task === this.offer.task) && (isOfferAcceptance(content) || isTrivialBarge(content));
10598
10701
  const judge = accepted ? void 0 : this.options.bargeIntent;
10599
10702
  return (accepted ? Promise.resolve("resume") : judge ? judge(content).catch((e) => {
10600
- log21.warn(`bargeIntent judge failed \u2014 using the allowlist: ${e.message}`);
10703
+ log22.warn(`bargeIntent judge failed \u2014 using the allowlist: ${e.message}`);
10601
10704
  return fallback();
10602
10705
  }) : Promise.resolve(fallback())).then((intent) => {
10603
10706
  this.notify("diag", "barge_intent", { intent, text: content.slice(0, 80) });
@@ -10622,7 +10725,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10622
10725
  return this.sendTurn(content, false, rec, ext);
10623
10726
  });
10624
10727
  }
10625
- return this.sendTurn(content, false, rec, ext);
10728
+ return this.sendTurn(typeof content === "string" ? content + this.acceptNote(content) : content, false, rec, ext);
10626
10729
  }
10627
10730
  /** Host-named user turns (send opts.id), so a later send can replace one. Bounded; a replace that arrives
10628
10731
  * BEFORE its target (the host's dispatch hops can reorder) leaves a pre-superseded record behind. */
@@ -10793,7 +10896,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10793
10896
  try {
10794
10897
  res = await this.voice.send(spec.text);
10795
10898
  } catch (e) {
10796
- log21.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
10899
+ log22.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
10797
10900
  } finally {
10798
10901
  this.voice.options.signal = prevSignal;
10799
10902
  }
@@ -10807,7 +10910,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10807
10910
  if (d === "abort") {
10808
10911
  if (this.voice.transcript.length > base) this.voice.transcript.length = base;
10809
10912
  this.speculativeAbortedCalls++;
10810
- log21.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
10913
+ log22.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
10811
10914
  if (this.spec === spec) this.spec = void 0;
10812
10915
  return res ?? empty;
10813
10916
  }
@@ -10875,6 +10978,7 @@ Update: ${brief}`;
10875
10978
  this.bargeMark = this.parkedRedeliver.length;
10876
10979
  this.reflexCut = false;
10877
10980
  this.offerOpen = false;
10981
+ this.asked = "";
10878
10982
  }
10879
10983
  const parked = [];
10880
10984
  for (const rec of this.tasks.values())
@@ -10918,6 +11022,7 @@ Update: ${brief}`;
10918
11022
  if (say) {
10919
11023
  this.offerOpen = false;
10920
11024
  this.options.host?.notify?.({ kind: "speak_utterance", message: say });
11025
+ this.noteVoiced(say);
10921
11026
  if (rec) {
10922
11027
  this.voicedBy.set(say.trim(), rec.id);
10923
11028
  if (this.voicedBy.size > 64) this.voicedBy.delete(this.voicedBy.keys().next().value);
@@ -10930,6 +11035,7 @@ Update: ${brief}`;
10930
11035
  this.offer = { text: rec.capOffer = this.options.spokenOffer, task: rec.id };
10931
11036
  this.offerOpen = true;
10932
11037
  this.options.host?.notify?.({ kind: "speak_utterance", message: this.offer.text });
11038
+ this.noteVoiced(this.offer.text);
10933
11039
  }
10934
11040
  this.notify("diag", "delivery_budget", { task: rec.id, spoke: rec.voicedChars, parked: rest.length });
10935
11041
  } else if (rec.deliveryParked === "barge") this.parkedRedeliver.push({ text: seg, task: rec.id });
@@ -10956,6 +11062,20 @@ Update: ${brief}`;
10956
11062
  offer = { text: "", task: "" };
10957
11063
  /** The cap offer was the last thing voiced (no delivery or turn has spoken since) — a plain yes accepts it (send). */
10958
11064
  offerOpen = false;
11065
+ /** The last thing voiced (reflex reply or delivery), when it ended on a question — a plain yes answers it (acceptNote). */
11066
+ asked = "";
11067
+ noteVoiced(text) {
11068
+ const last = text.trim().split(SENTENCE_SPLIT).at(-1) ?? "";
11069
+ this.asked = isQuestion(last) ? last.trim() : "";
11070
+ }
11071
+ /** 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
11072
+ * an offer read as ambiguous and the offer was asked again instead of done (Para phone call 2026-10-13). */
11073
+ acceptNote(content) {
11074
+ const q2 = this.asked;
11075
+ this.asked = "";
11076
+ return q2 && isOfferAcceptance(content) ? `
11077
+ [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.]` : "";
11078
+ }
10959
11079
  /** The task's outcome now belongs to its integration turn (failed / stopped early): take back its parked pieces and
10960
11080
  * keep it from being revived — a "go on" must not replay raw segments of a result the reflex is about to explain. */
10961
11081
  dropParked(rec) {
@@ -10982,6 +11102,7 @@ Update: ${brief}`;
10982
11102
  if (!text) return;
10983
11103
  this.offerOpen = false;
10984
11104
  this.options.host?.notify?.({ kind: "speak_utterance", message: text });
11105
+ this.noteVoiced(text);
10985
11106
  this.notify("diag", "parked_redelivered", { chars: text.length });
10986
11107
  }
10987
11108
  /** Resolve when all queued voice turns AND all in-flight worker tasks have settled (tests, graceful shutdown). */
@@ -11172,7 +11293,7 @@ Another agent just implemented the above. Independently check the CURRENT state
11172
11293
  this.notify("task_verify", `task ${id}: verifying`, { id });
11173
11294
  const cres = await new Agent(checkerOpts).run(checkBrief);
11174
11295
  if (cres.finishReason !== "stop") {
11175
- log21.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
11296
+ log22.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
11176
11297
  this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
11177
11298
  }
11178
11299
  const sum = (a = 0, b = 0) => a + b;
@@ -11308,7 +11429,7 @@ Another agent just implemented the above. Independently check the CURRENT state
11308
11429
  rec.status = "done";
11309
11430
  rec.result = res.text;
11310
11431
  const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
11311
- log21.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
11432
+ log22.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
11312
11433
  this.notify("task_done", `task ${id} (${rec.label}) completed`, {
11313
11434
  id,
11314
11435
  text: res.text,
@@ -11342,14 +11463,14 @@ Another agent just implemented the above. Independently check the CURRENT state
11342
11463
  foldIfSuperseded(rec, note) {
11343
11464
  if (rec.deliveryParked !== "moved_on") return false;
11344
11465
  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`);
11466
+ log22.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
11346
11467
  return true;
11347
11468
  }
11348
11469
  failTask(rec, msg) {
11349
11470
  this.dropAsk(rec.id);
11350
11471
  rec.status = "error";
11351
11472
  rec.result = msg;
11352
- log21.warn(`task ${rec.id} failed: ${msg}`);
11473
+ log22.warn(`task ${rec.id} failed: ${msg}`);
11353
11474
  this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`, { id: rec.id });
11354
11475
  if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
11355
11476
  this.dropParked(rec);
@@ -11647,7 +11768,7 @@ Another agent just implemented the above. Independently check the CURRENT state
11647
11768
  init_llm();
11648
11769
  init_logging();
11649
11770
  init_models();
11650
- var log22 = forComponent("ModelRouter");
11771
+ var log23 = forComponent("ModelRouter");
11651
11772
  var ModelRouterOptions = class {
11652
11773
  classifier = jevClassifier();
11653
11774
  /** Capability tiers → model id (any provider). basic/standard run the main session; premium is only ever an offload. */
@@ -11731,10 +11852,10 @@ var ModelRouter = class {
11731
11852
  if (tier === "premium") d.offload = this.state.offload && (c.sameThread ?? 0) >= o.sameThreadAt ? "continue" : "fresh";
11732
11853
  } catch (e) {
11733
11854
  const msg = e instanceof Error ? e.message : String(e);
11734
- log22.warn(`classifier failed, falling back to standard: ${msg}`);
11855
+ log23.warn(`classifier failed, falling back to standard: ${msg}`);
11735
11856
  d = { tier: "standard", model: o.models.standard, probs: {}, fallback: msg, classifyMs: Math.round(performance.now() - t0) };
11736
11857
  }
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`);
11858
+ 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
11859
  return d;
11739
11860
  }
11740
11861
  /** Run one user turn on `agent` with routing. Returns a RunResult carrying `costUsd` + `routing`. */
@@ -11786,7 +11907,7 @@ ${text}`;
11786
11907
  const answer = res.text || `(premium sub-agent finished with no reply; finishReason=${res.finishReason})`;
11787
11908
  agent.transcript.push({ role: "user", content }, { role: "assistant", content: answer });
11788
11909
  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`);
11910
+ log23.info(`offload ${cont ? "continue" : "fresh"} done \xB7 ${res.finishReason} \xB7 ${res.steps} steps`);
11790
11911
  const costUsd = o.costOf(main2, briefUsage) + o.costOf(o.models.premium, res.usage);
11791
11912
  return { ...res, text: answer, messages: agent.transcript, usage: addUsage(briefUsage, res.usage), costUsd, routing: d };
11792
11913
  }
@@ -11802,7 +11923,7 @@ The request:
11802
11923
  ${text}`);
11803
11924
  return { text: r.text.trim() || text, usage: r.usage };
11804
11925
  } catch (e) {
11805
- log22.warn(`brief failed, sending the raw request: ${e instanceof Error ? e.message : String(e)}`);
11926
+ log23.warn(`brief failed, sending the raw request: ${e instanceof Error ? e.message : String(e)}`);
11806
11927
  return { text };
11807
11928
  } finally {
11808
11929
  agent.transcript = snap;
@@ -11847,7 +11968,7 @@ async function jevAvailable(opts = {}) {
11847
11968
  const { env } = await loadJev(opts);
11848
11969
  return JEV_KEYS.some((k) => env[k]?.trim());
11849
11970
  } catch (e) {
11850
- log22.debug(`jev unavailable: ${e instanceof Error ? e.message : String(e)}`);
11971
+ log23.debug(`jev unavailable: ${e instanceof Error ? e.message : String(e)}`);
11851
11972
  return false;
11852
11973
  }
11853
11974
  }
@@ -12076,7 +12197,7 @@ init_logging();
12076
12197
  init_platform();
12077
12198
  import { spawn } from "child_process";
12078
12199
  import { createHash } from "crypto";
12079
- var log23 = forComponent("mcp");
12200
+ var log24 = forComponent("mcp");
12080
12201
  var PROTOCOL_VERSION = "2025-06-18";
12081
12202
  var DEFAULT_TIMEOUT_MS = 3e4;
12082
12203
  var DEFAULT_CALL_TIMEOUT_MS = 30 * 6e4;
@@ -12108,7 +12229,7 @@ var StdioTransport = class {
12108
12229
  proc.stdout.setEncoding("utf8");
12109
12230
  proc.stdout.on("data", (chunk) => this.onData(chunk));
12110
12231
  proc.stderr.setEncoding("utf8");
12111
- proc.stderr.on("data", (chunk) => log23.debug(`[${command}] stderr:`, chunk.trimEnd()));
12232
+ proc.stderr.on("data", (chunk) => log24.debug(`[${command}] stderr:`, chunk.trimEnd()));
12112
12233
  proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
12113
12234
  proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
12114
12235
  }
@@ -12122,7 +12243,7 @@ var StdioTransport = class {
12122
12243
  try {
12123
12244
  this.dispatch(JSON.parse(line));
12124
12245
  } catch (e) {
12125
- log23.debug("dropping non-JSON line from MCP server:", line, e);
12246
+ log24.debug("dropping non-JSON line from MCP server:", line, e);
12126
12247
  }
12127
12248
  }
12128
12249
  }
@@ -12132,7 +12253,7 @@ var StdioTransport = class {
12132
12253
  try {
12133
12254
  p2?.onProgress?.(msg.params ?? {});
12134
12255
  } catch (e) {
12135
- log23.debug("onProgress handler threw", e);
12256
+ log24.debug("onProgress handler threw", e);
12136
12257
  }
12137
12258
  return;
12138
12259
  }
@@ -12170,7 +12291,7 @@ var StdioTransport = class {
12170
12291
  const onAbort = () => {
12171
12292
  if (!this.pending.delete(id)) return;
12172
12293
  clearTimeout(timer);
12173
- this.notify("notifications/cancelled", { requestId: id, reason: String(signal?.reason?.message ?? "cancelled") }).catch((e) => log23.debug("cancel notify failed", e));
12294
+ this.notify("notifications/cancelled", { requestId: id, reason: String(signal?.reason?.message ?? "cancelled") }).catch((e) => log24.debug("cancel notify failed", e));
12174
12295
  reject(abortError(signal));
12175
12296
  };
12176
12297
  const done = () => signal?.removeEventListener("abort", onAbort);
@@ -12194,7 +12315,7 @@ var StdioTransport = class {
12194
12315
  try {
12195
12316
  this.proc?.stdin?.end();
12196
12317
  } catch (e) {
12197
- log23.debug("stdin end failed", e);
12318
+ log24.debug("stdin end failed", e);
12198
12319
  }
12199
12320
  if (!(IS_WIN && killTree(this.proc?.pid, "SIGTERM"))) this.proc?.kill();
12200
12321
  }
@@ -12229,7 +12350,7 @@ var HttpTransport = class {
12229
12350
  return await this.post({ jsonrpc: "2.0", id, method, params: opts.onProgress ? withProgressToken(params, id) : params }, false, signal, opts.onProgress);
12230
12351
  } catch (e) {
12231
12352
  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));
12353
+ void this.notify("notifications/cancelled", { requestId: id, reason: String(opts.signal.reason?.message ?? "cancelled") }).catch((err2) => log24.debug("cancel notify failed", err2));
12233
12354
  throw abortError(opts.signal);
12234
12355
  }
12235
12356
  throw e;
@@ -12272,7 +12393,7 @@ function sseMessage(line) {
12272
12393
  try {
12273
12394
  return JSON.parse(trimmed.slice(5).trim());
12274
12395
  } catch (e) {
12275
- log23.debug("skipping unparseable SSE data line", e);
12396
+ log24.debug("skipping unparseable SSE data line", e);
12276
12397
  return void 0;
12277
12398
  }
12278
12399
  }
@@ -12282,7 +12403,7 @@ async function readSseResponse(resp, id, onProgress) {
12282
12403
  try {
12283
12404
  onProgress?.(obj.params ?? {});
12284
12405
  } catch (e) {
12285
- log23.debug("onProgress handler threw", e);
12406
+ log24.debug("onProgress handler threw", e);
12286
12407
  }
12287
12408
  return void 0;
12288
12409
  }
@@ -12381,7 +12502,7 @@ async function mountWithDeadline(name, cfg, mountTimeoutMs) {
12381
12502
  return { name, client, tools, specs, serverInfo: init?.serverInfo, config: cfg };
12382
12503
  })(), mountTimeoutMs, name);
12383
12504
  } catch (e) {
12384
- await client.close().catch((err2) => log23.debug(`close after failed mount of "${name}": ${err2}`));
12505
+ await client.close().catch((err2) => log24.debug(`close after failed mount of "${name}": ${err2}`));
12385
12506
  throw e;
12386
12507
  }
12387
12508
  }
@@ -12392,15 +12513,15 @@ function validEntries(servers) {
12392
12513
  return Object.entries(servers).filter(([name, cfg]) => {
12393
12514
  if (!cfg || cfg.disabled) return false;
12394
12515
  if (!cfg.command && !cfg.url) {
12395
- log23.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
12516
+ log24.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
12396
12517
  return false;
12397
12518
  }
12398
12519
  return true;
12399
12520
  });
12400
12521
  }
12401
12522
  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}`);
12523
+ if (e instanceof McpAuthError) log24.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
12524
+ else log24.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
12404
12525
  }
12405
12526
  async function mountMcpServers(servers = {}, opts = {}) {
12406
12527
  const entries = validEntries(servers);
@@ -12410,7 +12531,7 @@ async function mountMcpServers(servers = {}, opts = {}) {
12410
12531
  const name = entries[i][0];
12411
12532
  if (r.status === "fulfilled") {
12412
12533
  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}` : ""}`);
12534
+ log24.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
12414
12535
  } else logMountFailure(name, r.reason);
12415
12536
  });
12416
12537
  return out;
@@ -12450,7 +12571,7 @@ var McpPool = class {
12450
12571
  const prev = this.warm.get(key);
12451
12572
  if (prev) {
12452
12573
  clearTimeout(prev.timer);
12453
- if (prev.client !== client) void prev.client.close().catch((err2) => log23.debug(`warm-pool replace close failed: ${err2}`));
12574
+ if (prev.client !== client) void prev.client.close().catch((err2) => log24.debug(`warm-pool replace close failed: ${err2}`));
12454
12575
  }
12455
12576
  const e = { client, timer: void 0 };
12456
12577
  this.warm.set(key, e);
@@ -12467,7 +12588,7 @@ var McpPool = class {
12467
12588
  const e = this.warm.get(key);
12468
12589
  if (!e) return;
12469
12590
  this.warm.delete(key);
12470
- await e.client.close().catch((err2) => log23.debug(`warm-pool evict close failed: ${err2}`));
12591
+ await e.client.close().catch((err2) => log24.debug(`warm-pool evict close failed: ${err2}`));
12471
12592
  }
12472
12593
  async closeAll() {
12473
12594
  for (const e of this.warm.values()) {
@@ -12686,7 +12807,7 @@ init_tools_shell();
12686
12807
  // src/tools.notify.ts
12687
12808
  init_logging();
12688
12809
  import { execFile as execFile2 } from "child_process";
12689
- var log25 = forComponent("notify");
12810
+ var log26 = forComponent("notify");
12690
12811
  var WIN_TOAST = [
12691
12812
  "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null",
12692
12813
  "$t = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)",
@@ -12717,7 +12838,7 @@ function makeNotifyTool(opts = {}) {
12717
12838
  return new Promise((resolve6) => {
12718
12839
  run(argv[0], argv[1], { timeout: 5e3, ...extra }, (e) => {
12719
12840
  if (e) {
12720
- log25.debug("notification failed", e);
12841
+ log26.debug("notification failed", e);
12721
12842
  resolve6(`Notification failed: ${e.message}`);
12722
12843
  } else resolve6("Notification shown.");
12723
12844
  });
@@ -12733,7 +12854,7 @@ import { BodDB as BodDB2 } from "@bod.ee/db";
12733
12854
  init_logging();
12734
12855
  import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
12735
12856
  import { homedir } from "os";
12736
- var log26 = forComponent("cli-util");
12857
+ var log27 = forComponent("cli-util");
12737
12858
  function dotDirs(base, sub, opts = {}) {
12738
12859
  const home = opts.home ?? homedir();
12739
12860
  const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
@@ -12750,7 +12871,7 @@ function parseJson(text, fallback, what = "json") {
12750
12871
  try {
12751
12872
  return JSON.parse(text);
12752
12873
  } catch (e) {
12753
- log26.debug(`parseJson(${what}) failed: ${e.message}`);
12874
+ log27.debug(`parseJson(${what}) failed: ${e.message}`);
12754
12875
  return fallback;
12755
12876
  }
12756
12877
  }
@@ -12760,7 +12881,7 @@ function readJsonFile(path, fallback) {
12760
12881
  try {
12761
12882
  text = readFileSync5(path, "utf8");
12762
12883
  } catch (e) {
12763
- log26.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
12884
+ log27.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
12764
12885
  return fallback;
12765
12886
  }
12766
12887
  return parseJson(text, fallback, path);
@@ -13064,7 +13185,7 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as rea
13064
13185
  import { homedir as homedir3 } from "os";
13065
13186
  import { dirname as dirname4, join as join7 } from "path";
13066
13187
  import { fileURLToPath } from "url";
13067
- var log27 = forComponent("VoiceIO");
13188
+ var log28 = forComponent("VoiceIO");
13068
13189
  var now3 = () => performance.now();
13069
13190
  var Player = class {
13070
13191
  proc = null;
@@ -13078,7 +13199,7 @@ var Player = class {
13078
13199
  ["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
13079
13200
  { stdio: ["pipe", "ignore", "ignore"] }
13080
13201
  );
13081
- this.proc.on("error", (e) => log27.warn(`ffplay error: ${e.message}`));
13202
+ this.proc.on("error", (e) => log28.warn(`ffplay error: ${e.message}`));
13082
13203
  this.proc.stdin.on("error", () => {
13083
13204
  });
13084
13205
  this.bytesWritten = 0;
@@ -13116,7 +13237,7 @@ function detectFfmpegMic() {
13116
13237
  const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
13117
13238
  const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
13118
13239
  if (!mic) throw new Error("no audio input device found");
13119
- log27.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
13240
+ log28.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
13120
13241
  return `:${mic.idx}`;
13121
13242
  }
13122
13243
  function detectDshowMic() {
@@ -13127,7 +13248,7 @@ function detectDshowMic() {
13127
13248
  const devices = [...(r.stderr ?? "").matchAll(/"([^"]+)"\s*\(audio\)/g)].map((m) => m[1]);
13128
13249
  const mic = devices.find((d) => !/stereo mix|loopback|virtual|cable|teams/i.test(d)) ?? devices[0];
13129
13250
  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}`);
13251
+ log28.debug(`ffmpeg dshow mic: ${mic}`);
13131
13252
  return `audio=${mic}`;
13132
13253
  }
13133
13254
  function detectedInputDevice() {
@@ -13163,15 +13284,15 @@ function resolveAecBinary() {
13163
13284
  if (existsSync4(bin) && statSync6(bin).mtimeMs >= statSync6(src).mtimeMs) return bin;
13164
13285
  if (spawnSync3("which", ["swiftc"]).status !== 0) return null;
13165
13286
  mkdirSync5(cacheDir, { recursive: true });
13166
- log27.info("compiling AEC mic helper (first run)\u2026");
13287
+ log28.info("compiling AEC mic helper (first run)\u2026");
13167
13288
  const build = spawnSync3("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
13168
13289
  if (build.status !== 0) {
13169
- log27.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
13290
+ log28.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
13170
13291
  return null;
13171
13292
  }
13172
13293
  const sign = spawnSync3("codesign", ["-fs", "-", bin], { encoding: "utf8" });
13173
13294
  if (sign.status !== 0) {
13174
- log27.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
13295
+ log28.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
13175
13296
  return null;
13176
13297
  }
13177
13298
  return bin;
@@ -13213,17 +13334,17 @@ var NodeMicSource = class {
13213
13334
  this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
13214
13335
  } else {
13215
13336
  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");
13337
+ log28.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
13217
13338
  const input = process.platform === "win32" ? ["-f", "dshow", "-i", detectDshowMic()] : ["-f", "avfoundation", "-i", detectFfmpegMic()];
13218
13339
  this.proc = spawn2(
13219
13340
  "ffmpeg",
13220
13341
  ["-loglevel", "error", ...input, "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
13221
13342
  { stdio: ["ignore", "pipe", "pipe"], ...process.platform === "win32" ? { windowsHide: true } : {} }
13222
13343
  );
13223
- this.proc.stderr.on("data", (d) => log27.warn(`ffmpeg: ${String(d).trim()}`));
13344
+ this.proc.stderr.on("data", (d) => log28.warn(`ffmpeg: ${String(d).trim()}`));
13224
13345
  }
13225
13346
  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`);
13347
+ if (c && !this.stopped) log28.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
13227
13348
  });
13228
13349
  this.proc.stdout.on("data", (chunk) => onChunk(chunk));
13229
13350
  }
@@ -13289,7 +13410,7 @@ var AecDuplexAudio = class {
13289
13410
  this.proc.stdin.on("error", () => {
13290
13411
  });
13291
13412
  this.proc.on("exit", (c) => {
13292
- if (c && !this.stopped) log27.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
13413
+ if (c && !this.stopped) log28.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
13293
13414
  });
13294
13415
  this.proc.stdout.on("data", (chunk) => {
13295
13416
  this.gotChunk = true;
@@ -13305,7 +13426,7 @@ var AecDuplexAudio = class {
13305
13426
  openMicSettings();
13306
13427
  this.onFatal?.("microphone permission denied \u2014 enable it in System Settings \u2192 Privacy & Security \u2192 Microphone for your terminal, then restart it");
13307
13428
  }
13308
- } else log27.debug(`mic-aec: ${s}`);
13429
+ } else log28.debug(`mic-aec: ${s}`);
13309
13430
  }
13310
13431
  });
13311
13432
  if (!this.noVpio && !this.triedFallback) {
@@ -13314,7 +13435,7 @@ var AecDuplexAudio = class {
13314
13435
  this.triedFallback = true;
13315
13436
  this.noVpio = true;
13316
13437
  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)");
13438
+ 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
13439
  this.onDegrade?.();
13319
13440
  this.killProc();
13320
13441
  this.spawnHelper();
@@ -13485,21 +13606,15 @@ function bindAckInstinct(v, instinct) {
13485
13606
  if (ev.kind === "ack_fired") instinct.ackOutcome(String(ev.phrase));
13486
13607
  else if (ev.kind === "ack_skipped" || ev.kind === "ack_cancelled") instinct.ackOutcome(null);
13487
13608
  } catch (e) {
13488
- log27.warn(`ack outcome tap failed: ${e instanceof Error ? e.message : e}`);
13609
+ log28.warn(`ack outcome tap failed: ${e instanceof Error ? e.message : e}`);
13489
13610
  }
13490
13611
  onDiag(ev);
13491
13612
  };
13492
13613
  }
13493
13614
  function bindSecondChannel(v, sc, live) {
13494
13615
  const o = v.options;
13495
- const { onPartial, onUtterance, onState, onBargeIn, onBackchannel } = o;
13496
- const { show } = sc.options;
13616
+ const { onPartial, onUtterance, onState, onBargeIn } = o;
13497
13617
  let own = false;
13498
- let blip = "";
13499
- o.onBackchannel = (p) => {
13500
- blip = p;
13501
- onBackchannel(p);
13502
- };
13503
13618
  const take = v.takeInterruptedReply.bind(v);
13504
13619
  v.takeInterruptedReply = () => {
13505
13620
  const r = take();
@@ -13507,7 +13622,7 @@ function bindSecondChannel(v, sc, live) {
13507
13622
  return r.queued.length ? { ...r, full: "", heard: "" } : null;
13508
13623
  };
13509
13624
  o.onPartial = (t) => {
13510
- if (live() && !v.audible) sc.onInterim(t);
13625
+ if (live() && (own || !v.audible)) sc.onInterim(t);
13511
13626
  onPartial(t);
13512
13627
  };
13513
13628
  o.onUtterance = ((t, meta) => {
@@ -13530,25 +13645,34 @@ function bindSecondChannel(v, sc, live) {
13530
13645
  if (live()) sc.noteReply(t);
13531
13646
  return speakDelta(t, ...rest);
13532
13647
  });
13648
+ Object.defineProperty(sc.options, "aec", { get: () => v.usingAec, set: () => {
13649
+ }, enumerable: true, configurable: true });
13533
13650
  Object.assign(sc.options, {
13534
- aec: v.usingAec,
13535
13651
  speak: (text, kind) => {
13536
13652
  if (!live() || v.audible) return false;
13537
- if (kind === "backchannel") {
13538
- if (sc.language !== "en") return false;
13539
- blip = "";
13540
- v.fireBackchannel();
13541
- return;
13542
- }
13653
+ if (kind === "backchannel") return speakBlip(v, text);
13543
13654
  own = true;
13544
13655
  v.speakFiller(text);
13545
13656
  return v.awaitIdle().finally(() => {
13546
13657
  own = false;
13547
13658
  });
13548
- },
13549
- show: (text, kind) => show(kind === "backchannel" && blip ? blip : text, kind)
13659
+ }
13550
13660
  });
13551
13661
  }
13662
+ function speakBlip(v, text) {
13663
+ const E = VoiceEngine;
13664
+ const pool = E.BACKCHANNELS;
13665
+ E.BACKCHANNELS = [text];
13666
+ try {
13667
+ v.fireBackchannel();
13668
+ return true;
13669
+ } catch (e) {
13670
+ log28.warn(`backchannel blip failed \u2014 line dropped: ${e.message}`);
13671
+ return false;
13672
+ } finally {
13673
+ E.BACKCHANNELS = pool;
13674
+ }
13675
+ }
13552
13676
  function channelInput(sc, m) {
13553
13677
  if (m.type === "interim") {
13554
13678
  if (typeof m.aec === "boolean") sc.options.aec = m.aec;
@@ -13562,7 +13686,7 @@ function channelInput(sc, m) {
13562
13686
  }
13563
13687
  if (m.type === "agent_state") {
13564
13688
  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`);
13689
+ else log28.warn(`agent_state: unknown state ${JSON.stringify(m.state)} \u2014 ignored`);
13566
13690
  return true;
13567
13691
  }
13568
13692
  return false;
@@ -13686,7 +13810,7 @@ function fakeVoiceParts(uttFile) {
13686
13810
  init_logging();
13687
13811
  import { appendFile, mkdirSync as mkdirSync6 } from "fs";
13688
13812
  import { dirname as dirname5 } from "path";
13689
- var log28 = forComponent("VoiceDiag");
13813
+ var log29 = forComponent("VoiceDiag");
13690
13814
  var JsonlDiagSink = class {
13691
13815
  constructor(path) {
13692
13816
  this.path = path;
@@ -13727,7 +13851,7 @@ var JsonlDiagSink = class {
13727
13851
  disable(e) {
13728
13852
  this.disabled = true;
13729
13853
  this.queue.length = 0;
13730
- log28.debug(`voice diag sink disabled (${this.path}): ${e instanceof Error ? e.message : e}`);
13854
+ log29.debug(`voice diag sink disabled (${this.path}): ${e instanceof Error ? e.message : e}`);
13731
13855
  }
13732
13856
  /** True once a failure permanently disabled the sink (tests/inspection). */
13733
13857
  get isDisabled() {
@@ -13749,7 +13873,7 @@ init_logging();
13749
13873
  init_logging();
13750
13874
  import { mkdir, readFile, writeFile } from "fs/promises";
13751
13875
  import { join as join8 } from "path";
13752
- var log29 = forComponent("ChannelBanks");
13876
+ var log30 = forComponent("ChannelBanks");
13753
13877
  var LINE_KINDS = ["backchannel", "filler", "bridge"];
13754
13878
  var BANK_KEYS = [...LINE_KINDS, "phatic"];
13755
13879
  var EN_PHATIC = [
@@ -13782,9 +13906,9 @@ var EN_PHATIC = [
13782
13906
  var EN_BANK = { ...CHANNEL_BANK, phatic: EN_PHATIC };
13783
13907
  var primaryLang = (tag) => tag.trim().toLowerCase().split(/[-_]/)[0];
13784
13908
  function isPhatic(text, bank) {
13785
- const t = norm2(text);
13909
+ const t = norm3(text);
13786
13910
  return !!t && !!bank?.phatic?.some((p) => {
13787
- const q2 = norm2(p);
13911
+ const q2 = norm3(p);
13788
13912
  return !!q2 && (t === q2 || t.startsWith(q2 + " "));
13789
13913
  });
13790
13914
  }
@@ -13803,8 +13927,10 @@ function requestOpener(text) {
13803
13927
  return ASK.includes(w[0]) || WANT.test(w.slice(0, 4).join(" ")) || new RegExp("^\u05EA\\p{Script=Hebrew}{2,}$", "u").test(w[0]);
13804
13928
  }
13805
13929
  function pendingQuestion(text, lastWords = 6) {
13806
- const t = text.toLowerCase().replace(/[^\p{L}\p{N}'\s]+/gu, " ").split(/\s+/).filter(Boolean).slice(-lastWords).join(" ");
13807
- return /\b(can|could|would|will|do|does|did|is|are|should)\s+(you|we|i|it|there|they|he|she)\b/.test(t) || /(^|\s)(האם|אתה יכול|את יכולה)(\s|$)/u.test(t);
13930
+ const all = text.toLowerCase().replace(/[^\p{L}\p{N}'\s.!?]+/gu, " ");
13931
+ const sentence = all.split(/[.!?]/).at(-1).split(/\s+/).filter(Boolean);
13932
+ const t = sentence.slice(-lastWords).join(" ");
13933
+ return /\b(can|could|would|will|do|does|did|is|are|should)\s+(you|we|i|it|there|they|he|she)\b/.test(t) || /(^|\s)(האם|אתה יכול|את יכולה)(\s|$)/u.test(t) || /\b(what|why|how|where|when|who|which)\s+(do|does|did|is|are|should|would|can|could|will)\s+(you|we|i|it|they|he|she)\b/.test(sentence.slice(-4 * lastWords).join(" "));
13808
13934
  }
13809
13935
  var SCRIPTS = [
13810
13936
  [new RegExp("\\p{Script=Hebrew}", "gu"), "he"],
@@ -13862,16 +13988,16 @@ function fitsLanguage(line, lang) {
13862
13988
  const ws = line.toLowerCase().match(new RegExp("\\p{L}+", "gu")) ?? [];
13863
13989
  return !ws.some((w) => !mine?.has(w) && Object.entries(STOP3).some(([l, s]) => l !== lang && s.has(w)));
13864
13990
  }
13865
- var norm2 = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}' ]/gu, "").replace(/\s+/g, " ").trim();
13991
+ var norm3 = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}' ]/gu, "").replace(/\s+/g, " ").trim();
13866
13992
  function cleanLine(s, lang, maxWords = 6) {
13867
13993
  const raw = s.trim().split("\n")[0];
13868
13994
  const l = raw.replace(/^[-*•\d.)\s]+/, "").replace(/^["'“”«»]+|["'“”«»]+$/g, "").replace(new RegExp("^\\p{L}+:\\s*", "u"), "").trim();
13869
13995
  if (!l || isLeakSuspect(raw) || isLeakSuspect(l)) {
13870
- if (l) log29.debug(`line "${raw}" reads as meta \u2014 rejected`);
13996
+ if (l) log30.debug(`line "${raw}" reads as meta \u2014 rejected`);
13871
13997
  return "";
13872
13998
  }
13873
13999
  if (lang && !fitsLanguage(l, lang)) {
13874
- log29.debug(`line "${l}" is not ${lang} \u2014 rejected`);
14000
+ log30.debug(`line "${l}" is not ${lang} \u2014 rejected`);
13875
14001
  return "";
13876
14002
  }
13877
14003
  return l.split(/\s+/).length > maxWords || l.length > 60 ? "" : l;
@@ -13883,9 +14009,9 @@ function validateBank(raw, lang, minPerKind = 3) {
13883
14009
  for (const k of BANK_KEYS) {
13884
14010
  const v = raw[k];
13885
14011
  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)));
14012
+ 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
14013
  if (out[k].length < minPerKind) {
13888
- log29.warn(`bank ${lang}: only ${out[k].length} valid ${k} lines \u2014 rejected`);
14014
+ log30.warn(`bank ${lang}: only ${out[k].length} valid ${k} lines \u2014 rejected`);
13889
14015
  return null;
13890
14016
  }
13891
14017
  }
@@ -13955,11 +14081,11 @@ var ChannelBanks = class {
13955
14081
  const f = this.file(lang);
13956
14082
  let cached = null;
13957
14083
  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}`);
14084
+ if (e.code !== "ENOENT") log30.warn(`bank cache ${f} unreadable \u2014 regenerating: ${e.message}`);
13959
14085
  return null;
13960
14086
  });
13961
14087
  if (cached && cached.v !== BANK_VERSION) {
13962
- log29.debug(`bank ${lang}: cache v${cached.v ?? 1} predates v${BANK_VERSION} \u2014 regenerating`);
14088
+ log30.debug(`bank ${lang}: cache v${cached.v ?? 1} predates v${BANK_VERSION} \u2014 regenerating`);
13963
14089
  cached = null;
13964
14090
  }
13965
14091
  const valid = cached && validateBank(cached.bank, lang, 0);
@@ -13967,7 +14093,7 @@ var ChannelBanks = class {
13967
14093
  if (valid) {
13968
14094
  for (const k of BANK_KEYS) if (!valid[k].length) valid[k] = [...this.o.fallback[lang]?.[k] ?? []];
13969
14095
  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"}`);
14096
+ log30.debug(`bank ${lang}: cache hit (${BANK_KEYS.map((k) => valid[k].length).join("/")})${complete ? "" : " \u2014 missing kinds, refreshing"}`);
13971
14097
  }
13972
14098
  if (complete && this.o.now() - cached.at < this.o.refreshMs) return;
13973
14099
  await this.generate(lang, valid);
@@ -13983,14 +14109,14 @@ var ChannelBanks = class {
13983
14109
  const bank = {};
13984
14110
  for (const k of BANK_KEYS) {
13985
14111
  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);
14112
+ bank[k] = [...fresh[k], ...have?.[k] ?? []].filter((x) => !seen.has(norm3(x)) && seen.add(norm3(x))).slice(0, this.o.maxPerKind);
13987
14113
  }
13988
14114
  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`);
14115
+ log30.debug(`bank ${lang}: ${have ? "refreshed" : "generated"} (${BANK_KEYS.map((k) => bank[k].length).join("/")}) in ${Math.round(this.o.now() - t0)}ms`);
13990
14116
  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}`));
14117
+ 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
14118
  } 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}`);
14119
+ log30.warn(`bank ${lang}: generation failed \u2014 ${have ? "keeping the cached bank" : lang in this.o.fallback ? "fallback bank" : "silent"}: ${e.message}`);
13994
14120
  } finally {
13995
14121
  clearTimeout(t);
13996
14122
  if (!this.banks.has(lang)) setTimeout(() => this.pending.delete(lang), this.o.retryMs).unref?.();
@@ -13999,7 +14125,7 @@ var ChannelBanks = class {
13999
14125
  };
14000
14126
 
14001
14127
  // src/voice/backchannelPlan.ts
14002
- var log30 = forComponent("BackchannelPlan");
14128
+ var log31 = forComponent("BackchannelPlan");
14003
14129
  var TURN_TYPES = ["story", "list", "complaint", "explanation", "short_command", "question", "other"];
14004
14130
  var DENSITIES = ["none", "low", "normal", "high"];
14005
14131
  var silentTurn = (p) => p.density === "none" || p.turn === "short_command" || p.turn === "question";
@@ -14064,13 +14190,13 @@ Reply JSON only: {"turn":"...","density":"...","cues":[{"when":["phrase",...],"l
14064
14190
  const reasoning = /gpt-oss/.test(o.model) ? { providerOptions: { reasoning_effort: "low" } } : {};
14065
14191
  const r = await this.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false, signal, maxTokens: o.maxTokens, ...reasoning });
14066
14192
  const p = parsePlan(r.content ?? "", c.lang);
14067
- if (!p) log30.debug(`unusable plan: ${(r.content ?? "").slice(0, 120)}`);
14193
+ if (!p) log31.debug(`unusable plan: ${(r.content ?? "").slice(0, 120)}`);
14068
14194
  return p;
14069
14195
  }
14070
14196
  };
14071
14197
 
14072
14198
  // src/voice/secondChannel.ts
14073
- var log31 = forComponent("SecondChannel");
14199
+ var log32 = forComponent("SecondChannel");
14074
14200
  var CHANNEL_MODES = {
14075
14201
  turn: { kinds: ["bridge"], bridgeDelayMs: 1500 },
14076
14202
  normal: {},
@@ -14180,13 +14306,13 @@ function withBudget(p, ms, what, ac) {
14180
14306
  let t;
14181
14307
  const late = new Promise((r) => {
14182
14308
  t = setTimeout(() => {
14183
- log31.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
14309
+ log32.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
14184
14310
  ac?.abort();
14185
14311
  r(null);
14186
14312
  }, ms);
14187
14313
  });
14188
14314
  return Promise.race([p.catch((e) => {
14189
- log31.warn(`${what} failed \u2014 falling back: ${e?.message ?? e}`);
14315
+ log32.warn(`${what} failed \u2014 falling back: ${e?.message ?? e}`);
14190
14316
  return null;
14191
14317
  }), late]).finally(() => clearTimeout(t));
14192
14318
  }
@@ -14258,7 +14384,7 @@ var SecondChannel = class {
14258
14384
  if (host) this.pinned = true;
14259
14385
  const lang = primaryLang(tag);
14260
14386
  if (!lang || lang === this.lang) return;
14261
- log31.debug(`language ${this.lang} \u2192 ${lang}`);
14387
+ log32.debug(`language ${this.lang} \u2192 ${lang}`);
14262
14388
  this.lang = lang;
14263
14389
  this.recent = [];
14264
14390
  this.picks.clear();
@@ -14276,12 +14402,7 @@ var SecondChannel = class {
14276
14402
  }
14277
14403
  get o() {
14278
14404
  const o = this.options;
14279
- if (o.mode !== "chatty" || o.aec) return o;
14280
- if (!this.degradedLogged) {
14281
- this.degradedLogged = true;
14282
- log31.info("chatty mode without echo cancellation \u2014 normal timing (our lines would re-trigger the mic)");
14283
- }
14284
- return { ...o, ...this.normalTiming };
14405
+ return o.mode !== "chatty" || o.aec ? o : { ...o, ...this.normalTiming };
14285
14406
  }
14286
14407
  /** The user's live partial for the current turn (the whole partial, not a delta). */
14287
14408
  onInterim(text) {
@@ -14394,7 +14515,7 @@ var SecondChannel = class {
14394
14515
  try {
14395
14516
  this.o.cancel();
14396
14517
  } catch (e) {
14397
- log31.warn(`cancel threw: ${e.message}`);
14518
+ log32.warn(`cancel threw: ${e.message}`);
14398
14519
  }
14399
14520
  }
14400
14521
  }
@@ -14465,6 +14586,11 @@ var SecondChannel = class {
14465
14586
  const n0 = words2(this.heard).length;
14466
14587
  this.decidedAt = n0;
14467
14588
  if (!(this.o.anticipate && this.plan && trigger === "interim")) cue = null;
14589
+ const opts = this.options;
14590
+ if (opts.mode === "chatty" && !opts.aec && !this.degradedLogged) {
14591
+ this.degradedLogged = true;
14592
+ log32.info("chatty mode without echo cancellation \u2014 normal timing (our lines would re-trigger the mic)");
14593
+ }
14468
14594
  const rule = this.rule(trigger, pauseMs, breath, cue);
14469
14595
  if (rule === "silence") return;
14470
14596
  this.busy = true;
@@ -14479,19 +14605,19 @@ var SecondChannel = class {
14479
14605
  const d = await withBudget(this.o.decide(this.tail(), { ...this.state(pauseMs), rule }, ac.signal), this.o.decisionBudgetMs, "channel decision", ac);
14480
14606
  choice = d?.choice;
14481
14607
  if (choice && choice in CHANNEL_ACTIONS && (choice === "silence" || this.allowed(choice))) kind = choice;
14482
- else if (choice && choice in CHANNEL_ACTIONS) log31.debug(`decider chose ${choice} for a ${rule} moment \u2014 the rule stands`);
14608
+ else if (choice && choice in CHANNEL_ACTIONS) log32.debug(`decider chose ${choice} for a ${rule} moment \u2014 the rule stands`);
14483
14609
  }
14484
14610
  if (kind === "silence") return ev({ choice, acted: "silence" });
14485
14611
  if (stale()) return ev({ choice, kind, acted: "stale" });
14486
14612
  if (!this.allowed(kind)) return ev({ choice, kind, acted: "gated" });
14487
14613
  const react = kind === "backchannel" && this.o.now() - this.lastReactAt >= this.o.reactionGapMs;
14488
- const text = cue && react && !this.recent.some((r) => norm2(r) === norm2(cue.line)) ? cue.line : await this.line(kind, react);
14614
+ const text = cue && react && !this.recent.some((r) => norm3(r) === norm3(cue.line)) ? cue.line : await this.line(kind, react);
14489
14615
  if (!text) return ev({ choice, kind, acted: "no_bank" });
14490
14616
  if (stale()) return ev({ choice, kind, text, acted: "stale" });
14491
14617
  if (!this.allowed(kind)) return ev({ choice, kind, text, acted: "gated" });
14492
14618
  const said = this.say(kind, text);
14493
14619
  if (said && cue) this.cueFrom = words2(this.heard).length;
14494
- if (said && kind === "backchannel" && this.o.reactionGapMs && !this.bank?.backchannel.some((b) => norm2(b) === norm2(text))) this.lastReactAt = this.o.now();
14620
+ if (said && kind === "backchannel" && this.o.reactionGapMs && !this.bank?.backchannel.some((b) => norm3(b) === norm3(text))) this.lastReactAt = this.o.now();
14495
14621
  ev({ choice, kind, text, acted: said ? "spoken" : "skipped" });
14496
14622
  } finally {
14497
14623
  this.busy = false;
@@ -14515,8 +14641,8 @@ var SecondChannel = class {
14515
14641
  const raw = await withBudget(this.o.generate(ctx, ac.signal), this.o.generateBudgetMs, `${kind} generate`);
14516
14642
  if (raw === null) ac.abort();
14517
14643
  const text = cleanLine(raw ?? "", this.lang);
14518
- if (text && !this.recent.some((r) => norm2(r) === norm2(text))) return text;
14519
- if (text) log31.debug(`generated "${text}" repeats a recent line \u2014 canned`);
14644
+ if (text && !this.recent.some((r) => norm3(r) === norm3(text))) return text;
14645
+ if (text) log32.debug(`generated "${text}" repeats a recent line \u2014 canned`);
14520
14646
  }
14521
14647
  return this.pick(kind);
14522
14648
  }
@@ -14526,8 +14652,8 @@ var SecondChannel = class {
14526
14652
  const pool = this.bank?.[kind] ?? [];
14527
14653
  if (!pool.length) return "";
14528
14654
  const hist = this.picks.get(kind) ?? [];
14529
- const recent = new Set([...hist.slice(-this.o.bankExclude), ...this.recent].map(norm2));
14530
- const fresh = pool.filter((x) => !recent.has(norm2(x)));
14655
+ const recent = new Set([...hist.slice(-this.o.bankExclude), ...this.recent].map(norm3));
14656
+ const fresh = pool.filter((x) => !recent.has(norm3(x)));
14531
14657
  const from = fresh.length ? fresh : pool;
14532
14658
  const p = from[Math.floor(this.o.random() * from.length)];
14533
14659
  this.picks.set(kind, [...hist, p].slice(-this.o.bankExclude));
@@ -14538,7 +14664,7 @@ var SecondChannel = class {
14538
14664
  try {
14539
14665
  r = this.o.speak(text, kind);
14540
14666
  } catch (e) {
14541
- log31.warn(`speak threw \u2014 line dropped: ${e.message}`);
14667
+ log32.warn(`speak threw \u2014 line dropped: ${e.message}`);
14542
14668
  return false;
14543
14669
  }
14544
14670
  if (r === false) return false;
@@ -14550,7 +14676,7 @@ var SecondChannel = class {
14550
14676
  this.o.show(text, kind);
14551
14677
  const p = Promise.resolve(r).then(() => {
14552
14678
  }, (e) => {
14553
- log31.warn(`channel line playback failed: ${e?.message ?? e}`);
14679
+ log32.warn(`channel line playback failed: ${e?.message ?? e}`);
14554
14680
  }).finally(() => {
14555
14681
  if (this.inflight === p) this.inflight = null;
14556
14682
  });
@@ -14747,7 +14873,7 @@ function applyOverlay(cfg, file) {
14747
14873
  // cli/hooks-config.ts
14748
14874
  import { spawnSync as spawnSync4 } from "child_process";
14749
14875
  init_platform();
14750
- var log32 = forComponent("hooks");
14876
+ var log33 = forComponent("hooks");
14751
14877
  var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
14752
14878
  function ruleMatches(rule, toolName) {
14753
14879
  if (!rule.tool || rule.tool === "*") return true;
@@ -14766,7 +14892,7 @@ function runCmd(rule, env) {
14766
14892
  });
14767
14893
  return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
14768
14894
  } catch (e) {
14769
- log32.debug(`hook command failed: ${rule.command}`, e);
14895
+ log33.debug(`hook command failed: ${rule.command}`, e);
14770
14896
  return { code: 1, out: String(e?.message ?? e) };
14771
14897
  }
14772
14898
  }
@@ -14874,7 +15000,7 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as rea
14874
15000
  import { homedir as homedir6 } from "os";
14875
15001
  import { join as join11 } from "path";
14876
15002
  init_platform();
14877
- var log33 = forComponent("session");
15003
+ var log34 = forComponent("session");
14878
15004
  var globalDir = () => join11(homedir6(), ".agent", "sessions");
14879
15005
  function indexLink(target, link2) {
14880
15006
  try {
@@ -14937,7 +15063,7 @@ var SessionStore = class {
14937
15063
  }
14938
15064
  load(id) {
14939
15065
  if (!this.safeId(id)) {
14940
- log33.debug(`rejecting unsafe session id: ${id}`);
15066
+ log34.debug(`rejecting unsafe session id: ${id}`);
14941
15067
  return void 0;
14942
15068
  }
14943
15069
  const path = join11(this.dir, `${id}.json`);
@@ -14945,7 +15071,7 @@ var SessionStore = class {
14945
15071
  try {
14946
15072
  return JSON.parse(readFileSync8(path, "utf8"));
14947
15073
  } catch (e) {
14948
- log33.debug(`unreadable session ${id} \u2014 ignoring`, e);
15074
+ log34.debug(`unreadable session ${id} \u2014 ignoring`, e);
14949
15075
  return void 0;
14950
15076
  }
14951
15077
  }
@@ -14958,7 +15084,7 @@ var SessionStore = class {
14958
15084
  try {
14959
15085
  metas.push(JSON.parse(readFileSync8(join11(this.dir, f), "utf8")).meta);
14960
15086
  } catch (e) {
14961
- log33.debug(`skipping unreadable session file ${f}`, e);
15087
+ log34.debug(`skipping unreadable session file ${f}`, e);
14962
15088
  }
14963
15089
  }
14964
15090
  return metas.sort((a, b) => b.updated - a.updated);
@@ -15197,7 +15323,7 @@ import { promisify } from "util";
15197
15323
  import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync9 } from "fs";
15198
15324
  import { homedir as homedir7 } from "os";
15199
15325
  import { join as join12, resolve as resolve4, sep as sep3, parse as parsePath } from "path";
15200
- var log34 = forComponent("checkpoints");
15326
+ var log35 = forComponent("checkpoints");
15201
15327
  var exec = promisify(execFile4);
15202
15328
  var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
15203
15329
  function isBroadRoot(p) {
@@ -15243,7 +15369,7 @@ var ShadowRepo = class {
15243
15369
  writeFileSync6(join12(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
15244
15370
  this.ready = true;
15245
15371
  } catch (e) {
15246
- log34.debug(`git checkpoints unavailable for ${this.workTree}`, e);
15372
+ log35.debug(`git checkpoints unavailable for ${this.workTree}`, e);
15247
15373
  this.ready = false;
15248
15374
  }
15249
15375
  return this.ready;
@@ -15254,7 +15380,7 @@ var ShadowRepo = class {
15254
15380
  }
15255
15381
  async commit(label, forced = []) {
15256
15382
  await this.run("add", "-A");
15257
- for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log34.debug(`force-add failed: ${p}`, e));
15383
+ for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log35.debug(`force-add failed: ${p}`, e));
15258
15384
  await this.run("commit", "--allow-empty", "-q", "-m", label);
15259
15385
  }
15260
15386
  /** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
@@ -15262,8 +15388,8 @@ var ShadowRepo = class {
15262
15388
  * turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
15263
15389
  * point per turn, so the REPL's turn↔frame mapping stays intact. */
15264
15390
  async amendForced(paths) {
15265
- for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log34.debug(`force-capture failed: ${p}`, e));
15266
- await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log34.debug("amend failed", e));
15391
+ for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log35.debug(`force-capture failed: ${p}`, e));
15392
+ await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log35.debug("amend failed", e));
15267
15393
  }
15268
15394
  /** Commits on `ref`, oldest-first (canonical index space). */
15269
15395
  async log(ref) {
@@ -15323,7 +15449,7 @@ var ShadowRepo = class {
15323
15449
  await this.run("gc", "--auto", "-q").catch(() => {
15324
15450
  });
15325
15451
  } catch (e) {
15326
- log34.debug("checkpoint prune failed", e);
15452
+ log35.debug("checkpoint prune failed", e);
15327
15453
  }
15328
15454
  }
15329
15455
  };
@@ -15391,7 +15517,7 @@ var GitCheckpoints = class {
15391
15517
  use(sessionId) {
15392
15518
  if (sessionId === this.session) return;
15393
15519
  this.session = sessionId;
15394
- if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log34.debug("re-point failed", e));
15520
+ if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log35.debug("re-point failed", e));
15395
15521
  }
15396
15522
  async begin(label) {
15397
15523
  if (!await this.start()) return;
@@ -15411,7 +15537,7 @@ var GitCheckpoints = class {
15411
15537
  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
15412
15538
  `);
15413
15539
  } else {
15414
- log34.debug("checkpoint commit failed", e);
15540
+ log35.debug("checkpoint commit failed", e);
15415
15541
  survivors.push(this.repos[i]);
15416
15542
  survivorCaches.push(this.caches[i] ?? []);
15417
15543
  }
@@ -15446,7 +15572,7 @@ var GitCheckpoints = class {
15446
15572
  if (this.forced.has(abs)) continue;
15447
15573
  this.forced.add(abs);
15448
15574
  const i = this.repoIndexFor(abs);
15449
- if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log34.debug("amendForced failed", e));
15575
+ if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log35.debug("amendForced failed", e));
15450
15576
  }
15451
15577
  }
15452
15578
  };
@@ -17231,7 +17357,7 @@ import { spawnSync as spawnSync6 } from "child_process";
17231
17357
  import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
17232
17358
  import { homedir as homedir8 } from "os";
17233
17359
  import { join as join14 } from "path";
17234
- var log35 = forComponent("os-sched");
17360
+ var log36 = forComponent("os-sched");
17235
17361
  var OsScheduler = class {
17236
17362
  options;
17237
17363
  constructor(options) {
@@ -17293,7 +17419,7 @@ var OsScheduler = class {
17293
17419
  }
17294
17420
  }
17295
17421
  } catch (e) {
17296
- log35.debug(`cancel ${id}`, e);
17422
+ log36.debug(`cancel ${id}`, e);
17297
17423
  }
17298
17424
  for (const f of [`${id}.json`, `${id}.sh`]) {
17299
17425
  try {
@@ -17411,7 +17537,7 @@ import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unli
17411
17537
  import { homedir as homedir9 } from "os";
17412
17538
  import { join as join15 } from "path";
17413
17539
  init_platform();
17414
- var log36 = forComponent("remote-trigger");
17540
+ var log37 = forComponent("remote-trigger");
17415
17541
  var TRIGGER_DIR = () => join15(homedir9(), ".agent", "triggers");
17416
17542
  var sockPath = (sessionId, dir = TRIGGER_DIR()) => join15(dir, `${sessionId}.sock`);
17417
17543
  var TriggerServer = class {
@@ -17449,13 +17575,13 @@ var TriggerServer = class {
17449
17575
  conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
17450
17576
  }
17451
17577
  });
17452
- conn.on("error", (e) => log36.debug("trigger conn error", e));
17578
+ conn.on("error", (e) => log37.debug("trigger conn error", e));
17453
17579
  });
17454
- this.server.on("error", (e) => log36.debug("trigger server error", e));
17580
+ this.server.on("error", (e) => log37.debug("trigger server error", e));
17455
17581
  this.server.listen(p);
17456
17582
  this.path = p;
17457
17583
  } catch (e) {
17458
- log36.debug("trigger server unavailable", e);
17584
+ log37.debug("trigger server unavailable", e);
17459
17585
  }
17460
17586
  }
17461
17587
  /** Re-bind on /resume (the session id changed). */
@@ -17587,7 +17713,7 @@ var strike = C("9");
17587
17713
  var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
17588
17714
  var mdPalette = { bold, dim, cyan, italic, strike, green, link };
17589
17715
  var err = (s) => process.stderr.write(s);
17590
- var log37 = forComponent("cli");
17716
+ var log38 = forComponent("cli");
17591
17717
  var VERSION = (() => {
17592
17718
  try {
17593
17719
  return JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
@@ -18436,7 +18562,7 @@ function turnCost(model, usage) {
18436
18562
  const priced = model.startsWith("claude-code/") ? `anthropic/${resolveModelAlias(model.slice("claude-code/".length))}` : model;
18437
18563
  return costOf(getModelInfo(priced)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, priced);
18438
18564
  }
18439
- async function evaluateGoal(ai, condition, transcript, log38) {
18565
+ async function evaluateGoal(ai, condition, transcript, log39) {
18440
18566
  const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
18441
18567
  const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
18442
18568
  return text.slice(0, 600);
@@ -18456,7 +18582,7 @@ ${recent}` }
18456
18582
  const match = r.content.match(/\{[\s\S]*\}/);
18457
18583
  if (match) return JSON.parse(match[0]);
18458
18584
  } catch (e) {
18459
- log38(dim(` (goal evaluator error: ${e?.message ?? e})
18585
+ log39(dim(` (goal evaluator error: ${e?.message ?? e})
18460
18586
  `));
18461
18587
  }
18462
18588
  return { met: false, reason: "evaluation unclear" };
@@ -18682,7 +18808,7 @@ function mcpAgentTools(mounted, opts) {
18682
18808
  return tools;
18683
18809
  }
18684
18810
  async function closeMcp(mounted) {
18685
- await Promise.all(mounted.map((m) => m.client.close().catch((e) => log37.debug("mcp close failed", e))));
18811
+ await Promise.all(mounted.map((m) => m.client.close().catch((e) => log38.debug("mcp close failed", e))));
18686
18812
  }
18687
18813
  var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
18688
18814
  function mentionRefs(line) {
@@ -18741,7 +18867,7 @@ async function expandMentions(fs, line) {
18741
18867
  if (loaded.includes(ref) || missing.includes(ref)) continue;
18742
18868
  if (ref.includes(":") && mcpMentionResolver) {
18743
18869
  const body = await mcpMentionResolver(ref).catch((e) => {
18744
- log37.debug("mcp mention resolve failed", e);
18870
+ log38.debug("mcp mention resolve failed", e);
18745
18871
  return null;
18746
18872
  });
18747
18873
  if (body != null) {
@@ -18832,7 +18958,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
18832
18958
  try {
18833
18959
  store.save(session);
18834
18960
  } catch (ex) {
18835
- log37.debug("mid-turn session flush failed", ex);
18961
+ log38.debug("mid-turn session flush failed", ex);
18836
18962
  }
18837
18963
  }
18838
18964
  return origNotify(e);
@@ -18996,14 +19122,14 @@ var isCancelTeardown = (e) => {
18996
19122
  function installCancelGuards(mounted) {
18997
19123
  process.on("unhandledRejection", (e) => {
18998
19124
  if (isCancelTeardown(e)) {
18999
- log37.debug("suppressed unhandledRejection (cursor stream cancel)", e);
19125
+ log38.debug("suppressed unhandledRejection (cursor stream cancel)", e);
19000
19126
  return;
19001
19127
  }
19002
- log37.error("unhandledRejection", e);
19128
+ log38.error("unhandledRejection", e);
19003
19129
  });
19004
19130
  process.on("uncaughtException", (e) => {
19005
19131
  if (isCancelTeardown(e)) {
19006
- log37.debug("suppressed uncaughtException (cursor stream cancel)", e);
19132
+ log38.debug("suppressed uncaughtException (cursor stream cancel)", e);
19007
19133
  return;
19008
19134
  }
19009
19135
  console.error(e);
@@ -19067,6 +19193,7 @@ async function repl(args, ai, cfg, cwd) {
19067
19193
  const decider = duplex ? leakDecider() : void 0;
19068
19194
  const instinct = decider && process.env.VOICE_INSTINCT === "1" ? new Instinct({
19069
19195
  decider,
19196
+ overlapModel: overlapModelFor(ai),
19070
19197
  context: () => ({ agentSaid: dx?.recentExcerpt(1).replace(/^\w+: /, ""), recent: dx?.recentExcerpt(4) }),
19071
19198
  onDecision: ({ kind, ...d }) => {
19072
19199
  voiceDiag?.push({ t: performance.now(), kind: `instinct_${kind}`, ...d });
@@ -19432,7 +19559,7 @@ async function repl(args, ai, cfg, cwd) {
19432
19559
  try {
19433
19560
  mkdirSync12(dir, { recursive: true });
19434
19561
  } catch (e) {
19435
- log37.debug("paste image dir create failed", e);
19562
+ log38.debug("paste image dir create failed", e);
19436
19563
  }
19437
19564
  const img = grabClipboardImage(dir, String(Date.now()));
19438
19565
  return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
@@ -19545,7 +19672,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
19545
19672
  mkdirSync12(join16(cwd, ".agent"), { recursive: true });
19546
19673
  appendFileSync(histPath, line + "\n");
19547
19674
  } catch (e) {
19548
- log37.debug("history write failed", e);
19675
+ log38.debug("history write failed", e);
19549
19676
  }
19550
19677
  };
19551
19678
  const ago = (t) => {
@@ -19617,7 +19744,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
19617
19744
  try {
19618
19745
  store.save(session);
19619
19746
  } catch (e) {
19620
- log37.debug("session save after rewind failed", e);
19747
+ log38.debug("session save after rewind failed", e);
19621
19748
  }
19622
19749
  err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
19623
19750
  `));
@@ -19652,7 +19779,7 @@ ${task}`;
19652
19779
  bangContext.length = 0;
19653
19780
  }
19654
19781
  const delta = await refreshCatalogs().catch((e) => {
19655
- log37.debug("catalog refresh failed", e);
19782
+ log38.debug("catalog refresh failed", e);
19656
19783
  return "";
19657
19784
  });
19658
19785
  if (delta) {
@@ -19915,7 +20042,7 @@ ${task}`;
19915
20042
  desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
19916
20043
  run: async () => {
19917
20044
  await refreshCatalogs().catch((e) => {
19918
- log37.debug("catalog refresh failed", e);
20045
+ log38.debug("catalog refresh failed", e);
19919
20046
  });
19920
20047
  face.reprepare();
19921
20048
  err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
@@ -20418,7 +20545,7 @@ ${task}`;
20418
20545
  try {
20419
20546
  for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
20420
20547
  } catch (e) {
20421
- log37.debug(`loadAgents(${d}) failed`, e);
20548
+ log38.debug(`loadAgents(${d}) failed`, e);
20422
20549
  }
20423
20550
  }
20424
20551
  if (!seen.size) {
@@ -20506,7 +20633,7 @@ ${task}`;
20506
20633
  }
20507
20634
  if (idx >= 0) {
20508
20635
  const old = mounted.splice(idx, 1)[0];
20509
- await old.client.close().catch((e) => log37.debug("mcp close failed", e));
20636
+ await old.client.close().catch((e) => log38.debug("mcp close failed", e));
20510
20637
  }
20511
20638
  try {
20512
20639
  const m = await mountMcpServer(name, conf);
@@ -20534,7 +20661,7 @@ ${task}`;
20534
20661
  }
20535
20662
  const m = mounted.splice(idx, 1)[0];
20536
20663
  remountMcpTools();
20537
- await m.client.close().catch((e) => log37.debug("mcp close failed", e));
20664
+ await m.client.close().catch((e) => log38.debug("mcp close failed", e));
20538
20665
  err(dim(` removed "${name}"
20539
20666
  `));
20540
20667
  return;
@@ -20737,7 +20864,7 @@ ${task}`;
20737
20864
  try {
20738
20865
  return readdirSync4(join16(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
20739
20866
  } catch (e) {
20740
- log37.debug("completion readdir failed", absDir, e);
20867
+ log38.debug("completion readdir failed", absDir, e);
20741
20868
  return null;
20742
20869
  }
20743
20870
  };
@@ -20874,7 +21001,7 @@ ${out}
20874
21001
  return;
20875
21002
  }
20876
21003
  await refreshCatalogs().catch((e) => {
20877
- log37.debug("catalog refresh failed", e);
21004
+ log38.debug("catalog refresh failed", e);
20878
21005
  });
20879
21006
  const sk = skills.find((x) => x.name === name);
20880
21007
  if (sk) {
@@ -20993,7 +21120,7 @@ ${out}
20993
21120
  if (delivery || cut) dx?.parkCutDelivery(delivery, !!cut, heard);
20994
21121
  const cutReply = cut && cut.full.length - cut.heard.length > 40 ? cut : null;
20995
21122
  const noteP = replaces ? replaces.note : !cutReply ? Promise.resolve("") : (instinct ? instinct.bargeIntent(text).catch((e) => {
20996
- log37.warn(`barge intent failed \u2014 model decides: ${e.message}`);
21123
+ log38.warn(`barge intent failed \u2014 model decides: ${e.message}`);
20997
21124
  return void 0;
20998
21125
  }) : Promise.resolve(void 0)).then(async (intent) => interruptionNote(cutReply, intent) + ackedNote(await instinct?.ackFor(text)));
20999
21126
  const cutIn = replaces ? replaces.cutIn : interjectNote;
@@ -21013,6 +21140,10 @@ ${out}
21013
21140
  }).finally(() => editorRef?.redrawNow());
21014
21141
  }
21015
21142
  });
21143
+ if (!instinct && decider && args.duplexV2 && softBarge(args.channelMode)) {
21144
+ const v = voiceIO;
21145
+ bindOverlapInstinct(v, new Instinct({ decider, overlapModel: overlapModelFor(ai) }), () => voiceIO === v);
21146
+ }
21016
21147
  if (instinct) {
21017
21148
  const v = voiceIO, o = v.options;
21018
21149
  const { onPartial } = o;
@@ -21023,7 +21154,7 @@ ${out}
21023
21154
  if (listening()) instinct.hear(text);
21024
21155
  onPartial(text);
21025
21156
  };
21026
- bindOverlapInstinct(v, instinct, () => voiceIO === v);
21157
+ if (!args.duplexV2 || softBarge(args.channelMode)) bindOverlapInstinct(v, instinct, () => voiceIO === v);
21027
21158
  bindAckInstinct(v, instinct);
21028
21159
  o.onUtterance = (text, meta) => {
21029
21160
  instinct.reset();
@@ -21362,10 +21493,17 @@ function makeSecondChannel(ai, reflexModel, mode = "normal", opts) {
21362
21493
  // Anticipated backchannels: on in chatty mode (its preset), else off until the offline replay proves them (tests/voice-bc-anticipate-eval.ts)
21363
21494
  plan: (c, s) => planner.plan(c, s),
21364
21495
  ...process.env.AGENTX_CHANNEL_ANTICIPATE === "1" ? { anticipate: true } : {},
21365
- 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`),
21496
+ 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`),
21366
21497
  ...opts
21367
21498
  });
21368
21499
  }
21500
+ function overlapResponder(ai, mode, out) {
21501
+ const judge = softBarge(mode) ? new OverlapJudge({ model: overlapModelFor(ai) }) : void 0;
21502
+ return (m) => {
21503
+ const text = String(m.text ?? "");
21504
+ 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 }));
21505
+ };
21506
+ }
21369
21507
  async function duplexServe(args, ai, cfg, cwd) {
21370
21508
  const out = (o) => process.stdout.write(JSON.stringify(o) + "\n");
21371
21509
  const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join16(cwd, ".agent", "mcp-auth.json") }));
@@ -21424,10 +21562,11 @@ async function duplexServe(args, ai, cfg, cwd) {
21424
21562
  try {
21425
21563
  store.save(session);
21426
21564
  } catch (e) {
21427
- log37.warn("duplex session save failed", e);
21565
+ log38.warn("duplex session save failed", e);
21428
21566
  }
21429
21567
  };
21430
- out({ type: "ready", sessionId: session.meta.id, reflexModel: dx.options.reflexModel, actModel: dx.options.actModel, ...notes.length ? { notes } : {} });
21568
+ const overlap2 = overlapResponder(ai, args.duplexV2 ? args.channelMode : void 0, out);
21569
+ 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 } : {} });
21431
21570
  const done = new Promise((resolveDone) => {
21432
21571
  const rl = createInterface({ input: process.stdin });
21433
21572
  rl.on("line", (line) => {
@@ -21442,7 +21581,8 @@ async function duplexServe(args, ai, cfg, cwd) {
21442
21581
  dx.userActive();
21443
21582
  activeTurn?.abort();
21444
21583
  dx.parkInFlightDeliveries();
21445
- } else if (sc && channelInput(sc, m)) {
21584
+ } else if (m.type === "overlap") overlap2(m);
21585
+ else if (sc && channelInput(sc, m)) {
21446
21586
  if (m.type === "interim" && String(m.text ?? "").trim()) dx.userActive();
21447
21587
  } else if (m.type === "cancel") out({ type: "task", kind: "cancel_result", id: m.task, message: dx.cancelTask(String(m.task)) });
21448
21588
  else if (m.type === "exit") rl.close();
@@ -21462,7 +21602,7 @@ async function duplexServe(args, ai, cfg, cwd) {
21462
21602
  done2();
21463
21603
  out({ type: "turn_done", id, finishReason: res?.finishReason ?? "error", ...why ? { error: why } : {} });
21464
21604
  }).catch((e) => {
21465
- log37.error("duplex turn failed", e);
21605
+ log38.error("duplex turn failed", e);
21466
21606
  done2();
21467
21607
  out({ type: "turn_done", id, finishReason: "error", error: String(e?.message ?? e) });
21468
21608
  });
@@ -21481,7 +21621,8 @@ async function channelServe(args, ai, cfg) {
21481
21621
  const { reflexModel, notes } = duplexModels({ voiceModel: args.voiceModel, reflexModel: cfg.reflexModel, thinkModel: false, actModel: String(args.model ?? cfg.model ?? ""), apiKeys: { ...cfg.apiKeys, ...apiKeysFromEnv() } });
21482
21622
  const model = reflexModel ?? new DuplexAgentOptions().reflexModel;
21483
21623
  const sc = makeSecondChannel(ai, model, args.channelMode, { show: (text, kind) => out({ type: "channel", kind, text }) });
21484
- out({ type: "ready", model, channelMode: args.channelMode, ...notes.length ? { notes } : {} });
21624
+ const overlap2 = overlapResponder(ai, args.channelMode, out);
21625
+ out({ type: "ready", model, channelMode: args.channelMode, softBarge: softBarge(args.channelMode), ...notes.length ? { notes } : {} });
21485
21626
  const rl = createInterface({ input: process.stdin });
21486
21627
  rl.on("line", (line) => {
21487
21628
  let m;
@@ -21494,6 +21635,7 @@ async function channelServe(args, ai, cfg) {
21494
21635
  if (m.type === "user" && typeof m.text === "string") sc.onUserTurn(m.text);
21495
21636
  else if (m.type === "text" && typeof m.text === "string") sc.onMainTokens(m.text);
21496
21637
  else if (m.type === "barge") sc.onBargeIn();
21638
+ else if (m.type === "overlap") overlap2(m);
21497
21639
  else if (m.type === "exit") rl.close();
21498
21640
  });
21499
21641
  await new Promise((r) => rl.on("close", () => r()));