@livx.cc/agentx 0.99.85 → 0.99.87

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 norm2 = (s) => s.trim();
712
+ const norm3 = (s) => s.trim();
713
713
  const cl = content.split("\n");
714
- const ol = oldStr.split("\n").map(norm2);
714
+ const ol = oldStr.split("\n").map(norm3);
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 (norm2(cl[i2 + j]) !== ol[j]) {
721
+ for (let j = 0; j < ol.length; j++) if (norm3(cl[i2 + j]) !== ol[j]) {
722
722
  ok = false;
723
723
  break;
724
724
  }
@@ -3431,7 +3431,7 @@ function copyTextToClipboard(text, platform2 = process.platform) {
3431
3431
  }
3432
3432
 
3433
3433
  // cli/cli.ts
3434
- import { join as join15, resolve as resolve5, basename as basename4, extname, dirname as dirname6 } from "path";
3434
+ import { join as join16, resolve as resolve5, basename as basename4, extname, dirname as dirname6 } from "path";
3435
3435
  import { AIClient, DecideClient, listModels, listProviders, getProviderFromModel, getModelInfo, resolveModel, isModelSupported, disposeCursorSessions, disposeClaudeCodeSessions } from "ai.libx.js";
3436
3436
 
3437
3437
  // src/ccBridge.ts
@@ -6046,14 +6046,14 @@ function summarize(messages, focus) {
6046
6046
  if (lastAssistant) lines.push(`last assistant: ${lastAssistant}`);
6047
6047
  if (carried.length) lines.push("earlier recap:", ...carried.map((l) => ` ${l}`));
6048
6048
  if (focus?.trim()) {
6049
- const words2 = focus.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
6049
+ const words3 = focus.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
6050
6050
  const kept = [];
6051
6051
  const ordered = [...messages.filter((m) => !isSyntheticSummary(m)), ...messages.filter(isSyntheticSummary)];
6052
6052
  outer: for (const msg of ordered) {
6053
6053
  if (msg.role === "tool") continue;
6054
6054
  for (const line of contentText(msg.content).split("\n")) {
6055
6055
  const l = line.toLowerCase();
6056
- if (line.trim() && words2.some((w) => l.includes(w))) {
6056
+ if (line.trim() && words3.some((w) => l.includes(w))) {
6057
6057
  kept.push(line.trim().slice(0, 200));
6058
6058
  if (kept.length >= 12) break outer;
6059
6059
  }
@@ -7101,13 +7101,13 @@ function isTranscriptComplete(text) {
7101
7101
  const trimmed = text.trim();
7102
7102
  if (!trimmed) return false;
7103
7103
  if (/[.?!]$/.test(trimmed)) return true;
7104
- const words2 = trimmed.toLowerCase().split(/\s+/);
7105
- const lastWord = words2[words2.length - 1];
7104
+ const words3 = trimmed.toLowerCase().split(/\s+/);
7105
+ const lastWord = words3[words3.length - 1];
7106
7106
  if (COMPLETE_ENDINGS.has(lastWord)) return true;
7107
7107
  if (INCOMPLETE_ENDINGS.has(lastWord)) return false;
7108
7108
  const lower = trimmed.toLowerCase();
7109
7109
  for (const marker of CONTINUATION_MARKERS) if (lower.endsWith(marker)) return false;
7110
- if (words2.length <= 2) return false;
7110
+ if (words3.length <= 2) return false;
7111
7111
  return true;
7112
7112
  }
7113
7113
  var MAX_HOLD_MS = 2e3;
@@ -7508,13 +7508,13 @@ var VoiceEngine = class _VoiceEngine {
7508
7508
  };
7509
7509
  if (this.options.revealMode === "word" || this.options.heardTimestamps) {
7510
7510
  this.tts.wantTimestamps = true;
7511
- this.tts.onTimestamps = (words2, start) => {
7511
+ this.tts.onTimestamps = (words3, start) => {
7512
7512
  if (this.interrupted) return;
7513
7513
  start.forEach((s, i) => {
7514
- if (this.wordAnchorPending) this.wordPending.push([words2[i] ?? "", s]);
7514
+ if (this.wordAnchorPending) this.wordPending.push([words3[i] ?? "", s]);
7515
7515
  else {
7516
7516
  this.wordStarts.push(this.wordOrigin + s);
7517
- this.wordTexts.push(words2[i] ?? "");
7517
+ this.wordTexts.push(words3[i] ?? "");
7518
7518
  }
7519
7519
  });
7520
7520
  };
@@ -7662,9 +7662,9 @@ var VoiceEngine = class _VoiceEngine {
7662
7662
  this.ackProbe = null;
7663
7663
  this.adaptiveAckFired = false;
7664
7664
  const lead = m ? m[1] : bare;
7665
- const norm2 = lead.replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
7665
+ const norm3 = lead.replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
7666
7666
  const same = this.ackPickedFired ? bare.match(_VoiceEngine.leadingPhrase(this.lastAck)) : null;
7667
- if (_VoiceEngine.ACK_LEADING.test(norm2)) {
7667
+ if (_VoiceEngine.ACK_LEADING.test(norm3)) {
7668
7668
  const rest = m ? m[2] : "";
7669
7669
  this.diag("reflex_ack_suppressed", { lead: lead.trim() });
7670
7670
  if (!rest.trim()) return "";
@@ -7750,8 +7750,8 @@ var VoiceEngine = class _VoiceEngine {
7750
7750
  while (n < this.wordStarts.length && this.wordStarts[n] <= playedSec) n++;
7751
7751
  if (n !== this.revealedN) {
7752
7752
  this.revealedN = n;
7753
- const words2 = this.revealText.trim().split(/\s+/).filter(Boolean);
7754
- this.options.onReveal(words2.slice(0, n).join(" "));
7753
+ const words3 = this.revealText.trim().split(/\s+/).filter(Boolean);
7754
+ this.options.onReveal(words3.slice(0, n).join(" "));
7755
7755
  }
7756
7756
  this.revealPoll = this.clock.setTimeout(tick, 60);
7757
7757
  };
@@ -7778,8 +7778,8 @@ var VoiceEngine = class _VoiceEngine {
7778
7778
  this.adaptiveAckFired = false;
7779
7779
  this.ackProbe = null;
7780
7780
  if (heldProbe) {
7781
- const norm2 = heldProbe.replace(/\[emotion[^\]]*\]/gi, "").replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
7782
- if (_VoiceEngine.ACK_LEADING.test(norm2)) this.diag("reflex_ack_suppressed", { lead: heldProbe.trim() });
7781
+ const norm3 = heldProbe.replace(/\[emotion[^\]]*\]/gi, "").replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
7782
+ if (_VoiceEngine.ACK_LEADING.test(norm3)) this.diag("reflex_ack_suppressed", { lead: heldProbe.trim() });
7783
7783
  else this.speakDelta(heldProbe);
7784
7784
  }
7785
7785
  if (!this.speaking) return;
@@ -8317,14 +8317,14 @@ var VoiceEngine = class _VoiceEngine {
8317
8317
  if (!prev) return next;
8318
8318
  if (!next) return prev;
8319
8319
  const pw = prev.split(/\s+/), nw = next.split(/\s+/);
8320
- const norm2 = _VoiceEngine.normWord;
8320
+ const norm3 = _VoiceEngine.normWord;
8321
8321
  if (pw.length >= 2 && pw.length <= nw.length) {
8322
8322
  let pfx = true;
8323
- for (let i = 0; i < pw.length - 1; i++) if (norm2(pw[i]) !== norm2(nw[i])) {
8323
+ for (let i = 0; i < pw.length - 1; i++) if (norm3(pw[i]) !== norm3(nw[i])) {
8324
8324
  pfx = false;
8325
8325
  break;
8326
8326
  }
8327
- if (pfx && norm2(nw[pw.length - 1]).startsWith(norm2(pw[pw.length - 1]))) return next;
8327
+ if (pfx && norm3(nw[pw.length - 1]).startsWith(norm3(pw[pw.length - 1]))) return next;
8328
8328
  }
8329
8329
  if (_VoiceEngine.restates(prev, next)) {
8330
8330
  this.diag("merge_restated", { dropped: prev.slice(0, 60) });
@@ -8333,7 +8333,7 @@ var VoiceEngine = class _VoiceEngine {
8333
8333
  const max = Math.min(pw.length, nw.length);
8334
8334
  for (let k = max; k > 0; k--) {
8335
8335
  let match = true;
8336
- for (let i = 0; i < k; i++) if (norm2(pw[pw.length - k + i]) !== norm2(nw[i])) {
8336
+ for (let i = 0; i < k; i++) if (norm3(pw[pw.length - k + i]) !== norm3(nw[i])) {
8337
8337
  match = false;
8338
8338
  break;
8339
8339
  }
@@ -8385,11 +8385,11 @@ var VoiceEngine = class _VoiceEngine {
8385
8385
  * race), never worse. Fillers ("mhm") deliberately don't count as replies — see speakFiller. */
8386
8386
  refinalizes(next) {
8387
8387
  const pw = this.lastDispatchWords, nw = next.trim().split(/\s+/);
8388
- const norm2 = _VoiceEngine.normWord;
8388
+ const norm3 = _VoiceEngine.normWord;
8389
8389
  if (pw.length < 3 || nw.length < pw.length) return false;
8390
8390
  if (this.clock.now() - this.lastDispatchAt >= _VoiceEngine.DUP_FINAL_MS) return false;
8391
- for (let i = 0; i < pw.length - 1; i++) if (norm2(pw[i]) !== norm2(nw[i])) return false;
8392
- const pLast = norm2(pw[pw.length - 1]), nSame = norm2(nw[pw.length - 1]);
8391
+ for (let i = 0; i < pw.length - 1; i++) if (norm3(pw[i]) !== norm3(nw[i])) return false;
8392
+ const pLast = norm3(pw[pw.length - 1]), nSame = norm3(nw[pw.length - 1]);
8393
8393
  if (!nSame.startsWith(pLast)) return false;
8394
8394
  return nw.length > pw.length || nSame.length > pLast.length;
8395
8395
  }
@@ -9285,6 +9285,17 @@ var ACK_BANK = {
9285
9285
  steer: ["Ah, okay, switching\u2014", "Ah, got it, so\u2014", "Oh, gotcha, sure\u2014"],
9286
9286
  resume: ["Right, so\u2014", "Anyway, so\u2014", "So, as I was saying\u2014"]
9287
9287
  };
9288
+ var CHANNEL_ACTIONS = {
9289
+ silence: 'the default \u2014 nothing to add. ALWAYS for a greeting, thanks, a yes/no, or any short complete turn the real reply will answer instantly ("hey", "how are you?", "thanks"); also mid-word or mid-clause',
9290
+ backchannel: 'they are mid-way through a longer story or explanation (several clauses) and just closed a clause: a tiny "mm-hm"/"right" shows you follow, without taking the turn',
9291
+ filler: 'they paused mid-thought, searching for words, and a soft "hmm" keeps the line warm',
9292
+ bridge: 'they finished a substantial request or hard question and the real answer takes a moment: a short "let me check\u2014" lead-in'
9293
+ };
9294
+ var CHANNEL_BANK = {
9295
+ backchannel: ["Mm-hm.", "Right.", "Mm, yeah.", "Uh-huh.", "I see.", "Mm."],
9296
+ filler: ["Hmm\u2026", "Mm\u2026", "Hm, mm\u2026", "Ahh\u2026"],
9297
+ bridge: ["Let me check\u2014", "One sec, looking\u2014", "Hm, let me see\u2014", "Okay, give me a moment\u2014", "Let me think on that\u2014"]
9298
+ };
9288
9299
  var INTENT_ACK_OVER = {
9289
9300
  stop: Object.keys(ACK_SITUATIONS).filter((s) => s !== "silent"),
9290
9301
  steer: ["command"],
@@ -9502,6 +9513,17 @@ var Instinct = class {
9502
9513
  if (pick?.text !== text.trim()) return null;
9503
9514
  return Promise.race([pick.p.then((p) => p ? pick.spoken : null), pick.spoken]);
9504
9515
  }
9516
+ /** Duplex v2 second channel: which (if any) listener line fits THIS instant — one Choice over CHANNEL_ACTIONS.
9517
+ * `heard` is the micro-context tail only (never history). Never rejects: a failed call → null (the rule decides). */
9518
+ channel(heard, state) {
9519
+ return this.ask(
9520
+ "channel",
9521
+ heard,
9522
+ { heard_so_far: heard, ...state },
9523
+ FRAME + "You are the quick side-voice of the conversation, NOT the one who answers. Pick the tiny sound a person would make right now, or none. Silence is the default: most moments call for nothing.",
9524
+ CHANNEL_ACTIONS
9525
+ );
9526
+ }
9505
9527
  /** A random phrase from `pool`, never the one said last (no back-to-back repeat across keys). */
9506
9528
  rotate(pool) {
9507
9529
  const fresh = pool.filter((x) => x !== this.lastAckSaid);
@@ -13022,8 +13044,8 @@ var CutInAbsorber = class {
13022
13044
  const cut = this.cut || (meta?.supersedes !== void 0 && meta.supersedes === this.absorbed?.text ? this.absorbed.cut : "");
13023
13045
  this.cut = "";
13024
13046
  this.absorbed = void 0;
13025
- const words2 = (s) => s.trim().split(/\s+/).filter(Boolean).length;
13026
- if (!cut || words2(text) - words2(cut) >= 3) return false;
13047
+ const words3 = (s) => s.trim().split(/\s+/).filter(Boolean).length;
13048
+ if (!cut || words3(text) - words3(cut) >= 3) return false;
13027
13049
  this.absorbed = { text, cut };
13028
13050
  return true;
13029
13051
  }
@@ -13082,6 +13104,79 @@ function bindAckInstinct(v, instinct) {
13082
13104
  onDiag(ev);
13083
13105
  };
13084
13106
  }
13107
+ function bindSecondChannel(v, sc, live) {
13108
+ const o = v.options;
13109
+ const { onPartial, onUtterance, onState, onBargeIn, onBackchannel } = o;
13110
+ const { show } = sc.options;
13111
+ let own = false;
13112
+ let blip = "";
13113
+ o.onBackchannel = (p) => {
13114
+ blip = p;
13115
+ onBackchannel(p);
13116
+ };
13117
+ const take = v.takeInterruptedReply.bind(v);
13118
+ v.takeInterruptedReply = () => {
13119
+ const r = take();
13120
+ if (!r || !sc.spoke(r.full)) return r;
13121
+ return r.queued.length ? { ...r, full: "", heard: "" } : null;
13122
+ };
13123
+ o.onPartial = (t) => {
13124
+ if (live() && !v.audible) sc.onInterim(t);
13125
+ onPartial(t);
13126
+ };
13127
+ o.onUtterance = ((t, meta) => {
13128
+ if (live() && t.trim()) sc.onUserTurn(t);
13129
+ onUtterance(t, meta);
13130
+ });
13131
+ o.onState = (s) => {
13132
+ if (live() && !own) {
13133
+ if (s === "speaking") sc.onAgentState("first_audio");
13134
+ else if (s === "listening") sc.onAgentState("idle");
13135
+ }
13136
+ onState(s);
13137
+ };
13138
+ o.onBargeIn = (phase) => {
13139
+ sc.onBargeIn();
13140
+ onBargeIn(phase);
13141
+ };
13142
+ Object.assign(sc.options, {
13143
+ aec: v.usingAec,
13144
+ speak: (text, kind) => {
13145
+ if (!live() || v.audible) return false;
13146
+ if (kind === "backchannel") {
13147
+ if (sc.language !== "en") return false;
13148
+ blip = "";
13149
+ v.fireBackchannel();
13150
+ return;
13151
+ }
13152
+ own = true;
13153
+ v.speakFiller(text);
13154
+ return v.awaitIdle().finally(() => {
13155
+ own = false;
13156
+ });
13157
+ },
13158
+ show: (text, kind) => show(kind === "backchannel" && blip ? blip : text, kind)
13159
+ });
13160
+ }
13161
+ function channelInput(sc, m) {
13162
+ if (m.type === "interim") {
13163
+ if (typeof m.aec === "boolean") sc.options.aec = m.aec;
13164
+ if (typeof m.lang === "string") sc.setLanguage(m.lang, true);
13165
+ if (typeof m.text === "string") sc.onInterim(m.text);
13166
+ return true;
13167
+ }
13168
+ if (m.type === "pause") {
13169
+ sc.onPause(Number(m.ms) || 0);
13170
+ return true;
13171
+ }
13172
+ if (m.type === "agent_state") {
13173
+ if (typeof m.state === "string" && MAIN_STATES.has(m.state)) sc.onAgentState(m.state);
13174
+ else log27.warn(`agent_state: unknown state ${JSON.stringify(m.state)} \u2014 ignored`);
13175
+ return true;
13176
+ }
13177
+ return false;
13178
+ }
13179
+ var MAIN_STATES = /* @__PURE__ */ new Set(["idle", "queued", "thinking", "streaming_text", "first_audio", "speaking"]);
13085
13180
  var VoiceIOOptions = class extends VoiceEngineOptions {
13086
13181
  sonioxApiKey = process.env.SONIOX_API_KEY ?? "";
13087
13182
  cartesiaApiKey = process.env.CARTESIA_API_KEY ?? "";
@@ -13251,16 +13346,581 @@ var JsonlDiagSink = class {
13251
13346
  }
13252
13347
  };
13253
13348
 
13349
+ // src/voice/secondChannel.ts
13350
+ init_logging();
13351
+
13352
+ // src/voice/channelLang.ts
13353
+ init_logging();
13354
+ import { mkdir, readFile, writeFile } from "fs/promises";
13355
+ import { join as join8 } from "path";
13356
+ var log29 = forComponent("ChannelBanks");
13357
+ var LINE_KINDS = ["backchannel", "filler", "bridge"];
13358
+ var BANK_KEYS = [...LINE_KINDS, "phatic"];
13359
+ var EN_PHATIC = [
13360
+ "hey",
13361
+ "hi",
13362
+ "hello",
13363
+ "yo",
13364
+ "hiya",
13365
+ "morning",
13366
+ "good morning",
13367
+ "good evening",
13368
+ "good afternoon",
13369
+ "thanks",
13370
+ "thank you",
13371
+ "ok",
13372
+ "okay",
13373
+ "yes",
13374
+ "yeah",
13375
+ "yep",
13376
+ "no",
13377
+ "nope",
13378
+ "sure",
13379
+ "bye",
13380
+ "goodbye",
13381
+ "how are you",
13382
+ "what's up",
13383
+ "whats up",
13384
+ "can you hear me"
13385
+ ];
13386
+ var EN_BANK = { ...CHANNEL_BANK, phatic: EN_PHATIC };
13387
+ var primaryLang = (tag) => tag.trim().toLowerCase().split(/[-_]/)[0];
13388
+ function isPhatic(text, bank) {
13389
+ const t = norm2(text);
13390
+ return !!t && !!bank?.phatic?.some((p) => {
13391
+ const q2 = norm2(p);
13392
+ return !!q2 && (t === q2 || t.startsWith(q2 + " "));
13393
+ });
13394
+ }
13395
+ var SCRIPTS = [
13396
+ [new RegExp("\\p{Script=Hebrew}", "gu"), "he"],
13397
+ [new RegExp("\\p{Script=Arabic}", "gu"), "ar"],
13398
+ [new RegExp("\\p{Script=Cyrillic}", "gu"), "ru"],
13399
+ [new RegExp("\\p{Script=Greek}", "gu"), "el"],
13400
+ [/[\p{Script=Hiragana}\p{Script=Katakana}]/gu, "ja"],
13401
+ [new RegExp("\\p{Script=Hangul}", "gu"), "ko"],
13402
+ [new RegExp("\\p{Script=Han}", "gu"), "zh"],
13403
+ [new RegExp("\\p{Script=Thai}", "gu"), "th"],
13404
+ [new RegExp("\\p{Script=Devanagari}", "gu"), "hi"],
13405
+ [new RegExp("\\p{Script=Latin}", "gu"), "latin"]
13406
+ ];
13407
+ var STOP3 = Object.fromEntries(Object.entries({
13408
+ en: "hi hello hey yes thanks let see check one sec just give right well look the and is are you i it to of that what this was have with for not but my me",
13409
+ es: "hola s\xED gracias bueno a me te lo se le del al el la los las que es y de en un una por para con no pero mi yo qu\xE9 est\xE1",
13410
+ fr: "bonjour salut oui merci le la les est et de des un une que je tu il pas pour avec mais mon qui",
13411
+ de: "hallo ja danke der die das und ist ich du nicht ein eine mit zu was aber mein wie auch",
13412
+ it: "ciao grazie s\xEC a mi ti si il lo la gli che \xE8 e di un una non per con ma mio io sono",
13413
+ pt: "ol\xE1 oi sim obrigado obrigada o a os as que \xE9 e de em um uma n\xE3o para com mas meu eu voc\xEA",
13414
+ nl: "hoi dank de het een en is ik je niet van dat met voor maar mijn wat"
13415
+ }).map(([k, v]) => [k, new Set(v.split(" "))]));
13416
+ var scriptOf = (w) => SCRIPTS.find(([re]) => {
13417
+ re.lastIndex = 0;
13418
+ return re.test(w);
13419
+ })?.[1] ?? null;
13420
+ function detectLanguage(text) {
13421
+ const ws = text.toLowerCase().match(new RegExp("\\p{L}+", "gu")) ?? [];
13422
+ const n = /* @__PURE__ */ new Map();
13423
+ for (const w of ws) {
13424
+ const s = scriptOf(w);
13425
+ if (s) n.set(s, (n.get(s) ?? 0) + 1);
13426
+ }
13427
+ const [best, c] = [...n].sort((a2, b2) => b2[1] - a2[1])[0] ?? [null, 0];
13428
+ if (best !== "latin") return c >= 1 && (best === "zh" || best === "ja" || c >= 2 || ws.length === 1) ? best : null;
13429
+ const score = Object.entries(STOP3).map(([l2, s]) => [l2, ws.filter((w) => s.has(w)).length]).sort((a2, b2) => b2[1] - a2[1]);
13430
+ const [[l, a], [, b]] = score;
13431
+ return a > b && (a >= 2 || a === 1 && ws.length <= 3) ? l : null;
13432
+ }
13433
+ var NAMES = new Intl.DisplayNames(["en"], { type: "language" });
13434
+ var languageName = (lang) => {
13435
+ try {
13436
+ return NAMES.of(lang) ?? lang;
13437
+ } catch {
13438
+ return lang;
13439
+ }
13440
+ };
13441
+ function fitsLanguage(line, lang) {
13442
+ const own = SCRIPTS.find(([, l]) => l === lang)?.[0];
13443
+ if (own) return (line.match(own)?.length ?? 0) > 0 && !new RegExp("\\p{Script=Latin}{2,}", "u").test(line);
13444
+ if (/[^\p{Script=Latin}\p{M}\p{P}\p{Z}\p{N}\p{S}]/u.test(line)) return false;
13445
+ const mine = STOP3[lang];
13446
+ const ws = line.toLowerCase().match(new RegExp("\\p{L}+", "gu")) ?? [];
13447
+ return !ws.some((w) => !mine?.has(w) && Object.entries(STOP3).some(([l, s]) => l !== lang && s.has(w)));
13448
+ }
13449
+ var norm2 = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}' ]/gu, "").replace(/\s+/g, " ").trim();
13450
+ function cleanLine(s, lang, maxWords = 6) {
13451
+ const raw = s.trim().split("\n")[0];
13452
+ const l = raw.replace(/^[-*•\d.)\s]+/, "").replace(/^["'“”«»]+|["'“”«»]+$/g, "").replace(new RegExp("^\\p{L}+:\\s*", "u"), "").trim();
13453
+ if (!l || isLeakSuspect(raw) || isLeakSuspect(l)) {
13454
+ if (l) log29.debug(`line "${raw}" reads as meta \u2014 rejected`);
13455
+ return "";
13456
+ }
13457
+ if (lang && !fitsLanguage(l, lang)) {
13458
+ log29.debug(`line "${l}" is not ${lang} \u2014 rejected`);
13459
+ return "";
13460
+ }
13461
+ return l.split(/\s+/).length > maxWords || l.length > 60 ? "" : l;
13462
+ }
13463
+ function validateBank(raw, lang, minPerKind = 3) {
13464
+ if (!raw || typeof raw !== "object") return null;
13465
+ const out = {};
13466
+ for (const k of BANK_KEYS) {
13467
+ const v = raw[k];
13468
+ const seen = /* @__PURE__ */ new Set();
13469
+ out[k] = (Array.isArray(v) ? v : []).filter((x) => typeof x === "string").map((x) => cleanLine(x, lang)).filter((x) => x && !seen.has(norm2(x)) && seen.add(norm2(x)));
13470
+ if (out[k].length < minPerKind) {
13471
+ log29.warn(`bank ${lang}: only ${out[k].length} valid ${k} lines \u2014 rejected`);
13472
+ return null;
13473
+ }
13474
+ }
13475
+ return out;
13476
+ }
13477
+ var KIND_HINT = {
13478
+ backchannel: 'tiny listener cues while they keep talking (like "mm-hm", "right") \u2014 never takes the turn',
13479
+ filler: 'soft thinking sounds at their pause (like "hmm\u2026")',
13480
+ bridge: 'quick lead-ins before the real answer while it is prepared (like "let me check\u2014")',
13481
+ phatic: 'greetings, thanks and yes/no openers a person starts a turn with (like "hi", "thanks", "how are you")'
13482
+ };
13483
+ function fastBankGenerator(ai, model, perKind = 12) {
13484
+ return async (lang, avoid, signal) => {
13485
+ const name = languageName(lang);
13486
+ const prompt = `Write short spoken side-sounds for a live voice conversation, in ${name} (natural, casual, how native ${name} speakers really talk \u2014 not translated English). ${perKind} varied lines per kind, 1-4 words each \u2014 short, idiomatic, grammatical everyday phrases a native would actually say:
13487
+ ${BANK_KEYS.map((k) => `- ${k}: ${KIND_HINT[k]}`).join("\n")}
13488
+ ` + (avoid.length ? `Do not repeat these existing ones: ${avoid.slice(0, 60).join(" | ")}
13489
+ ` : "") + `Reply with JSON only: {"backchannel":[...],"filler":[...],"bridge":[...],"phatic":[...]}. Every line in ${name}; no explanations.`;
13490
+ const reasoning = /gpt-oss/.test(model) ? { providerOptions: { reasoning_effort: "low" } } : {};
13491
+ const r = await ai.chat({ model, messages: [{ role: "user", content: prompt }], stream: false, signal, maxTokens: 2e3, ...reasoning });
13492
+ const m = (r.content ?? "").match(/\{[\s\S]*\}/);
13493
+ if (!m) throw new Error(`no JSON in bank reply: ${(r.content ?? "").slice(0, 80)}`);
13494
+ return JSON.parse(m[0]);
13495
+ };
13496
+ }
13497
+ var ChannelBanksOptions = class {
13498
+ /** Raw bank for a language (fastBankGenerator). Absent → English fallback only. */
13499
+ generate;
13500
+ /** Disk cache dir (one `<lang>.json` per language). Absent → in-memory only. */
13501
+ cacheDir;
13502
+ /** A cached bank older than this is still used, and topped up by one background call. */
13503
+ refreshMs = 24 * 36e5;
13504
+ /** Lines kept per kind (newest win) — the cap on growth. */
13505
+ maxPerKind = 40;
13506
+ timeoutMs = 2e4;
13507
+ /** After a failed cold generation, the next attempt waits this long. */
13508
+ retryMs = 6e4;
13509
+ /** Cold-start/offline banks (English only by default). */
13510
+ fallback = { en: EN_BANK };
13511
+ now = () => Date.now();
13512
+ };
13513
+ var ChannelBanks = class {
13514
+ constructor(options) {
13515
+ this.options = options;
13516
+ this.options = { ...new ChannelBanksOptions(), ...options };
13517
+ }
13518
+ options;
13519
+ banks = /* @__PURE__ */ new Map();
13520
+ pending = /* @__PURE__ */ new Map();
13521
+ get o() {
13522
+ return this.options;
13523
+ }
13524
+ /** The bank to speak from for `lang` now — generated/cached, else the fallback, else null (= stay silent). */
13525
+ get(lang) {
13526
+ return this.banks.get(lang) ?? this.o.fallback[lang] ?? null;
13527
+ }
13528
+ /** Load (cache) and/or generate `lang`'s bank in the background. Never throws, never blocks the caller. */
13529
+ ensure(lang) {
13530
+ if (!this.pending.has(lang)) this.pending.set(lang, this.load(lang));
13531
+ return this.pending.get(lang);
13532
+ }
13533
+ file(lang) {
13534
+ return this.o.cacheDir ? join8(this.o.cacheDir, `${lang.replace(/[^\w-]/g, "")}.json`) : null;
13535
+ }
13536
+ async load(lang) {
13537
+ const f = this.file(lang);
13538
+ let cached = null;
13539
+ if (f) cached = await readFile(f, "utf8").then((s) => JSON.parse(s)).catch((e) => {
13540
+ if (e.code !== "ENOENT") log29.warn(`bank cache ${f} unreadable \u2014 regenerating: ${e.message}`);
13541
+ return null;
13542
+ });
13543
+ const valid = cached && validateBank(cached.bank, lang, 1);
13544
+ if (valid) {
13545
+ this.banks.set(lang, valid);
13546
+ log29.debug(`bank ${lang}: cache hit (${BANK_KEYS.map((k) => valid[k].length).join("/")})`);
13547
+ }
13548
+ if (valid && this.o.now() - cached.at < this.o.refreshMs) return;
13549
+ await this.generate(lang, valid);
13550
+ }
13551
+ async generate(lang, have) {
13552
+ if (!this.o.generate) return;
13553
+ const ac = new AbortController();
13554
+ const t = setTimeout(() => ac.abort(), this.o.timeoutMs);
13555
+ const t0 = this.o.now();
13556
+ try {
13557
+ const fresh = validateBank(await this.o.generate(lang, have ? BANK_KEYS.flatMap((k) => have[k]) : [], ac.signal), lang);
13558
+ if (!fresh) return;
13559
+ const bank = {};
13560
+ for (const k of BANK_KEYS) {
13561
+ const seen = /* @__PURE__ */ new Set();
13562
+ bank[k] = [...fresh[k], ...have?.[k] ?? []].filter((x) => !seen.has(norm2(x)) && seen.add(norm2(x))).slice(0, this.o.maxPerKind);
13563
+ }
13564
+ this.banks.set(lang, bank);
13565
+ log29.info(`bank ${lang}: ${have ? "refreshed" : "generated"} (${BANK_KEYS.map((k) => bank[k].length).join("/")}) in ${Math.round(this.o.now() - t0)}ms`);
13566
+ const f = this.file(lang);
13567
+ if (f) await mkdir(this.o.cacheDir, { recursive: true }).then(() => writeFile(f, JSON.stringify({ at: this.o.now(), bank }))).catch((e) => log29.warn(`bank cache write ${f} failed: ${e.message}`));
13568
+ } catch (e) {
13569
+ log29.warn(`bank ${lang}: generation failed \u2014 ${have ? "keeping the cached bank" : lang in this.o.fallback ? "fallback bank" : "silent"}: ${e.message}`);
13570
+ } finally {
13571
+ clearTimeout(t);
13572
+ if (!this.banks.has(lang)) setTimeout(() => this.pending.delete(lang), this.o.retryMs).unref?.();
13573
+ }
13574
+ }
13575
+ };
13576
+
13577
+ // src/voice/secondChannel.ts
13578
+ var log30 = forComponent("SecondChannel");
13579
+ var SecondChannelOptions = class {
13580
+ /** Refiner (Jev/fast LLM): the choice over CHANNEL_ACTIONS, or null on failure. Absent → the rule alone decides. */
13581
+ decide;
13582
+ /** Fast-model line writer (see fastLineGenerator). Absent or late → the canned bank. */
13583
+ generate;
13584
+ /** Per-language banks (ChannelBanks). Absent → the hardcoded English bank, and silence in any other language. */
13585
+ banks;
13586
+ /** The host's language (BCP-47; pinned — detection never overrides it). Absent → unknown until the user's words
13587
+ * tell it confidently; the channel is silent until then (never English by default). */
13588
+ lang;
13589
+ /** Kinds worth generating (content-conditioned); the rest are canned sounds. */
13590
+ generateFor = ["backchannel", "bridge"];
13591
+ /** Host TTS on the second channel; resolves when the line finished playing. `false` = the host could not play it. */
13592
+ speak = () => {
13593
+ };
13594
+ /** Host UI (caption / muted line) — never history. */
13595
+ show = () => {
13596
+ };
13597
+ /** Stop the in-flight line's audio now (barge-in only). */
13598
+ cancel = () => {
13599
+ };
13600
+ onEvent = () => {
13601
+ };
13602
+ /** Echo cancellation on: backchannels may play while the user speaks. Off (half-duplex) → fillers/bridges in silences only. */
13603
+ aec = false;
13604
+ decideEveryWords = 3;
13605
+ decisionBudgetMs = 300;
13606
+ generateBudgetMs = 500;
13607
+ minGapMs = 2500;
13608
+ /** Backchannel after backchannel needs this gap (v1 Instinct's cadence, ≤12/min). */
13609
+ backchannelGapMs = 5e3;
13610
+ /** A decision whose partial grew by this many words while in flight is stale → dropped. */
13611
+ staleWords = 3;
13612
+ microWords = 30;
13613
+ avoidLines = 4;
13614
+ /** Canned picks exclude the last N picks of the same kind. */
13615
+ bankExclude = 2;
13616
+ backchannelMinWords = 8;
13617
+ /** No new interim for this long → a pause (hosts may also call onPause). */
13618
+ fillerPauseMs = 700;
13619
+ fillerMinWords = 4;
13620
+ bridgeMinWords = 5;
13621
+ now = () => performance.now();
13622
+ random = Math.random;
13623
+ };
13624
+ var words2 = (s) => s.trim() ? s.trim().split(/\s+/) : [];
13625
+ function withBudget(p, ms, what) {
13626
+ let t;
13627
+ const late = new Promise((r) => {
13628
+ t = setTimeout(() => {
13629
+ log30.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
13630
+ r(null);
13631
+ }, ms);
13632
+ });
13633
+ return Promise.race([p.catch((e) => {
13634
+ log30.warn(`${what} failed \u2014 falling back: ${e?.message ?? e}`);
13635
+ return null;
13636
+ }), late]).finally(() => clearTimeout(t));
13637
+ }
13638
+ var SecondChannel = class {
13639
+ constructor(options) {
13640
+ this.options = options;
13641
+ this.options = { ...new SecondChannelOptions(), ...options };
13642
+ this.startedAt = this.o.now();
13643
+ if (this.o.lang) this.setLanguage(this.o.lang, true);
13644
+ }
13645
+ options;
13646
+ heard = "";
13647
+ userSpeaking = false;
13648
+ main = "idle";
13649
+ lead = "";
13650
+ gen = 0;
13651
+ // bumped by barge-in / a new user turn → in-flight decisions drop
13652
+ decidedAt = 0;
13653
+ // word count at the last decision
13654
+ busy = false;
13655
+ pauseTimer = null;
13656
+ lastAt = -Infinity;
13657
+ lastKind = null;
13658
+ live = null;
13659
+ // our last line, until channel-1 audio takes the floor
13660
+ cutLine = null;
13661
+ // the line the last barge actually cut (spoke())
13662
+ bridged = false;
13663
+ // one bridge per main turn
13664
+ recent = [];
13665
+ picks = /* @__PURE__ */ new Map();
13666
+ inflight = null;
13667
+ startedAt;
13668
+ lang = null;
13669
+ pinned = false;
13670
+ /** The language lines are spoken in (detected from the user's words, or set by the host). */
13671
+ get language() {
13672
+ return this.lang;
13673
+ }
13674
+ /** Switch language: its bank starts loading in the background. `host` = the host's STT-reported language — it pins
13675
+ * the language (later detection never overrides it; only another host call does). Tags normalize to 'he-IL' → 'he'. */
13676
+ setLanguage(tag, host = false) {
13677
+ if (!tag || this.pinned && !host) return;
13678
+ if (host) this.pinned = true;
13679
+ const lang = primaryLang(tag);
13680
+ if (!lang || lang === this.lang) return;
13681
+ log30.info(`language ${this.lang} \u2192 ${lang}`);
13682
+ this.lang = lang;
13683
+ this.recent = [];
13684
+ this.picks.clear();
13685
+ void this.o.banks?.ensure(lang);
13686
+ }
13687
+ detect(text) {
13688
+ if (this.pinned) return;
13689
+ const l = detectLanguage(text);
13690
+ if (l) this.setLanguage(l);
13691
+ }
13692
+ get bank() {
13693
+ return this.lang ? this.o.banks ? this.o.banks.get(this.lang) : this.lang === "en" ? EN_BANK : null : null;
13694
+ }
13695
+ get o() {
13696
+ return this.options;
13697
+ }
13698
+ /** The user's live partial for the current turn (the whole partial, not a delta). */
13699
+ onInterim(text) {
13700
+ const t = text.trim();
13701
+ if (!t || t === this.heard) return;
13702
+ if (this.main !== "idle") this.onAgentState("idle");
13703
+ this.heard = t;
13704
+ this.detect(t);
13705
+ this.userSpeaking = true;
13706
+ if (this.pauseTimer) clearTimeout(this.pauseTimer);
13707
+ this.pauseTimer = setTimeout(() => {
13708
+ this.pauseTimer = null;
13709
+ this.onPause(this.o.fillerPauseMs);
13710
+ }, this.o.fillerPauseMs);
13711
+ if (words2(t).length - this.decidedAt >= this.o.decideEveryWords) void this.consider("interim");
13712
+ }
13713
+ /** The user went quiet for `ms` (host VAD/STT pause, or the internal interim timer). */
13714
+ onPause(ms) {
13715
+ if (this.pauseTimer) {
13716
+ clearTimeout(this.pauseTimer);
13717
+ this.pauseTimer = null;
13718
+ }
13719
+ this.userSpeaking = false;
13720
+ void this.consider("pause", ms);
13721
+ }
13722
+ /** Channel-1 lifecycle. `queued` = the user's turn was dispatched to the main agent. */
13723
+ onAgentState(s) {
13724
+ if (s === this.main) return;
13725
+ this.main = s;
13726
+ if (s === "first_audio" || s === "speaking") this.live = null;
13727
+ if (s === "idle") {
13728
+ this.heard = "";
13729
+ this.decidedAt = 0;
13730
+ this.lead = "";
13731
+ this.bridged = false;
13732
+ return;
13733
+ }
13734
+ if (s === "queued") {
13735
+ this.gen++;
13736
+ this.userSpeaking = false;
13737
+ this.lead = "";
13738
+ this.bridged = false;
13739
+ if (this.pauseTimer) clearTimeout(this.pauseTimer);
13740
+ this.pauseTimer = null;
13741
+ }
13742
+ if (s === "queued" || s === "thinking" || s === "streaming_text") void this.consider("working");
13743
+ }
13744
+ /** The user's final line was dispatched to the main agent (its words are the bridge's micro-context). */
13745
+ onUserTurn(text) {
13746
+ this.heard = text.trim();
13747
+ this.detect(this.heard);
13748
+ this.main = "idle";
13749
+ this.onAgentState("queued");
13750
+ }
13751
+ /** The main answer's streamed text (before its audio): a bridge can lead into it. */
13752
+ onMainTokens(text) {
13753
+ if (this.lead.length < 120) this.lead += text;
13754
+ if (this.main === "queued" || this.main === "thinking") this.onAgentState("streaming_text");
13755
+ }
13756
+ /** The user barged in: cut the in-flight line now and drop every pending decision. */
13757
+ onBargeIn() {
13758
+ this.gen++;
13759
+ this.cutLine = this.inflight || this.live ? this.live : null;
13760
+ if (this.inflight) {
13761
+ this.inflight = null;
13762
+ try {
13763
+ this.o.cancel();
13764
+ } catch (e) {
13765
+ log30.warn(`cancel threw: ${e.message}`);
13766
+ }
13767
+ }
13768
+ }
13769
+ /** Is `text` the line the last barge-in cut (ours, still holding the floor)? Hosts strip it from the cut reply — it is
13770
+ * not the agent's speech. By identity, not by text: a real reply that merely repeats a recent line is kept. Consumed once. */
13771
+ spoke(text) {
13772
+ const c = this.cutLine;
13773
+ this.cutLine = null;
13774
+ return !!c && c === text.trim();
13775
+ }
13776
+ /** Resolves when the in-flight line finished playing (immediately when none) — hosts enqueue channel-1 audio behind it. */
13777
+ drain() {
13778
+ return this.inflight ?? Promise.resolve();
13779
+ }
13780
+ get audioLocked() {
13781
+ return this.main === "first_audio" || this.main === "speaking";
13782
+ }
13783
+ get working() {
13784
+ return this.main === "queued" || this.main === "thinking" || this.main === "streaming_text";
13785
+ }
13786
+ tail() {
13787
+ return words2(this.heard).slice(-this.o.microWords).join(" ");
13788
+ }
13789
+ /** Hard gates every line passes, whoever proposed it. */
13790
+ allowed(k) {
13791
+ if (this.audioLocked || this.inflight || this.o.now() - this.lastAt < this.o.minGapMs || k === this.lastKind && (k !== "backchannel" || this.o.now() - this.lastAt < this.o.backchannelGapMs)) return false;
13792
+ if (k === "backchannel") return this.o.aec && this.userSpeaking && this.main === "idle";
13793
+ if (k === "filler") return !this.userSpeaking && this.main === "idle";
13794
+ return this.working && !this.bridged;
13795
+ }
13796
+ /** 0ms rule: the candidate line for this trigger, or silence. */
13797
+ rule(trigger, pauseMs = 0) {
13798
+ const w = words2(this.heard);
13799
+ if (!this.lang) return "silence";
13800
+ if (w.length <= 3 || w.length <= 6 && isPhatic(this.heard, this.bank)) return "silence";
13801
+ const k = trigger === "interim" ? w.length >= this.o.backchannelMinWords && /[,.;:!?…]$/.test(this.heard) ? "backchannel" : null : trigger === "pause" ? pauseMs >= this.o.fillerPauseMs && w.length >= this.o.fillerMinWords ? "filler" : null : w.length >= this.o.bridgeMinWords ? "bridge" : null;
13802
+ return k && this.allowed(k) ? k : "silence";
13803
+ }
13804
+ async consider(trigger, pauseMs) {
13805
+ if (this.busy) return;
13806
+ const n0 = words2(this.heard).length;
13807
+ this.decidedAt = n0;
13808
+ const rule = this.rule(trigger, pauseMs);
13809
+ if (rule === "silence") return;
13810
+ this.busy = true;
13811
+ const gen = this.gen, t0 = this.o.now();
13812
+ const ev = (e) => this.o.onEvent({ trigger, rule, ms: Math.round(this.o.now() - t0), ...e });
13813
+ const stale = () => gen !== this.gen || words2(this.heard).length - n0 >= this.o.staleWords;
13814
+ try {
13815
+ let kind = rule;
13816
+ let choice;
13817
+ if (this.o.decide) {
13818
+ const d = await withBudget(this.o.decide(this.tail(), this.state(pauseMs)), this.o.decisionBudgetMs, "channel decision");
13819
+ choice = d?.choice;
13820
+ if (choice && choice in CHANNEL_ACTIONS) kind = choice;
13821
+ }
13822
+ if (kind === "silence") return ev({ choice, acted: "silence" });
13823
+ if (stale()) return ev({ choice, kind, acted: "stale" });
13824
+ if (!this.allowed(kind)) return ev({ choice, kind, acted: "gated" });
13825
+ const text = await this.line(kind);
13826
+ if (!text) return ev({ choice, kind, acted: "no_bank" });
13827
+ if (stale()) return ev({ choice, kind, text, acted: "stale" });
13828
+ if (!this.allowed(kind)) return ev({ choice, kind, text, acted: "gated" });
13829
+ ev({ choice, kind, text, acted: this.say(kind, text) ? "spoken" : "skipped" });
13830
+ } finally {
13831
+ this.busy = false;
13832
+ }
13833
+ }
13834
+ state(pauseMs) {
13835
+ const now4 = this.o.now();
13836
+ return {
13837
+ user_speaking: this.userSpeaking,
13838
+ pause_ms: pauseMs ?? null,
13839
+ agent_state: this.main,
13840
+ last_line: this.recent.at(-1) ?? null,
13841
+ ms_since_last_line: Number.isFinite(this.lastAt) ? Math.round(now4 - this.lastAt) : null,
13842
+ call_ms: Math.round(now4 - this.startedAt)
13843
+ };
13844
+ }
13845
+ async line(kind) {
13846
+ if (this.o.generate && this.o.generateFor.includes(kind)) {
13847
+ const ac = new AbortController();
13848
+ const ctx = { kind, heard: this.tail(), state: this.main, avoid: [...this.recent], lang: this.lang, ...this.lead ? { lead: this.lead } : {} };
13849
+ const raw = await withBudget(this.o.generate(ctx, ac.signal), this.o.generateBudgetMs, `${kind} generate`);
13850
+ if (raw === null) ac.abort();
13851
+ const text = cleanLine(raw ?? "", this.lang);
13852
+ if (text && !this.recent.some((r) => norm2(r) === norm2(text))) return text;
13853
+ if (text) log30.debug(`generated "${text}" repeats a recent line \u2014 canned`);
13854
+ }
13855
+ return this.pick(kind);
13856
+ }
13857
+ /** Canned rotation in the user's language: never one of the last `bankExclude` picks of this kind, nor a recent
13858
+ * line. '' = no bank for this language yet (silence). */
13859
+ pick(kind) {
13860
+ const pool = this.bank?.[kind] ?? [];
13861
+ if (!pool.length) return "";
13862
+ const hist = this.picks.get(kind) ?? [];
13863
+ const recent = new Set([...hist.slice(-this.o.bankExclude), ...this.recent].map(norm2));
13864
+ const fresh = pool.filter((x) => !recent.has(norm2(x)));
13865
+ const from = fresh.length ? fresh : pool;
13866
+ const p = from[Math.floor(this.o.random() * from.length)];
13867
+ this.picks.set(kind, [...hist, p].slice(-this.o.bankExclude));
13868
+ return p;
13869
+ }
13870
+ say(kind, text) {
13871
+ let r;
13872
+ try {
13873
+ r = this.o.speak(text, kind);
13874
+ } catch (e) {
13875
+ log30.warn(`speak threw \u2014 line dropped: ${e.message}`);
13876
+ return false;
13877
+ }
13878
+ if (r === false) return false;
13879
+ this.lastAt = this.o.now();
13880
+ this.lastKind = kind;
13881
+ if (kind === "bridge") this.bridged = true;
13882
+ this.recent = [...this.recent, text].slice(-this.o.avoidLines);
13883
+ this.live = text.trim();
13884
+ this.o.show(text, kind);
13885
+ const p = Promise.resolve(r).then(() => {
13886
+ }, (e) => {
13887
+ log30.warn(`channel line playback failed: ${e?.message ?? e}`);
13888
+ }).finally(() => {
13889
+ if (this.inflight === p) this.inflight = null;
13890
+ });
13891
+ this.inflight = p;
13892
+ return true;
13893
+ }
13894
+ };
13895
+ var KIND_HINT2 = {
13896
+ backchannel: "a tiny listener cue while they keep talking \u2014 shows you follow THEIR point, does not take the turn",
13897
+ filler: "a soft thinking sound at their pause",
13898
+ bridge: "a quick lead-in before the real answer, while it is being prepared"
13899
+ };
13900
+ function fastLineGenerator(ai, model) {
13901
+ return async (c, signal) => {
13902
+ const prompt = `You voice the tiny side-sounds of a live spoken conversation. Write ONE line of 1-5 words: ${KIND_HINT2[c.kind]}.
13903
+ The user just said: "${c.heard}"
13904
+ The assistant is: ${c.state}.
13905
+ ` + (c.lead ? `The real answer begins: "${c.lead.trim()}" \u2014 lead naturally into it; do not repeat it.
13906
+ ` : "") + (c.avoid.length ? `Do not reuse any of: ${c.avoid.map((a) => `"${a}"`).join(", ")}.
13907
+ ` : "") + `React to their actual words, casually, in ${languageName(c.lang)}. Output only the line.`;
13908
+ const reasoning = /gpt-oss/.test(model) ? { providerOptions: { reasoning_effort: "low" } } : {};
13909
+ const r = await ai.chat({ model, messages: [{ role: "user", content: prompt }], stream: false, signal, maxTokens: 150, ...reasoning });
13910
+ return r.content ?? "";
13911
+ };
13912
+ }
13913
+
13254
13914
  // cli/config.ts
13255
13915
  import { homedir as homedir5 } from "os";
13256
13916
  import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
13257
- import { join as join9 } from "path";
13917
+ import { join as join10 } from "path";
13258
13918
  import { pathToFileURL } from "url";
13259
13919
 
13260
13920
  // cli/permissions.ts
13261
13921
  import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
13262
13922
  import { homedir as homedir4 } from "os";
13263
- import { join as join8 } from "path";
13923
+ import { join as join9 } from "path";
13264
13924
  var RULE_RE = /^(\w[\w*-]*)(?:\((.+)\))?$/;
13265
13925
  function parseOne(raw, decision) {
13266
13926
  const m = RULE_RE.exec(raw.trim());
@@ -13282,13 +13942,13 @@ function parsePermRules(perms, explicit = false) {
13282
13942
  function describeRule(r) {
13283
13943
  return `${r.decision.padEnd(5)} ${r.tool ?? "*"}${r.pathGlob ? `(${r.pathGlob})` : ""}`;
13284
13944
  }
13285
- var PERM_FILE = (cwd) => join8(cwd, ".agent", "permissions.json");
13945
+ var PERM_FILE = (cwd) => join9(cwd, ".agent", "permissions.json");
13286
13946
  function loadPersistedRules(cwd) {
13287
13947
  const j = readJsonFile(PERM_FILE(cwd), null);
13288
13948
  return j ? { allow: j.allow ?? [], ask: j.ask ?? [], deny: j.deny ?? [] } : {};
13289
13949
  }
13290
13950
  function loadClaudeSettings(cwd, home = homedir4()) {
13291
- const files = [join8(home, ".claude", "settings.json"), join8(cwd, ".claude", "settings.json"), join8(cwd, ".claude", "settings.local.json")];
13951
+ const files = [join9(home, ".claude", "settings.json"), join9(cwd, ".claude", "settings.json"), join9(cwd, ".claude", "settings.local.json")];
13292
13952
  let out = {};
13293
13953
  for (const p of files) {
13294
13954
  const perms = readJsonFile(p, null)?.permissions;
@@ -13301,7 +13961,7 @@ function persistRule(cwd, decision, ruleStr) {
13301
13961
  const list = cur[decision] ??= [];
13302
13962
  if (!list.includes(ruleStr)) list.push(ruleStr);
13303
13963
  try {
13304
- mkdirSync7(join8(cwd, ".agent"), { recursive: true });
13964
+ mkdirSync7(join9(cwd, ".agent"), { recursive: true });
13305
13965
  writeFileSync4(PERM_FILE(cwd), JSON.stringify(cur, null, 2) + "\n");
13306
13966
  } catch {
13307
13967
  }
@@ -13315,7 +13975,7 @@ function mergePerms(a, b) {
13315
13975
  }
13316
13976
  return Object.keys(out).length ? out : void 0;
13317
13977
  }
13318
- var TRUST_FILE = join8(homedir4(), ".agent", "trusted.json");
13978
+ var TRUST_FILE = join9(homedir4(), ".agent", "trusted.json");
13319
13979
  function isTrusted(cwd, file = TRUST_FILE) {
13320
13980
  const list = readJsonFile(file, []);
13321
13981
  return Array.isArray(list) && list.includes(cwd);
@@ -13325,7 +13985,7 @@ function trustDir(cwd, file = TRUST_FILE) {
13325
13985
  if (!Array.isArray(list)) list = [];
13326
13986
  if (!list.includes(cwd)) list.push(cwd);
13327
13987
  try {
13328
- mkdirSync7(join8(file, ".."), { recursive: true });
13988
+ mkdirSync7(join9(file, ".."), { recursive: true });
13329
13989
  writeFileSync4(file, JSON.stringify(list, null, 2) + "\n");
13330
13990
  } catch {
13331
13991
  }
@@ -13335,7 +13995,7 @@ function trustDir(cwd, file = TRUST_FILE) {
13335
13995
  var FILES = ["config.ts", "config.js", "config.mjs", "config.json"];
13336
13996
  async function loadFrom(dir) {
13337
13997
  for (const f of FILES) {
13338
- const p = join9(dir, ".agent", f);
13998
+ const p = join10(dir, ".agent", f);
13339
13999
  if (!existsSync5(p)) continue;
13340
14000
  try {
13341
14001
  const mod = await import(pathToFileURL(p).href, f.endsWith(".json") ? { with: { type: "json" } } : void 0);
@@ -13348,7 +14008,7 @@ async function loadFrom(dir) {
13348
14008
  return {};
13349
14009
  }
13350
14010
  function loadSettings(dir) {
13351
- const p = join9(dir, ".agent", "settings.json");
14011
+ const p = join10(dir, ".agent", "settings.json");
13352
14012
  if (!existsSync5(p)) return {};
13353
14013
  try {
13354
14014
  const raw = JSON.parse(readFileSync7(p, "utf8"));
@@ -13397,7 +14057,7 @@ function applyOverlay(cfg, file) {
13397
14057
  // cli/hooks-config.ts
13398
14058
  import { spawnSync as spawnSync4 } from "child_process";
13399
14059
  init_platform();
13400
- var log29 = forComponent("hooks");
14060
+ var log31 = forComponent("hooks");
13401
14061
  var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
13402
14062
  function ruleMatches(rule, toolName) {
13403
14063
  if (!rule.tool || rule.tool === "*") return true;
@@ -13416,7 +14076,7 @@ function runCmd(rule, env) {
13416
14076
  });
13417
14077
  return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
13418
14078
  } catch (e) {
13419
- log29.debug(`hook command failed: ${rule.command}`, e);
14079
+ log31.debug(`hook command failed: ${rule.command}`, e);
13420
14080
  return { code: 1, out: String(e?.message ?? e) };
13421
14081
  }
13422
14082
  }
@@ -13522,10 +14182,10 @@ function formatDiff(ops, opts = {}) {
13522
14182
  // cli/session.ts
13523
14183
  import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync5, readdirSync, renameSync, symlinkSync as symlinkSync2, unlinkSync, readlinkSync, statSync as statSync7 } from "fs";
13524
14184
  import { homedir as homedir6 } from "os";
13525
- import { join as join10 } from "path";
14185
+ import { join as join11 } from "path";
13526
14186
  init_platform();
13527
- var log30 = forComponent("session");
13528
- var globalDir = () => join10(homedir6(), ".agent", "sessions");
14187
+ var log32 = forComponent("session");
14188
+ var globalDir = () => join11(homedir6(), ".agent", "sessions");
13529
14189
  function indexLink(target, link2) {
13530
14190
  try {
13531
14191
  symlinkSync2(target, link2);
@@ -13547,7 +14207,7 @@ function readIndexLink(link2) {
13547
14207
  var SessionStore = class {
13548
14208
  dir;
13549
14209
  constructor(cwd) {
13550
- this.dir = join10(cwd, ".agent", "sessions");
14210
+ this.dir = join11(cwd, ".agent", "sessions");
13551
14211
  }
13552
14212
  /** Sortable, human-readable id: `YYYYMMDD-HHMMSS-<folder>`. */
13553
14213
  newId(now4 = Date.now(), cwd) {
@@ -13555,10 +14215,10 @@ var SessionStore = class {
13555
14215
  const p = (n, w = 2) => String(n).padStart(w, "0");
13556
14216
  const slug2 = pathTail(cwd ?? process.cwd())?.replace(/[^A-Za-z0-9_-]/g, "") || "session";
13557
14217
  let id = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${slug2}`;
13558
- if (existsSync6(this.dir) && existsSync6(join10(this.dir, `${id}.json`))) {
14218
+ if (existsSync6(this.dir) && existsSync6(join11(this.dir, `${id}.json`))) {
13559
14219
  for (let i = 2; i <= 99; i++) {
13560
14220
  const c = `${id}-${i}`;
13561
- if (!existsSync6(join10(this.dir, `${c}.json`))) {
14221
+ if (!existsSync6(join11(this.dir, `${c}.json`))) {
13562
14222
  id = c;
13563
14223
  break;
13564
14224
  }
@@ -13573,29 +14233,29 @@ var SessionStore = class {
13573
14233
  save(data) {
13574
14234
  if (!this.safeId(data.meta.id)) throw new Error(`unsafe session id: ${data.meta.id}`);
13575
14235
  if (!existsSync6(this.dir)) mkdirSync8(this.dir, { recursive: true });
13576
- const path = join10(this.dir, `${data.meta.id}.json`);
14236
+ const path = join11(this.dir, `${data.meta.id}.json`);
13577
14237
  const tmp = `${path}.${process.pid}.tmp`;
13578
14238
  writeFileSync5(tmp, JSON.stringify(data));
13579
14239
  renameSync(tmp, path);
13580
14240
  try {
13581
14241
  const gd = globalDir();
13582
14242
  if (!existsSync6(gd)) mkdirSync8(gd, { recursive: true });
13583
- const link2 = join10(gd, `${data.meta.id}.json`);
14243
+ const link2 = join11(gd, `${data.meta.id}.json`);
13584
14244
  if (!existsSync6(link2)) indexLink(path, link2);
13585
14245
  } catch {
13586
14246
  }
13587
14247
  }
13588
14248
  load(id) {
13589
14249
  if (!this.safeId(id)) {
13590
- log30.debug(`rejecting unsafe session id: ${id}`);
14250
+ log32.debug(`rejecting unsafe session id: ${id}`);
13591
14251
  return void 0;
13592
14252
  }
13593
- const path = join10(this.dir, `${id}.json`);
14253
+ const path = join11(this.dir, `${id}.json`);
13594
14254
  if (!existsSync6(path)) return void 0;
13595
14255
  try {
13596
14256
  return JSON.parse(readFileSync8(path, "utf8"));
13597
14257
  } catch (e) {
13598
- log30.debug(`unreadable session ${id} \u2014 ignoring`, e);
14258
+ log32.debug(`unreadable session ${id} \u2014 ignoring`, e);
13599
14259
  return void 0;
13600
14260
  }
13601
14261
  }
@@ -13606,9 +14266,9 @@ var SessionStore = class {
13606
14266
  for (const f of readdirSync(this.dir)) {
13607
14267
  if (!f.endsWith(".json")) continue;
13608
14268
  try {
13609
- metas.push(JSON.parse(readFileSync8(join10(this.dir, f), "utf8")).meta);
14269
+ metas.push(JSON.parse(readFileSync8(join11(this.dir, f), "utf8")).meta);
13610
14270
  } catch (e) {
13611
- log30.debug(`skipping unreadable session file ${f}`, e);
14271
+ log32.debug(`skipping unreadable session file ${f}`, e);
13612
14272
  }
13613
14273
  }
13614
14274
  return metas.sort((a, b) => b.updated - a.updated);
@@ -13621,7 +14281,7 @@ var SessionStore = class {
13621
14281
  for (const f of readdirSync(this.dir)) {
13622
14282
  if (!f.endsWith(".json")) continue;
13623
14283
  try {
13624
- const st = statSync7(join10(this.dir, f));
14284
+ const st = statSync7(join11(this.dir, f));
13625
14285
  out.push({ id: f.slice(0, -5), mtime: st.mtimeMs, size: st.size });
13626
14286
  } catch {
13627
14287
  }
@@ -13640,7 +14300,7 @@ var SessionStore = class {
13640
14300
  function globalSessionLoad(idOrPrefix) {
13641
14301
  const gd = globalDir();
13642
14302
  if (!existsSync6(gd)) return void 0;
13643
- const exact = join10(gd, `${idOrPrefix}.json`);
14303
+ const exact = join11(gd, `${idOrPrefix}.json`);
13644
14304
  if (existsSync6(exact)) {
13645
14305
  try {
13646
14306
  const target = readIndexLink(exact);
@@ -13654,7 +14314,7 @@ function globalSessionLoad(idOrPrefix) {
13654
14314
  if (!f.endsWith(".json")) continue;
13655
14315
  const base = f.slice(0, -5);
13656
14316
  if (base.includes(idOrPrefix) || base.endsWith(idOrPrefix)) {
13657
- const target = readIndexLink(join10(gd, f));
14317
+ const target = readIndexLink(join11(gd, f));
13658
14318
  return JSON.parse(readFileSync8(target, "utf8"));
13659
14319
  }
13660
14320
  }
@@ -13669,10 +14329,10 @@ function globalSessionList() {
13669
14329
  for (const f of readdirSync(gd)) {
13670
14330
  if (!f.endsWith(".json")) continue;
13671
14331
  try {
13672
- const target = readIndexLink(join10(gd, f));
14332
+ const target = readIndexLink(join11(gd, f));
13673
14333
  if (!existsSync6(target)) {
13674
14334
  try {
13675
- unlinkSync(join10(gd, f));
14335
+ unlinkSync(join11(gd, f));
13676
14336
  } catch {
13677
14337
  }
13678
14338
  continue;
@@ -13846,8 +14506,8 @@ import { execFile as execFile4 } from "child_process";
13846
14506
  import { promisify } from "util";
13847
14507
  import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync9 } from "fs";
13848
14508
  import { homedir as homedir7 } from "os";
13849
- import { join as join11, resolve as resolve4, sep as sep3, parse as parsePath } from "path";
13850
- var log31 = forComponent("checkpoints");
14509
+ import { join as join12, resolve as resolve4, sep as sep3, parse as parsePath } from "path";
14510
+ var log33 = forComponent("checkpoints");
13851
14511
  var exec = promisify(execFile4);
13852
14512
  var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
13853
14513
  function isBroadRoot(p) {
@@ -13889,11 +14549,11 @@ var ShadowRepo = class {
13889
14549
  mkdirSync9(this.gitDir, { recursive: true });
13890
14550
  const valid = await this.run("rev-parse", "--git-dir").then(() => true, () => false);
13891
14551
  if (!valid) await this.run("init", "-q");
13892
- mkdirSync9(join11(this.gitDir, "info"), { recursive: true });
13893
- writeFileSync6(join11(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
14552
+ mkdirSync9(join12(this.gitDir, "info"), { recursive: true });
14553
+ writeFileSync6(join12(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
13894
14554
  this.ready = true;
13895
14555
  } catch (e) {
13896
- log31.debug(`git checkpoints unavailable for ${this.workTree}`, e);
14556
+ log33.debug(`git checkpoints unavailable for ${this.workTree}`, e);
13897
14557
  this.ready = false;
13898
14558
  }
13899
14559
  return this.ready;
@@ -13904,7 +14564,7 @@ var ShadowRepo = class {
13904
14564
  }
13905
14565
  async commit(label, forced = []) {
13906
14566
  await this.run("add", "-A");
13907
- for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log31.debug(`force-add failed: ${p}`, e));
14567
+ for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log33.debug(`force-add failed: ${p}`, e));
13908
14568
  await this.run("commit", "--allow-empty", "-q", "-m", label);
13909
14569
  }
13910
14570
  /** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
@@ -13912,8 +14572,8 @@ var ShadowRepo = class {
13912
14572
  * turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
13913
14573
  * point per turn, so the REPL's turn↔frame mapping stays intact. */
13914
14574
  async amendForced(paths) {
13915
- for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log31.debug(`force-capture failed: ${p}`, e));
13916
- await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log31.debug("amend failed", e));
14575
+ for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log33.debug(`force-capture failed: ${p}`, e));
14576
+ await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log33.debug("amend failed", e));
13917
14577
  }
13918
14578
  /** Commits on `ref`, oldest-first (canonical index space). */
13919
14579
  async log(ref) {
@@ -13973,7 +14633,7 @@ var ShadowRepo = class {
13973
14633
  await this.run("gc", "--auto", "-q").catch(() => {
13974
14634
  });
13975
14635
  } catch (e) {
13976
- log31.debug("checkpoint prune failed", e);
14636
+ log33.debug("checkpoint prune failed", e);
13977
14637
  }
13978
14638
  }
13979
14639
  };
@@ -14005,7 +14665,7 @@ var GitCheckpoints = class {
14005
14665
  const abs = resolve4(d);
14006
14666
  if (abs === cwd || abs.startsWith(cwd + sep3)) continue;
14007
14667
  if (cwd.startsWith(abs + sep3)) continue;
14008
- out.push({ workTree: abs, gitDir: join11(abs, ".agent", "checkpoints.git") });
14668
+ out.push({ workTree: abs, gitDir: join12(abs, ".agent", "checkpoints.git") });
14009
14669
  }
14010
14670
  return out;
14011
14671
  }
@@ -14041,7 +14701,7 @@ var GitCheckpoints = class {
14041
14701
  use(sessionId) {
14042
14702
  if (sessionId === this.session) return;
14043
14703
  this.session = sessionId;
14044
- if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log31.debug("re-point failed", e));
14704
+ if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log33.debug("re-point failed", e));
14045
14705
  }
14046
14706
  async begin(label) {
14047
14707
  if (!await this.start()) return;
@@ -14061,7 +14721,7 @@ var GitCheckpoints = class {
14061
14721
  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
14062
14722
  `);
14063
14723
  } else {
14064
- log31.debug("checkpoint commit failed", e);
14724
+ log33.debug("checkpoint commit failed", e);
14065
14725
  survivors.push(this.repos[i]);
14066
14726
  survivorCaches.push(this.caches[i] ?? []);
14067
14727
  }
@@ -14096,7 +14756,7 @@ var GitCheckpoints = class {
14096
14756
  if (this.forced.has(abs)) continue;
14097
14757
  this.forced.add(abs);
14098
14758
  const i = this.repoIndexFor(abs);
14099
- if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log31.debug("amendForced failed", e));
14759
+ if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log33.debug("amendForced failed", e));
14100
14760
  }
14101
14761
  }
14102
14762
  };
@@ -14148,7 +14808,7 @@ var GitCheckpointsOptions = class {
14148
14808
  /** Real working tree to snapshot (the launch cwd). */
14149
14809
  workTree = process.cwd();
14150
14810
  /** Isolated git dir for the cwd shadow repo (kept out of the user's real .git). */
14151
- gitDir = join11(process.cwd(), ".agent", "checkpoints.git");
14811
+ gitDir = join12(process.cwd(), ".agent", "checkpoints.git");
14152
14812
  /** Extra mounted dirs (`--add-dir`); those outside cwd each get their own shadow repo. */
14153
14813
  addDirs = [];
14154
14814
  /** Conversation id → per-session restore-point ref. */
@@ -14224,7 +14884,7 @@ import { emitKeypressEvents } from "readline";
14224
14884
  import { spawnSync as spawnSync5 } from "child_process";
14225
14885
  import { writeFileSync as writeFileSync7, readFileSync as readFileSync9, unlinkSync as unlinkSync2 } from "fs";
14226
14886
  import { tmpdir as tmpdir2 } from "os";
14227
- import { join as join12 } from "path";
14887
+ import { join as join13 } from "path";
14228
14888
 
14229
14889
  // cli/bidi.ts
14230
14890
  var RTL_RE = /[\u0590-\u05ff\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb1d-\ufdff\ufe70-\ufeff]/;
@@ -15412,7 +16072,7 @@ function createLineEditor(out, input = keyInput) {
15412
16072
  function externalEdit(s) {
15413
16073
  const spec = process.env.VISUAL || process.env.EDITOR || DEFAULT_EDITOR;
15414
16074
  const [cmd, ...cargs] = spec.split(" ").filter(Boolean);
15415
- const file = join12(tmpdir2(), `agentx-edit-${process.pid}-${Date.now()}.md`);
16075
+ const file = join13(tmpdir2(), `agentx-edit-${process.pid}-${Date.now()}.md`);
15416
16076
  try {
15417
16077
  writeFileSync7(file, s.buf);
15418
16078
  process.stdin.setRawMode(false);
@@ -15880,21 +16540,21 @@ init_hostPath();
15880
16540
  import { spawnSync as spawnSync6 } from "child_process";
15881
16541
  import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
15882
16542
  import { homedir as homedir8 } from "os";
15883
- import { join as join13 } from "path";
15884
- var log32 = forComponent("os-sched");
16543
+ import { join as join14 } from "path";
16544
+ var log34 = forComponent("os-sched");
15885
16545
  var OsScheduler = class {
15886
16546
  options;
15887
16547
  constructor(options) {
15888
16548
  this.options = { ...new OsSchedulerOptions(), ...options };
15889
16549
  }
15890
16550
  get dir() {
15891
- return join13(this.options.home, ".agent", "sched");
16551
+ return join14(this.options.home, ".agent", "sched");
15892
16552
  }
15893
16553
  label(id) {
15894
16554
  return `cc.livx.agentx.sched-${id}`;
15895
16555
  }
15896
16556
  plistPath(id) {
15897
- return join13(this.options.home, "Library", "LaunchAgents", `${this.label(id)}.plist`);
16557
+ return join14(this.options.home, "Library", "LaunchAgents", `${this.label(id)}.plist`);
15898
16558
  }
15899
16559
  run(cmd, args, input) {
15900
16560
  return this.options.exec(cmd, args, input);
@@ -15910,11 +16570,11 @@ var OsScheduler = class {
15910
16570
  const script = this.writeScript(spec, oneOff);
15911
16571
  const mechanism = this.options.platform === "darwin" ? this.scheduleDarwin(spec, script) : this.scheduleLinux(spec, script, oneOff);
15912
16572
  const meta = { ...spec, created: Date.now(), mechanism };
15913
- writeFileSync8(join13(this.dir, `${spec.id}.json`), JSON.stringify(meta, null, 2));
16573
+ writeFileSync8(join14(this.dir, `${spec.id}.json`), JSON.stringify(meta, null, 2));
15914
16574
  return mechanism;
15915
16575
  }
15916
16576
  cancel(id) {
15917
- const meta = readJsonFile(join13(this.dir, `${id}.json`), null);
16577
+ const meta = readJsonFile(join14(this.dir, `${id}.json`), null);
15918
16578
  if (!meta) return false;
15919
16579
  try {
15920
16580
  if (this.options.platform === "darwin") {
@@ -15943,11 +16603,11 @@ var OsScheduler = class {
15943
16603
  }
15944
16604
  }
15945
16605
  } catch (e) {
15946
- log32.debug(`cancel ${id}`, e);
16606
+ log34.debug(`cancel ${id}`, e);
15947
16607
  }
15948
16608
  for (const f of [`${id}.json`, `${id}.sh`]) {
15949
16609
  try {
15950
- unlinkSync3(join13(this.dir, f));
16610
+ unlinkSync3(join14(this.dir, f));
15951
16611
  } catch {
15952
16612
  }
15953
16613
  }
@@ -15955,19 +16615,19 @@ var OsScheduler = class {
15955
16615
  }
15956
16616
  list() {
15957
16617
  if (!existsSync7(this.dir)) return [];
15958
- return readdirSync2(this.dir).filter((f) => f.endsWith(".json")).map((f) => readJsonFile(join13(this.dir, f), null)).filter(Boolean);
16618
+ return readdirSync2(this.dir).filter((f) => f.endsWith(".json")).map((f) => readJsonFile(join14(this.dir, f), null)).filter(Boolean);
15959
16619
  }
15960
16620
  /** The per-job runner script: cd to the project, headless-resume the session, log, notify. */
15961
16621
  writeScript(spec, oneOff) {
15962
- const p = join13(this.dir, `${spec.id}.sh`);
16622
+ const p = join14(this.dir, `${spec.id}.sh`);
15963
16623
  const q2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
15964
- const cleanup = oneOff ? this.options.platform === "darwin" ? `launchctl remove ${this.label(spec.id)} 2>/dev/null; rm -f ${q2(this.plistPath(spec.id))} ${q2(join13(this.dir, `${spec.id}.json`))} ${q2(p)}
15965
- ` : `rm -f ${q2(join13(this.dir, `${spec.id}.json`))} ${q2(p)}
16624
+ const cleanup = oneOff ? this.options.platform === "darwin" ? `launchctl remove ${this.label(spec.id)} 2>/dev/null; rm -f ${q2(this.plistPath(spec.id))} ${q2(join14(this.dir, `${spec.id}.json`))} ${q2(p)}
16625
+ ` : `rm -f ${q2(join14(this.dir, `${spec.id}.json`))} ${q2(p)}
15966
16626
  ` : "";
15967
16627
  writeFileSync8(p, `#!/bin/sh
15968
16628
  # agentx scheduled job ${spec.id}${spec.label ? ` \u2014 ${spec.label}` : ""}
15969
16629
  cd ${q2(spec.cwd)} || exit 1
15970
- ${this.options.agentx} -p ${q2(spec.prompt)} --resume ${q2(spec.sessionId)} --yes >> ${q2(join13(this.dir, `${spec.id}.log`))} 2>&1
16630
+ ${this.options.agentx} -p ${q2(spec.prompt)} --resume ${q2(spec.sessionId)} --yes >> ${q2(join14(this.dir, `${spec.id}.log`))} 2>&1
15971
16631
  ${cleanup}`);
15972
16632
  chmodSync(p, 493);
15973
16633
  return p;
@@ -16004,7 +16664,7 @@ ${trigger}
16004
16664
  <key>RunAtLoad</key><false/>
16005
16665
  </dict></plist>
16006
16666
  `;
16007
- mkdirSync10(join13(this.options.home, "Library", "LaunchAgents"), { recursive: true });
16667
+ mkdirSync10(join14(this.options.home, "Library", "LaunchAgents"), { recursive: true });
16008
16668
  writeFileSync8(this.plistPath(spec.id), plist);
16009
16669
  this.run("launchctl", ["load", this.plistPath(spec.id)]);
16010
16670
  return `launchd:${this.label(spec.id)}`;
@@ -16059,11 +16719,11 @@ import { createServer as createServer2, createConnection } from "net";
16059
16719
  import { spawn as spawn3 } from "child_process";
16060
16720
  import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "fs";
16061
16721
  import { homedir as homedir9 } from "os";
16062
- import { join as join14 } from "path";
16722
+ import { join as join15 } from "path";
16063
16723
  init_platform();
16064
- var log33 = forComponent("remote-trigger");
16065
- var TRIGGER_DIR = () => join14(homedir9(), ".agent", "triggers");
16066
- var sockPath = (sessionId, dir = TRIGGER_DIR()) => join14(dir, `${sessionId}.sock`);
16724
+ var log35 = forComponent("remote-trigger");
16725
+ var TRIGGER_DIR = () => join15(homedir9(), ".agent", "triggers");
16726
+ var sockPath = (sessionId, dir = TRIGGER_DIR()) => join15(dir, `${sessionId}.sock`);
16067
16727
  var TriggerServer = class {
16068
16728
  constructor(onTrigger) {
16069
16729
  this.onTrigger = onTrigger;
@@ -16099,13 +16759,13 @@ var TriggerServer = class {
16099
16759
  conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
16100
16760
  }
16101
16761
  });
16102
- conn.on("error", (e) => log33.debug("trigger conn error", e));
16762
+ conn.on("error", (e) => log35.debug("trigger conn error", e));
16103
16763
  });
16104
- this.server.on("error", (e) => log33.debug("trigger server error", e));
16764
+ this.server.on("error", (e) => log35.debug("trigger server error", e));
16105
16765
  this.server.listen(p);
16106
16766
  this.path = p;
16107
16767
  } catch (e) {
16108
- log33.debug("trigger server unavailable", e);
16768
+ log35.debug("trigger server unavailable", e);
16109
16769
  }
16110
16770
  }
16111
16771
  /** Re-bind on /resume (the session id changed). */
@@ -16237,7 +16897,7 @@ var strike = C("9");
16237
16897
  var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
16238
16898
  var mdPalette = { bold, dim, cyan, italic, strike, green, link };
16239
16899
  var err = (s) => process.stderr.write(s);
16240
- var log34 = forComponent("cli");
16900
+ var log36 = forComponent("cli");
16241
16901
  var VERSION = (() => {
16242
16902
  try {
16243
16903
  return JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
@@ -16377,7 +17037,10 @@ function parseArgs(argv) {
16377
17037
  a.hardenNet = true;
16378
17038
  } else if (x === "--subagents") a.subagents = true;
16379
17039
  else if (x === "--duplex") a.duplex = true;
16380
- else if (x === "--conversational" || x === "--convo" || x === "--voice") {
17040
+ else if (x === "--duplex-v2") {
17041
+ a.duplex = true;
17042
+ a.duplexV2 = true;
17043
+ } else if (x === "--conversational" || x === "--convo" || x === "--voice") {
16381
17044
  a.voice = true;
16382
17045
  a.duplex = true;
16383
17046
  } else if (x === "--voice-model") a.voiceModel = val(++i, x);
@@ -16461,6 +17124,8 @@ Flags:
16461
17124
  --append-system-prompt <t> extra instructions appended to the system prompt for this run
16462
17125
  --duplex duplex mode: a fast voice model replies instantly and delegates real work
16463
17126
  to a background worker agent (-m model); results are re-voiced when ready
17127
+ --duplex-v2 duplex + a second voice channel: backchannels, fillers and bridges that never
17128
+ enter the agent's history (headless: stdin interim/pause/agent_state, stdout channel)
16464
17129
  --conversational duplex with a conversation-native register \u2014 short fast turns, fillers,
16465
17130
  impulsive reactions, human pacing (implies --duplex; aliases: --convo, --voice)
16466
17131
  with SONIOX_API_KEY + CARTESIA_API_KEY(+VOICE_ID) set: real voice I/O \u2014 mic in,
@@ -16563,10 +17228,10 @@ function loadEnvFile(file) {
16563
17228
  }
16564
17229
  function loadInstallEnv() {
16565
17230
  let dir = dirname6(import.meta.path);
16566
- for (let i = 0; i < 5 && !existsSync9(join15(dir, "package.json")); i++) dir = dirname6(dir);
17231
+ for (let i = 0; i < 5 && !existsSync9(join16(dir, "package.json")); i++) dir = dirname6(dir);
16567
17232
  for (const name of [".env", ".env.local"]) {
16568
- loadEnvFile(join15(dir, name));
16569
- loadEnvFile(join15(homedir10(), ".agent", name));
17233
+ loadEnvFile(join16(dir, name));
17234
+ loadEnvFile(join16(homedir10(), ".agent", name));
16570
17235
  }
16571
17236
  }
16572
17237
  async function loadBodifySecrets() {
@@ -17063,7 +17728,7 @@ function turnCost(model, usage) {
17063
17728
  const priced = model.startsWith("claude-code/") ? `anthropic/${resolveModelAlias(model.slice("claude-code/".length))}` : model;
17064
17729
  return costOf(getModelInfo(priced)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, priced);
17065
17730
  }
17066
- async function evaluateGoal(ai, condition, transcript, log35) {
17731
+ async function evaluateGoal(ai, condition, transcript, log37) {
17067
17732
  const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
17068
17733
  const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
17069
17734
  return text.slice(0, 600);
@@ -17083,7 +17748,7 @@ ${recent}` }
17083
17748
  const match = r.content.match(/\{[\s\S]*\}/);
17084
17749
  if (match) return JSON.parse(match[0]);
17085
17750
  } catch (e) {
17086
- log35(dim(` (goal evaluator error: ${e?.message ?? e})
17751
+ log37(dim(` (goal evaluator error: ${e?.message ?? e})
17087
17752
  `));
17088
17753
  }
17089
17754
  return { met: false, reason: "evaluation unclear" };
@@ -17309,13 +17974,13 @@ function mcpAgentTools(mounted, opts) {
17309
17974
  return tools;
17310
17975
  }
17311
17976
  async function closeMcp(mounted) {
17312
- await Promise.all(mounted.map((m) => m.client.close().catch((e) => log34.debug("mcp close failed", e))));
17977
+ await Promise.all(mounted.map((m) => m.client.close().catch((e) => log36.debug("mcp close failed", e))));
17313
17978
  }
17314
17979
  var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
17315
17980
  function mentionRefs(line) {
17316
17981
  return [...line.matchAll(/(?:^|\s)@(?:"([^"]+)"|(\S+))/g)].map((m) => m[1] ?? m[2].replace(/[?!.,;:)\]}'">]+$/, "")).filter(Boolean);
17317
17982
  }
17318
- var untilde = (p) => p.startsWith("~/") || IS_WIN && p.startsWith("~\\") ? join15(homedir10(), p.slice(2)) : p;
17983
+ var untilde = (p) => p.startsWith("~/") || IS_WIN && p.startsWith("~\\") ? join16(homedir10(), p.slice(2)) : p;
17319
17984
  var PATH_LIKE = IS_WIN ? /^(\/|~[\\/]|\.\.?[\\/]|[A-Za-z]:[\\/]|\\\\)/ : /^(\/|~\/|\.\/|\.\.\/)/;
17320
17985
  var userPath = (cwd, p) => {
17321
17986
  const t = untilde(p);
@@ -17368,7 +18033,7 @@ async function expandMentions(fs, line) {
17368
18033
  if (loaded.includes(ref) || missing.includes(ref)) continue;
17369
18034
  if (ref.includes(":") && mcpMentionResolver) {
17370
18035
  const body = await mcpMentionResolver(ref).catch((e) => {
17371
- log34.debug("mcp mention resolve failed", e);
18036
+ log36.debug("mcp mention resolve failed", e);
17372
18037
  return null;
17373
18038
  });
17374
18039
  if (body != null) {
@@ -17459,7 +18124,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
17459
18124
  try {
17460
18125
  store.save(session);
17461
18126
  } catch (ex) {
17462
- log34.debug("mid-turn session flush failed", ex);
18127
+ log36.debug("mid-turn session flush failed", ex);
17463
18128
  }
17464
18129
  }
17465
18130
  return origNotify(e);
@@ -17527,7 +18192,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
17527
18192
  }
17528
18193
  return { ok, res };
17529
18194
  }
17530
- var remintSessionImages = (data, store, cwd) => remintImageRefs(data.messages, [join15(store.dir, `${data.meta.id}.images`), cwd]);
18195
+ var remintSessionImages = (data, store, cwd) => remintImageRefs(data.messages, [join16(store.dir, `${data.meta.id}.images`), cwd]);
17531
18196
  function startSession(args, store, agent, cwd) {
17532
18197
  if (args.resume || args.cont) {
17533
18198
  const data = args.resume ? store.load(args.resume) ?? globalSessionLoad(args.resume) : store.latestData();
@@ -17574,19 +18239,19 @@ var AGENTS_MD_TEMPLATE = `# ${"${name}"}
17574
18239
  `;
17575
18240
  function initInstructions(cwd) {
17576
18241
  for (const f of ["AGENTS.md", "CLAUDE.md"]) {
17577
- if (existsSync9(join15(cwd, f))) {
18242
+ if (existsSync9(join16(cwd, f))) {
17578
18243
  err(yellow(` ${f} already exists \u2014 leaving it as-is
17579
18244
  `));
17580
18245
  return;
17581
18246
  }
17582
18247
  }
17583
- const path = join15(cwd, "AGENTS.md");
18248
+ const path = join16(cwd, "AGENTS.md");
17584
18249
  writeFileSync9(path, AGENTS_MD_TEMPLATE.replace("${name}", basename4(cwd)));
17585
18250
  err(green(` created ${path}
17586
18251
  `) + dim(" edit it, then it auto-loads into every run.\n"));
17587
18252
  }
17588
18253
  function persistSetting(cwd, key, value) {
17589
- const path = join15(cwd, ".agent", "settings.json");
18254
+ const path = join16(cwd, ".agent", "settings.json");
17590
18255
  try {
17591
18256
  const obj = existsSync9(path) ? JSON.parse(readFileSync10(path, "utf8")) : {};
17592
18257
  if (obj[key] === value) return;
@@ -17608,14 +18273,14 @@ var isCancelTeardown = (e) => {
17608
18273
  function installCancelGuards(mounted) {
17609
18274
  process.on("unhandledRejection", (e) => {
17610
18275
  if (isCancelTeardown(e)) {
17611
- log34.debug("suppressed unhandledRejection (cursor stream cancel)", e);
18276
+ log36.debug("suppressed unhandledRejection (cursor stream cancel)", e);
17612
18277
  return;
17613
18278
  }
17614
- log34.error("unhandledRejection", e);
18279
+ log36.error("unhandledRejection", e);
17615
18280
  });
17616
18281
  process.on("uncaughtException", (e) => {
17617
18282
  if (isCancelTeardown(e)) {
17618
- log34.debug("suppressed uncaughtException (cursor stream cancel)", e);
18283
+ log36.debug("suppressed uncaughtException (cursor stream cancel)", e);
17619
18284
  return;
17620
18285
  }
17621
18286
  console.error(e);
@@ -17624,7 +18289,7 @@ function installCancelGuards(mounted) {
17624
18289
  });
17625
18290
  }
17626
18291
  async function repl(args, ai, cfg, cwd) {
17627
- const oauth = new McpOAuth({ storePath: join15(cwd, ".agent", "mcp-auth.json") });
18292
+ const oauth = new McpOAuth({ storePath: join16(cwd, ".agent", "mcp-auth.json") });
17628
18293
  const mounted = await mountMcp(cfg, oauth);
17629
18294
  let mcpToolNames = [];
17630
18295
  const initialMcpTools = mcpAgentTools(mounted);
@@ -17902,7 +18567,7 @@ async function repl(args, ai, cfg, cwd) {
17902
18567
  quickLook: {
17903
18568
  branch: () => {
17904
18569
  try {
17905
- const head = readFileSync10(join15(cwd, ".git", "HEAD"), "utf8").trim();
18570
+ const head = readFileSync10(join16(cwd, ".git", "HEAD"), "utf8").trim();
17906
18571
  return head.startsWith("ref: refs/heads/") ? `branch: ${head.slice("ref: refs/heads/".length)}` : `detached HEAD at ${head.slice(0, 12)}`;
17907
18572
  } catch {
17908
18573
  return "not a git repository";
@@ -18036,11 +18701,11 @@ async function repl(args, ai, cfg, cwd) {
18036
18701
  const pendingImages = [];
18037
18702
  const bangContext = [];
18038
18703
  const grabClipboardAttachment = () => {
18039
- const dir = join15(store.dir, `${session.meta.id}.images`);
18704
+ const dir = join16(store.dir, `${session.meta.id}.images`);
18040
18705
  try {
18041
18706
  mkdirSync12(dir, { recursive: true });
18042
18707
  } catch (e) {
18043
- log34.debug("paste image dir create failed", e);
18708
+ log36.debug("paste image dir create failed", e);
18044
18709
  }
18045
18710
  const img = grabClipboardImage(dir, String(Date.now()));
18046
18711
  return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
@@ -18094,7 +18759,7 @@ async function repl(args, ai, cfg, cwd) {
18094
18759
  err(dim(` \u23F0 ${scheduler.size} scheduled job(s) re-armed
18095
18760
  `));
18096
18761
  }
18097
- const checkpoints = args.vfs || args.boddb ? new CheckpointStack(agent.options.fs) : new GitCheckpoints({ workTree: cwd, gitDir: join15(cwd, ".agent", "checkpoints.git"), addDirs: args.addDirs, sessionId: session.meta.id });
18762
+ const checkpoints = args.vfs || args.boddb ? new CheckpointStack(agent.options.fs) : new GitCheckpoints({ workTree: cwd, gitDir: join16(cwd, ".agent", "checkpoints.git"), addDirs: args.addDirs, sessionId: session.meta.id });
18098
18763
  const cpHooks = checkpoints.hooks?.();
18099
18764
  if (cpHooks) {
18100
18765
  agent.options.hooks = composeHooks(agent.options.hooks, cpHooks);
@@ -18146,14 +18811,14 @@ ${lines.join("\n")}
18146
18811
  Added entries are loadable now via the Skill/SlashCommand tools; removed ones are gone even if still listed in the system prompt.
18147
18812
  </system-reminder>`;
18148
18813
  };
18149
- const histPath = join15(cwd, ".agent", "history");
18814
+ const histPath = join16(cwd, ".agent", "history");
18150
18815
  const history = existsSync9(histPath) ? readFileSync10(histPath, "utf8").split("\n").filter(Boolean).reverse().slice(0, 500) : [];
18151
18816
  const remember2 = (line) => {
18152
18817
  try {
18153
- mkdirSync12(join15(cwd, ".agent"), { recursive: true });
18818
+ mkdirSync12(join16(cwd, ".agent"), { recursive: true });
18154
18819
  appendFileSync(histPath, line + "\n");
18155
18820
  } catch (e) {
18156
- log34.debug("history write failed", e);
18821
+ log36.debug("history write failed", e);
18157
18822
  }
18158
18823
  };
18159
18824
  const ago = (t) => {
@@ -18225,7 +18890,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
18225
18890
  try {
18226
18891
  store.save(session);
18227
18892
  } catch (e) {
18228
- log34.debug("session save after rewind failed", e);
18893
+ log36.debug("session save after rewind failed", e);
18229
18894
  }
18230
18895
  err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
18231
18896
  `));
@@ -18260,7 +18925,7 @@ ${task}`;
18260
18925
  bangContext.length = 0;
18261
18926
  }
18262
18927
  const delta = await refreshCatalogs().catch((e) => {
18263
- log34.debug("catalog refresh failed", e);
18928
+ log36.debug("catalog refresh failed", e);
18264
18929
  return "";
18265
18930
  });
18266
18931
  if (delta) {
@@ -18523,7 +19188,7 @@ ${task}`;
18523
19188
  desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
18524
19189
  run: async () => {
18525
19190
  await refreshCatalogs().catch((e) => {
18526
- log34.debug("catalog refresh failed", e);
19191
+ log36.debug("catalog refresh failed", e);
18527
19192
  });
18528
19193
  face.reprepare();
18529
19194
  err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
@@ -19026,7 +19691,7 @@ ${task}`;
19026
19691
  try {
19027
19692
  for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
19028
19693
  } catch (e) {
19029
- log34.debug(`loadAgents(${d}) failed`, e);
19694
+ log36.debug(`loadAgents(${d}) failed`, e);
19030
19695
  }
19031
19696
  }
19032
19697
  if (!seen.size) {
@@ -19114,7 +19779,7 @@ ${task}`;
19114
19779
  }
19115
19780
  if (idx >= 0) {
19116
19781
  const old = mounted.splice(idx, 1)[0];
19117
- await old.client.close().catch((e) => log34.debug("mcp close failed", e));
19782
+ await old.client.close().catch((e) => log36.debug("mcp close failed", e));
19118
19783
  }
19119
19784
  try {
19120
19785
  const m = await mountMcpServer(name, conf);
@@ -19142,7 +19807,7 @@ ${task}`;
19142
19807
  }
19143
19808
  const m = mounted.splice(idx, 1)[0];
19144
19809
  remountMcpTools();
19145
- await m.client.close().catch((e) => log34.debug("mcp close failed", e));
19810
+ await m.client.close().catch((e) => log36.debug("mcp close failed", e));
19146
19811
  err(dim(` removed "${name}"
19147
19812
  `));
19148
19813
  return;
@@ -19257,7 +19922,7 @@ ${task}`;
19257
19922
  return;
19258
19923
  }
19259
19924
  const md = exportMarkdown(session.meta, shown);
19260
- const name = a[0] ? extname(a[0]) ? a[0] : a[0] + ".md" : join15(".agent", "exports", `${session.meta.id}.md`);
19925
+ const name = a[0] ? extname(a[0]) ? a[0] : a[0] + ".md" : join16(".agent", "exports", `${session.meta.id}.md`);
19261
19926
  const path = resolve5(cwd, name);
19262
19927
  try {
19263
19928
  mkdirSync12(dirname6(path), { recursive: true });
@@ -19343,9 +20008,9 @@ ${task}`;
19343
20008
  });
19344
20009
  const listDir = (absDir) => {
19345
20010
  try {
19346
- return readdirSync4(join15(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
20011
+ return readdirSync4(join16(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
19347
20012
  } catch (e) {
19348
- log34.debug("completion readdir failed", absDir, e);
20013
+ log36.debug("completion readdir failed", absDir, e);
19349
20014
  return null;
19350
20015
  }
19351
20016
  };
@@ -19482,7 +20147,7 @@ ${out}
19482
20147
  return;
19483
20148
  }
19484
20149
  await refreshCatalogs().catch((e) => {
19485
- log34.debug("catalog refresh failed", e);
20150
+ log36.debug("catalog refresh failed", e);
19486
20151
  });
19487
20152
  const sk = skills.find((x) => x.name === name);
19488
20153
  if (sk) {
@@ -19528,7 +20193,7 @@ ${out}
19528
20193
  return false;
19529
20194
  }
19530
20195
  const fakeVoice = process.env.AGENTX_VOICE_FAKE ? fakeVoiceParts(process.env.AGENTX_VOICE_FAKE) : null;
19531
- voiceDiag = new JsonlDiagSink(join15(store.dir, `${session.meta.id}.voice.jsonl`));
20196
+ voiceDiag = new JsonlDiagSink(join16(store.dir, `${session.meta.id}.voice.jsonl`));
19532
20197
  voiceIO = new VoiceIO({
19533
20198
  ...fakeVoice ?? {},
19534
20199
  onDiag: (ev) => voiceDiag?.push(ev),
@@ -19593,7 +20258,7 @@ ${out}
19593
20258
  if (delivery || cut) dx?.parkCutDelivery(delivery, !!cut, heard);
19594
20259
  const cutReply = cut && cut.full.length - cut.heard.length > 40 ? cut : null;
19595
20260
  const noteP = replaces ? replaces.note : !cutReply ? Promise.resolve("") : (instinct ? instinct.bargeIntent(text).catch((e) => {
19596
- log34.warn(`barge intent failed \u2014 model decides: ${e.message}`);
20261
+ log36.warn(`barge intent failed \u2014 model decides: ${e.message}`);
19597
20262
  return void 0;
19598
20263
  }) : Promise.resolve(void 0)).then(async (intent) => interruptionNote(cutReply, intent) + ackedNote(await instinct?.ackFor(text)));
19599
20264
  const cutIn = replaces ? replaces.cutIn : interjectNote;
@@ -19688,6 +20353,14 @@ ${out}
19688
20353
  };
19689
20354
  try {
19690
20355
  await voiceIO.start();
20356
+ if (args.duplexV2 && dx) {
20357
+ const v = voiceIO;
20358
+ bindSecondChannel(v, makeSecondChannel(ai, dx.options.reflexModel, { show: (t) => {
20359
+ err("\r\x1B[0J" + dim(` \u21B3 ${t}
20360
+ `));
20361
+ editorRef?.redrawNow();
20362
+ } }), () => voiceIO === v);
20363
+ }
19691
20364
  const inDev = voiceIO.usingAec ? detectedInputDevice() : null;
19692
20365
  const inNote = inDev ? `, in: ${inDev.name}` : "";
19693
20366
  err(dim(` \u{1F3A4} voice on (${voiceIO.usingAec ? "echo-cancelled" : "heuristic echo \u2014 headphones recommended"}${inNote}) \u2014 just talk; speak over it to interrupt; say "voice off" to stop
@@ -19938,17 +20611,32 @@ function duplexError(e, reflexModel) {
19938
20611
  return code && !msg.includes(String(code)) ? `${msg} (${code})` : msg;
19939
20612
  }
19940
20613
  var leakDecider = () => process.env.TYPESAFE_AI_KEY ? new DecideClient({ apiKey: process.env.TYPESAFE_AI_KEY }) : void 0;
20614
+ function makeSecondChannel(ai, reflexModel, opts) {
20615
+ const decider = leakDecider();
20616
+ const instinct = decider ? new Instinct({ decider }) : void 0;
20617
+ const model = process.env.AGENTX_CHANNEL_MODEL || reflexModel;
20618
+ return new SecondChannel({
20619
+ // Banks in the user's language: generated once per language, cached + topped up across sessions (~/.agent/cache).
20620
+ banks: new ChannelBanks({ generate: fastBankGenerator(ai, model), cacheDir: join16(homedir10(), ".agent", "cache", "channel-banks") }),
20621
+ decide: instinct ? (heard, state) => instinct.channel(heard, state) : void 0,
20622
+ generate: fastLineGenerator(ai, model),
20623
+ onEvent: (e) => log36.debug(`channel ${e.trigger}: rule=${e.rule} choice=${e.choice ?? "-"} \u2192 ${e.acted}${e.text ? ` "${e.text}"` : ""} ${e.ms}ms`),
20624
+ ...opts
20625
+ });
20626
+ }
19941
20627
  async function duplexServe(args, ai, cfg, cwd) {
19942
20628
  const out = (o) => process.stdout.write(JSON.stringify(o) + "\n");
19943
- const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join15(cwd, ".agent", "mcp-auth.json") }));
20629
+ const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join16(cwd, ".agent", "mcp-auth.json") }));
19944
20630
  installCancelGuards(mounted);
19945
20631
  const agent = await makeAgent(args, ai, cfg, mcpAgentTools(mounted, { quiet: true }), mounted.map((m) => m.name));
19946
20632
  const { host: _h, stream: _s, signal: _g, providerOptions: _po, ...wo } = agent.options;
19947
20633
  const host = {
19948
20634
  notify(e) {
19949
20635
  const k = String(e.kind);
19950
- if (k === "text_delta") out({ type: "text", text: e.message });
19951
- else if (k === "thinking_delta") out({ type: "thinking", text: e.message });
20636
+ if (k === "text_delta") {
20637
+ out({ type: "text", text: e.message });
20638
+ sc?.onMainTokens(String(e.message ?? ""));
20639
+ } else if (k === "thinking_delta") out({ type: "thinking", text: e.message });
19952
20640
  else if (k === "speak_utterance") out({ type: "speak", text: e.message });
19953
20641
  else if (k === "hold_filler") out({ type: "filler", text: e.message });
19954
20642
  else if (k === "turn_replace") out({ type: "replaced", ...e.data });
@@ -19980,6 +20668,8 @@ async function duplexServe(args, ai, cfg, cwd) {
19980
20668
  reflexOptions: { fs: agent.options.fs, tools: [exitSessionTool(() => out({ type: "exit" }))] }
19981
20669
  });
19982
20670
  if (args.voiceSystemPrompt) dx.voice.options.systemPrompt += "\n\n" + args.voiceSystemPrompt;
20671
+ let scTurn = 0;
20672
+ const sc = args.duplexV2 ? makeSecondChannel(ai, dx.options.reflexModel, { show: (text, kind) => out({ type: "channel", kind, text }) }) : void 0;
19983
20673
  const store = new SessionStore(cwd);
19984
20674
  const session = startSession(args, store, dx.voice, cwd);
19985
20675
  persist = () => {
@@ -19988,7 +20678,7 @@ async function duplexServe(args, ai, cfg, cwd) {
19988
20678
  try {
19989
20679
  store.save(session);
19990
20680
  } catch (e) {
19991
- log34.warn("duplex session save failed", e);
20681
+ log36.warn("duplex session save failed", e);
19992
20682
  }
19993
20683
  };
19994
20684
  out({ type: "ready", sessionId: session.meta.id, reflexModel: dx.options.reflexModel, actModel: dx.options.actModel, ...notes.length ? { notes } : {} });
@@ -20002,20 +20692,30 @@ async function duplexServe(args, ai, cfg, cwd) {
20002
20692
  return;
20003
20693
  }
20004
20694
  if (m.type === "barge") {
20695
+ sc?.onBargeIn();
20005
20696
  activeTurn?.abort();
20006
20697
  dx.parkInFlightDeliveries();
20698
+ } else if (sc && channelInput(sc, m)) {
20007
20699
  } else if (m.type === "cancel") out({ type: "task", kind: "cancel_result", id: m.task, message: dx.cancelTask(String(m.task)) });
20008
20700
  else if (m.type === "exit") rl.close();
20009
20701
  else if (m.type === "user" && typeof m.text === "string" && m.text.trim()) {
20010
- const { reply, delivery, heard } = splitCut(m.cut ?? null);
20702
+ const cut = m.cut && sc?.spoke(String(m.cut.full ?? "")) ? { ...m.cut, full: "", heard: "" } : m.cut;
20703
+ const { reply, delivery, heard } = splitCut(cut ?? null);
20011
20704
  if (delivery || reply) dx.parkCutDelivery(delivery, !!reply, heard);
20012
20705
  const note = reply && reply.full.length - reply.heard.length > 40 ? interruptionNote(reply) : "";
20013
20706
  const id = m.id ? String(m.id) : void 0;
20707
+ sc?.onUserTurn(m.text);
20708
+ const seq = ++scTurn;
20709
+ const done2 = () => {
20710
+ if (seq === scTurn) sc?.onAgentState("idle");
20711
+ };
20014
20712
  void runTurn(dx.voice, store, session, m.text + note, void 0, cwd, (c) => dx.send(c, { id, replaces: m.replaces ? String(m.replaces) : void 0, followUp: typeof m.followUp === "string" || m.followUp && typeof m.followUp === "object" ? m.followUp : void 0 })).then(({ res, error }) => {
20015
20713
  const why = (res?.finishReason ?? "error") === "error" ? duplexError(res?.error ?? error, dx.options.reflexModel) : "";
20714
+ done2();
20016
20715
  out({ type: "turn_done", id, finishReason: res?.finishReason ?? "error", ...why ? { error: why } : {} });
20017
20716
  }).catch((e) => {
20018
- log34.error("duplex turn failed", e);
20717
+ log36.error("duplex turn failed", e);
20718
+ done2();
20019
20719
  out({ type: "turn_done", id, finishReason: "error", error: String(e?.message ?? e) });
20020
20720
  });
20021
20721
  }
@@ -20175,7 +20875,7 @@ async function main() {
20175
20875
  worktreeCleanup?.();
20176
20876
  process.exit(0);
20177
20877
  }
20178
- const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join15(cwd, ".agent", "mcp-auth.json") }));
20878
+ const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join16(cwd, ".agent", "mcp-auth.json") }));
20179
20879
  const agent = await makeAgent(args, ai, cfg, mcpAgentTools(mounted), mounted.map((m) => m.name));
20180
20880
  const store = new SessionStore(cwd);
20181
20881
  const session = startSession(args, store, agent, cwd);