@livx.cc/agentx 0.99.120 → 0.99.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2754,7 +2754,7 @@ async function psGroupCpuSeconds(pgid) {
2754
2754
  try {
2755
2755
  return await winTreeCpuSeconds(pgid);
2756
2756
  } catch (e) {
2757
- log9.debug(`cpu probe failed for ${pgid}: ${e}`);
2757
+ log10.debug(`cpu probe failed for ${pgid}: ${e}`);
2758
2758
  return void 0;
2759
2759
  }
2760
2760
  }
@@ -2763,7 +2763,7 @@ async function psGroupCpuSeconds(pgid) {
2763
2763
  const out = await new Promise((res, rej) => execFile2("ps", ["-A", "-o", "pid=,ppid=,pgid=,time="], { timeout: 5e3, env: { ...process.env, LC_ALL: "C" } }, (e, so) => e ? rej(e) : res(String(so))));
2764
2764
  return sumTreeCpu(out, pgid);
2765
2765
  } catch (e) {
2766
- log9.debug(`ps cpu probe failed for group ${pgid}: ${e}`);
2766
+ log10.debug(`ps cpu probe failed for group ${pgid}: ${e}`);
2767
2767
  return void 0;
2768
2768
  }
2769
2769
  }
@@ -2912,7 +2912,7 @@ function makeRealShellTool(options) {
2912
2912
  proc.stderr?.on("data", collect);
2913
2913
  proc.on("error", (err) => {
2914
2914
  if (err?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
2915
- log9.debug("shell spawn error", err);
2915
+ log10.debug("shell spawn error", err);
2916
2916
  finish(`[exit 1] ${err?.message ?? err}${out ? "\n" + clean(out) : ""}`);
2917
2917
  });
2918
2918
  proc.on("close", (code) => {
@@ -2991,7 +2991,7 @@ ${clean(out) || "(no output yet)"}`;
2991
2991
  }
2992
2992
  ];
2993
2993
  }
2994
- var log9, clean, DETACHED, shellArgv, SECRET_ENV_RE, _spawn, ShellJobRegistry, shellStatus, NO_JOB2;
2994
+ var log10, clean, DETACHED, shellArgv, SECRET_ENV_RE, _spawn, ShellJobRegistry, shellStatus, NO_JOB2;
2995
2995
  var init_tools_shell = __esm({
2996
2996
  "src/tools.shell.ts"() {
2997
2997
  "use strict";
@@ -3001,7 +3001,7 @@ var init_tools_shell = __esm({
3001
3001
  init_shell_sandbox();
3002
3002
  init_tools_jobs();
3003
3003
  init_platform();
3004
- log9 = forComponent("shell");
3004
+ log10 = forComponent("shell");
3005
3005
  clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
3006
3006
  DETACHED = { stdio: ["ignore", "pipe", "pipe"], ...DETACH_OPTIONS };
3007
3007
  shellArgv = (sh, command) => ({ bin: sh.bin, args: sh.args(command), ...sh.spawnOptions ? { spawnOptions: sh.spawnOptions } : {} });
@@ -4276,7 +4276,11 @@ function childOptionsFor(opts, fs, depth, maxDepth, agentType, model, root) {
4276
4276
  if (po) childOpts.providerOptions = po;
4277
4277
  const deny = opts.permissions?.options.rules.filter((r) => r.decision === "deny") ?? [];
4278
4278
  if (deny.length) childOpts.permissions = new PermissionPolicy({ rules: deny, default: "allow" });
4279
- if (opts.hooks?.preToolUse) childOpts.hooks = { preToolUse: opts.hooks.preToolUse.bind(opts.hooks) };
4279
+ const h = opts.hooks;
4280
+ if (h?.preToolUse || opts.inheritPostToolUse && h?.postToolUse) childOpts.hooks = {
4281
+ ...h.preToolUse ? { preToolUse: h.preToolUse.bind(h) } : {},
4282
+ ...opts.inheritPostToolUse && h.postToolUse ? { postToolUse: h.postToolUse.bind(h) } : {}
4283
+ };
4280
4284
  return childOpts;
4281
4285
  }
4282
4286
  function makeTaskTool(opts) {
@@ -4500,6 +4504,147 @@ async function loadAgents(fs, dir) {
4500
4504
  init_OverlayFilesystem();
4501
4505
  init_logging();
4502
4506
 
4507
+ // src/modes.ts
4508
+ init_logging();
4509
+ var log8 = forComponent("modes");
4510
+ var WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "ApplyEdits"]);
4511
+ var PLANNER_BLOCKED = /* @__PURE__ */ new Set([...WRITE_TOOLS, "bash", "Shell"]);
4512
+ var PLANNER_REFUSAL = "Refused: you are the planner and do not write files. Delegate the change with Task/TaskBatch (model set to the worker) and give the worker a full spec.";
4513
+ var PLAN_FIRST = "Refused: check your plan with `Advise` before the first write (files, data shape, the hard parts). Then write.";
4514
+ var ADVISE_LIMIT = 3;
4515
+ var SNAPSHOT_CAP = 6e4;
4516
+ function pairOk(kind, main, other) {
4517
+ if (other === main) {
4518
+ log8.warn(`${kind} off: both sides are ${main}`);
4519
+ return false;
4520
+ }
4521
+ if (main.startsWith("cursor/") || other.startsWith("cursor/")) {
4522
+ log8.warn(`${kind} off: cursor models are not supported (${main} / ${other})`);
4523
+ return false;
4524
+ }
4525
+ return true;
4526
+ }
4527
+ function delegateWorkerFor(planner, worker) {
4528
+ return worker && pairOk("delegate", planner, worker) ? worker : void 0;
4529
+ }
4530
+ function advisorFor(worker, advisor) {
4531
+ return advisor && pairOk("advisor", worker, advisor) ? advisor : void 0;
4532
+ }
4533
+ function delegateTools(tools, planner) {
4534
+ return tools.map((t) => !PLANNER_BLOCKED.has(t.name) ? t : {
4535
+ ...t,
4536
+ run: async (...a) => a[1]?.model === planner ? PLANNER_REFUSAL : t.run(...a)
4537
+ });
4538
+ }
4539
+ function delegatePrompt(worker) {
4540
+ return `
4541
+
4542
+ ## You are the PLANNER
4543
+ You cannot write files: Write, Edit, MultiEdit, ApplyEdits and the shells are refused for you. Every file change goes through a worker. Read what you need, decide the design (files, data shape, interfaces, the rules that matter), then hand the writing to workers with \`TaskBatch\` (independent files in parallel) or \`Task\`, with \`model: "${worker}"\` (the default). Give each worker a complete, self-contained spec: it sees the files but not your reasoning. When they finish, read the result and send fix-up tasks for anything wrong or missing. Finish only when the work is complete.`;
4544
+ }
4545
+ function advisorPrompt(advisor) {
4546
+ return `
4547
+
4548
+ ## You have an ADVISOR
4549
+ The \`Advise\` tool asks a stronger model (${advisor}). You do the work yourself. Call \`Advise\` once BEFORE you write \u2014 writes are refused until you do (ask it to check your plan: files, data shape, the hard parts) \u2014 and once AFTER the work is complete (ask it to review for defects), then fix what it finds. Do not call it for small steps.`;
4550
+ }
4551
+ async function textOf(r) {
4552
+ if (r && typeof r[Symbol.asyncIterator] === "function") {
4553
+ let s = "";
4554
+ for await (const c of r) if (typeof c?.content === "string") s += c.content;
4555
+ return s;
4556
+ }
4557
+ return String(r?.content ?? r?.text ?? "");
4558
+ }
4559
+ function makeAdvisor(o) {
4560
+ let used = 0;
4561
+ const limit = o.limit ?? ADVISE_LIMIT;
4562
+ const tool = {
4563
+ name: "Advise",
4564
+ description: `Ask a stronger model (${o.advisor}) for advice. It sees the user's request${o.snapshot ? " and the current state" : ""}, not this conversation, so put everything it needs in \`question\`. Use it at most ${limit} times: once for the plan before you write, once to review the finished work.`,
4565
+ parameters: { type: "object", properties: { question: { type: "string", description: "what you want advice on, with the context it needs" } }, required: ["question"] },
4566
+ async run(args) {
4567
+ const question = String(args?.question ?? "").trim();
4568
+ if (!question) return "No advice: the question is empty.";
4569
+ if (used >= limit) return `No advice: the limit of ${limit} calls is used. Continue with your own judgment.`;
4570
+ used++;
4571
+ let state = "";
4572
+ if (o.snapshot) {
4573
+ const s = String(await Promise.resolve(o.snapshot()).catch((err) => {
4574
+ log8.warn("advisor snapshot failed:", String(err));
4575
+ return "";
4576
+ }) || "(empty)");
4577
+ state = `
4578
+
4579
+ ## Current state
4580
+ ${s.length > SNAPSHOT_CAP ? `${s.slice(0, SNAPSHOT_CAP)}
4581
+ \u2026(cut)` : s}`;
4582
+ }
4583
+ const review = used > 1 ? "" : "\n\n(When the work is complete, call Advise again for a review.)";
4584
+ try {
4585
+ const r = await o.ai.chat({
4586
+ model: o.advisor,
4587
+ messages: [
4588
+ { role: "system", content: "You advise a developer mid-task. Answer the question directly and concretely: the plan, the data shape, the risks, or the defects with their fix. Be brief. Do not write the whole solution." },
4589
+ { role: "user", content: `## User request
4590
+ ${o.request()}${state}
4591
+
4592
+ ## Question
4593
+ ${question}` }
4594
+ ],
4595
+ maxTokens: 2e3
4596
+ });
4597
+ return ((await textOf(r)).trim() || "No advice: the advisor returned nothing.") + review;
4598
+ } catch (err) {
4599
+ log8.warn("advisor call failed:", String(err));
4600
+ return `No advice: the advisor call failed (${String(err?.message ?? err).slice(0, 120)}). Continue with your own judgment.`;
4601
+ }
4602
+ }
4603
+ };
4604
+ const gate = (tools) => tools.map((t) => !WRITE_TOOLS.has(t.name) ? t : { ...t, run: async (...a) => used ? t.run(...a) : PLAN_FIRST });
4605
+ return { tool, gate, used: () => used };
4606
+ }
4607
+ function meterWorkers(ai, main) {
4608
+ let total;
4609
+ let resolved;
4610
+ const seen = () => {
4611
+ resolved = ai.modelUsed ?? resolved;
4612
+ };
4613
+ const add = (u) => {
4614
+ if (!u) return;
4615
+ const t = total ??= { promptTokens: 0, completionTokens: 0, totalTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
4616
+ t.promptTokens += u.promptTokens ?? 0;
4617
+ t.completionTokens += u.completionTokens ?? 0;
4618
+ t.totalTokens += u.totalTokens ?? 0;
4619
+ t.cacheCreationTokens += u.cacheCreationTokens ?? 0;
4620
+ t.cacheReadTokens += u.cacheReadTokens ?? 0;
4621
+ };
4622
+ async function* tap(it) {
4623
+ let last;
4624
+ try {
4625
+ for await (const c of it) {
4626
+ if (c?.usage) last = c.usage;
4627
+ yield c;
4628
+ }
4629
+ } finally {
4630
+ add(last);
4631
+ seen();
4632
+ }
4633
+ }
4634
+ const chat = async (o) => {
4635
+ const r = await ai.chat(o);
4636
+ if (o?.model === main) return r;
4637
+ if (r && typeof r[Symbol.asyncIterator] === "function") return tap(r);
4638
+ add(r?.usage);
4639
+ seen();
4640
+ return r;
4641
+ };
4642
+ return { ai: new Proxy(ai, { get: (t, k) => k === "chat" ? chat : Reflect.get(t, k) }), usage: () => total && { ...total }, model: () => resolved, reset: () => {
4643
+ total = void 0;
4644
+ resolved = void 0;
4645
+ } };
4646
+ }
4647
+
4503
4648
  // src/lint.ts
4504
4649
  var CODE_RE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
4505
4650
  function checkSyntax(path, content) {
@@ -4579,7 +4724,7 @@ function reasoningToChatFragment(model, effort) {
4579
4724
  }
4580
4725
 
4581
4726
  // src/Agent.ts
4582
- var log8 = forComponent("Agent");
4727
+ var log9 = forComponent("Agent");
4583
4728
  function envInt(key, fallback) {
4584
4729
  const n = typeof process !== "undefined" ? Number(process.env?.[key]) : NaN;
4585
4730
  return Number.isFinite(n) && n >= 0 ? n : fallback;
@@ -4708,6 +4853,13 @@ var AgentOptions = class {
4708
4853
  hooks;
4709
4854
  /** If true: add the `Task` tool so the agent can spawn depth-limited child agents over the VFS. */
4710
4855
  subagents = false;
4856
+ /** DELEGATE MODE: `model` plans, `worker` writes. The planner's Write/Edit/MultiEdit/ApplyEdits/bash/Shell are
4857
+ * refused at execution; Task/TaskBatch are on (maxDepth 1, children default to `worker`) and children get the
4858
+ * parent's `postToolUse` too. Off when the models are equal or either is cursor/*. Wins over `advisor`. */
4859
+ delegate;
4860
+ /** ADVISOR MODE: `model` does the work; an `Advise` tool asks `advisor.model` (≤`limit`, default 3) with only
4861
+ * the request + `snapshot()` (host state, optional) + the question. The first write waits for one consult. */
4862
+ advisor;
4711
4863
  /** VFS dir of typed-subagent defs (`<dir>/<name>.md`). If set with `subagents`: inject a catalog + enable the `Task` `agentType` param. */
4712
4864
  agentsDir;
4713
4865
  /** Current recursion depth (0 = top-level); spawned children run at depth+1. */
@@ -4816,6 +4968,11 @@ var Agent = class _Agent {
4816
4968
  lastTrimNotified = 0;
4817
4969
  // last auto-trim drop count surfaced via host.notify (dedup)
4818
4970
  activeTools = [];
4971
+ meter;
4972
+ // delegate/advisor: meters the second model's calls
4973
+ modeModel;
4974
+ lastTask = "";
4975
+ // the current turn's text — what the advisor reads as the request
4819
4976
  /** claude-code bridge for agentx-only tools (ccBridge.ts) + the current step's executor it calls. */
4820
4977
  ccServer;
4821
4978
  ccExec;
@@ -4999,7 +5156,7 @@ var Agent = class _Agent {
4999
5156
  const disk = new NodeDiskFilesystem2(process.cwd());
5000
5157
  await disk.init();
5001
5158
  this.options.fs = new JailedFilesystem2(disk);
5002
- log8.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
5159
+ log9.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
5003
5160
  }
5004
5161
  this.buildCtx();
5005
5162
  }
@@ -5016,6 +5173,12 @@ var Agent = class _Agent {
5016
5173
  const fs = o.fs;
5017
5174
  let systemPrompt = o.systemPrompt;
5018
5175
  let tools = o.tools;
5176
+ this.ctx.model = o.model;
5177
+ const worker = o.delegate && delegateWorkerFor(o.model, o.delegate.worker);
5178
+ const advisor = !worker && o.advisor ? advisorFor(o.model, o.advisor.model) : void 0;
5179
+ this.meter = worker || advisor ? meterWorkers(o.ai, o.model) : void 0;
5180
+ const ai = this.meter?.ai ?? o.ai;
5181
+ const adv = advisor ? makeAdvisor({ ai, advisor, request: () => this.lastTask, snapshot: o.advisor.snapshot, limit: o.advisor.limit }) : void 0;
5019
5182
  if (o.instructionFiles !== false) {
5020
5183
  const names = Array.isArray(o.instructionFiles) ? o.instructionFiles : void 0;
5021
5184
  const ins = await loadInstructions(fs, names);
@@ -5046,7 +5209,7 @@ var Agent = class _Agent {
5046
5209
  if (tool) tools = [...tools, tool];
5047
5210
  }
5048
5211
  if (o.host && o.askUserQuestion) tools = [...tools, askUserQuestionTool];
5049
- if (o.subagents) {
5212
+ if (o.subagents || worker) {
5050
5213
  let agents;
5051
5214
  if (o.agentsDir) {
5052
5215
  const loaded = await loadAgents(fs, o.agentsDir);
@@ -5054,17 +5217,18 @@ var Agent = class _Agent {
5054
5217
  if (loaded.catalog) systemPrompt += "\n\n" + loaded.catalog;
5055
5218
  }
5056
5219
  const taskOpts = {
5057
- ai: o.ai,
5058
- model: o.model,
5220
+ ai,
5221
+ model: worker || o.model,
5059
5222
  fs,
5060
5223
  depth: o.depth,
5061
- maxDepth: o.maxDepth,
5224
+ maxDepth: worker ? Math.min(o.maxDepth, o.depth + 1) : o.maxDepth,
5062
5225
  agents,
5226
+ inheritPostToolUse: !!worker,
5063
5227
  hooks: o.hooks,
5064
5228
  traceDir: o.traceDir,
5065
5229
  permissions: o.permissions,
5066
5230
  // a child obeys the parent's allow/deny rules (else Task is a one-call bypass)
5067
- tools: [...o.tools, ...this.injectedTools],
5231
+ tools: worker ? delegateTools([...o.tools, ...this.injectedTools], o.model) : [...o.tools, ...this.injectedTools],
5068
5232
  systemPrompt: o.systemPrompt,
5069
5233
  // A child never resumes the parent's own claude-code session (the host's `resume` link is the parent's).
5070
5234
  providerOptionsFor: o.providerOptionsFor ?? ((m) => modelProvider(m) === modelProvider(o.model) && o.providerOptions ? { ...o.providerOptions, resume: void 0, onClaudeCodeSession: void 0 } : void 0)
@@ -5077,6 +5241,15 @@ var Agent = class _Agent {
5077
5241
  if (plan) tools = [...tools, plan.tool];
5078
5242
  this.activeHooks = composeHooks(o.hooks, plan?.hooks, o.permissions?.hooks());
5079
5243
  this.activeTools = [...tools, ...this.injectedTools];
5244
+ if (worker) {
5245
+ this.activeTools = delegateTools(this.activeTools, o.model);
5246
+ systemPrompt += delegatePrompt(worker);
5247
+ }
5248
+ if (adv) {
5249
+ this.activeTools = [...adv.gate(this.activeTools), adv.tool];
5250
+ systemPrompt += advisorPrompt(advisor);
5251
+ }
5252
+ this.modeModel = worker || advisor;
5080
5253
  this.systemPromptCache = systemPrompt;
5081
5254
  this.prepared = true;
5082
5255
  return systemPrompt;
@@ -5089,6 +5262,7 @@ var Agent = class _Agent {
5089
5262
  this.started = false;
5090
5263
  this.invokedTools.clear();
5091
5264
  this.fabricatedClaims.clear();
5265
+ this.lastTask = contentText(task);
5092
5266
  const systemPrompt = await this.prepare(contentText(task));
5093
5267
  const startCtx = await this.fireSessionStart();
5094
5268
  const userContent = await this.applyPromptSubmit(task);
@@ -5124,6 +5298,7 @@ var Agent = class _Agent {
5124
5298
  */
5125
5299
  async send(task) {
5126
5300
  await this.ensureFs();
5301
+ this.lastTask = contentText(task);
5127
5302
  const systemPrompt = await this.prepare(contentText(task));
5128
5303
  const startCtx = await this.fireSessionStart();
5129
5304
  const userContent = await this.applyPromptSubmit(task);
@@ -5174,6 +5349,12 @@ var Agent = class _Agent {
5174
5349
  if (next !== this.transcript) this.transcript = next;
5175
5350
  }
5176
5351
  async runLoop() {
5352
+ this.meter?.reset();
5353
+ const res = await this.loop();
5354
+ const workerUsage = this.meter?.usage();
5355
+ return workerUsage ? { ...res, workerModel: this.meter.model() ?? this.modeModel, workerUsage } : res;
5356
+ }
5357
+ async loop() {
5177
5358
  const o = this.options;
5178
5359
  const wireTools = toWireTools(this.activeTools);
5179
5360
  const useStream = o.stream === true && typeof o.host?.notify === "function";
@@ -5193,7 +5374,7 @@ var Agent = class _Agent {
5193
5374
  let lastProgressAt = start;
5194
5375
  const turnUser = this.transcript[this.transcript.length - 1];
5195
5376
  const kill = (finishReason) => {
5196
- log8.warn(`kill-switch: ${finishReason} (steps=${steps}, tokens=${usage.totalTokens}, budgetTokens=${Math.round(spent)}, ms=${Date.now() - start - this.parkedMs}${finishReason === "timeout" ? `, idle=${Date.now() - lastProgressAt - this.parkedMs}ms` : ""}${this.parkedMs ? ` +${this.parkedMs} parked` : ""})`);
5377
+ log9.warn(`kill-switch: ${finishReason} (steps=${steps}, tokens=${usage.totalTokens}, budgetTokens=${Math.round(spent)}, ms=${Date.now() - start - this.parkedMs}${finishReason === "timeout" ? `, idle=${Date.now() - lastProgressAt - this.parkedMs}ms` : ""}${this.parkedMs ? ` +${this.parkedMs} parked` : ""})`);
5197
5378
  this.jobs.killAll(`run ended: ${finishReason}`, true);
5198
5379
  const claims = this.survivingClaims();
5199
5380
  return { text: lastAssistantText(this.transcript, turnUser), steps, finishReason, messages: this.transcript, usage, usageEstimated, ...finishReason === "budget" ? { budget: { spent: Math.round(spent), limit: o.maxTokens, stepPrompt } } : {}, ...claims.length ? { fabricatedToolClaims: claims } : {} };
@@ -5219,9 +5400,9 @@ var Agent = class _Agent {
5219
5400
  this.nativeHoldMs = o.nativeHoldMs;
5220
5401
  if (isCursorWithTools) {
5221
5402
  if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
5222
- log8.warn(`providerOptions.toolTimeoutMs=${hostToolTimeoutMs}ms is below what the registered tools need \u2014 RAISED to ${effectiveToolTimeoutMs}ms. ${toolTimeoutViolations(this.activeTools, hostToolTimeoutMs).length} tool(s) could outlive it, and the transport would have destroyed their results. To lower it, lower the tools' own maxDurationMs (e.g. the Shell tool's maxTimeoutMs) instead.`);
5403
+ log9.warn(`providerOptions.toolTimeoutMs=${hostToolTimeoutMs}ms is below what the registered tools need \u2014 RAISED to ${effectiveToolTimeoutMs}ms. ${toolTimeoutViolations(this.activeTools, hostToolTimeoutMs).length} tool(s) could outlive it, and the transport would have destroyed their results. To lower it, lower the tools' own maxDurationMs (e.g. the Shell tool's maxTimeoutMs) instead.`);
5223
5404
  }
5224
- for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log8.warn(v);
5405
+ for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log9.warn(v);
5225
5406
  }
5226
5407
  const hostExec = async (name, args) => {
5227
5408
  const tc = { id: `host-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
@@ -5280,7 +5461,7 @@ var Agent = class _Agent {
5280
5461
  } catch (err) {
5281
5462
  if (!this.imagesOff && /does not support .*input/i.test(String(err?.message ?? "")) && sent.some((m) => Array.isArray(m.content) && m.role === "tool")) {
5282
5463
  this.imagesOff = true;
5283
- log8.warn(`model refused image input (${err?.message}) \u2014 resending with image Reads as text stubs`);
5464
+ log9.warn(`model refused image input (${err?.message}) \u2014 resending with image Reads as text stubs`);
5284
5465
  sent = this.trimContext();
5285
5466
  attempt--;
5286
5467
  continue;
@@ -5292,11 +5473,11 @@ var Agent = class _Agent {
5292
5473
  const empty = err?.code === "empty" || err?.code === "cursor_silent";
5293
5474
  const maxAttempts = empty ? Math.max(o.emptyRetries, 0) : 2;
5294
5475
  const replaysTool = stalled && this.toolRanThisAttempt;
5295
- if (replaysTool) log8.warn("model stall after tool(s) already ran this step \u2014 not retrying (a retry would replay them)");
5476
+ if (replaysTool) log9.warn("model stall after tool(s) already ran this step \u2014 not retrying (a retry would replay them)");
5296
5477
  const transient = !o.signal?.aborted && !isAbortError(err) && !replaysTool && attempt < maxAttempts && (network || serverSide || stalled || empty);
5297
5478
  if (!transient) throw err;
5298
5479
  const waitMs = (empty ? o.emptyRetryBaseMs : 1e3) * (attempt + 1);
5299
- log8.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err?.message ?? err}) \u2014 retrying in ${waitMs}ms`);
5480
+ log9.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err?.message ?? err}) \u2014 retrying in ${waitMs}ms`);
5300
5481
  o.host?.notify?.({ kind: "retry", message: `${empty ? "model returned nothing" : stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
5301
5482
  await new Promise((r) => setTimeout(r, waitMs));
5302
5483
  }
@@ -5304,12 +5485,12 @@ var Agent = class _Agent {
5304
5485
  } catch (err) {
5305
5486
  if (err?.code === "budget") return kill("budget");
5306
5487
  if (err?.code === "stall") {
5307
- log8.warn(`model stalled out after retries: ${err?.message ?? err}`);
5488
+ log9.warn(`model stalled out after retries: ${err?.message ?? err}`);
5308
5489
  return { ...kill("timeout"), providerFinishReason: "none (stalled)", error: err };
5309
5490
  }
5310
5491
  if (o.signal?.aborted || isAbortError(err)) return kill("aborted");
5311
5492
  if (err?.code === "empty" || err?.code === "cursor_silent") {
5312
- log8.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
5493
+ log9.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
5313
5494
  const pfr = err?.providerFinishReason;
5314
5495
  return { text: "", steps, finishReason: "empty", ...pfr ? { providerFinishReason: pfr } : {}, messages: this.transcript, usage, usageEstimated, error: err };
5315
5496
  }
@@ -5322,13 +5503,13 @@ var Agent = class _Agent {
5322
5503
  }
5323
5504
  if (bodyStr && err instanceof Error && !err.message.includes(bodyStr)) err.detail = bodyStr;
5324
5505
  const errLog = err instanceof Error ? { message: err.message, ...err.detail ? { detail: err.detail } : {}, stack: err.stack } : err;
5325
- log8.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
5506
+ log9.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
5326
5507
  return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err };
5327
5508
  }
5328
5509
  if (o.signal?.aborted) return kill("aborted");
5329
5510
  if (res.finishReason === "error" && !res.toolCalls?.length) {
5330
5511
  const err = new Error(contentText(res.content ?? "").trim() || "provider error");
5331
- log8.error(`chat() failed: ${err.message}`);
5512
+ log9.error(`chat() failed: ${err.message}`);
5332
5513
  return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err };
5333
5514
  }
5334
5515
  if (!res.usage) {
@@ -5396,21 +5577,21 @@ var Agent = class _Agent {
5396
5577
  if (emptyTurn && o.failEmptyTurns) {
5397
5578
  if (!emptyNudged && canNudge()) {
5398
5579
  emptyNudged = true;
5399
- log8.warn(`empty model turn after tool work (step ${steps}, provider finishReason=${res.finishReason ?? "n/a"}) \u2014 nudging once`);
5580
+ log9.warn(`empty model turn after tool work (step ${steps}, provider finishReason=${res.finishReason ?? "n/a"}) \u2014 nudging once`);
5400
5581
  this.transcript.push({ role: "user", content: "Continue \u2014 you returned nothing. Answer the user now using the tool results above, or keep working if you are not done." });
5401
5582
  continue;
5402
5583
  }
5403
- log8.warn(`model returned empty turns after tool work \u2014 surfacing as failed`);
5584
+ log9.warn(`model returned empty turns after tool work \u2014 surfacing as failed`);
5404
5585
  const e = new Error("empty_response: model returned an empty turn (no text, no tools) after a re-prompt");
5405
5586
  e.code = "empty";
5406
5587
  return { text: "", steps, finishReason: "empty", ...res.finishReason ? { providerFinishReason: res.finishReason } : {}, messages: this.transcript, usage, usageEstimated, error: e };
5407
5588
  }
5408
5589
  const fabricated = o.catchFabricatedToolUse && !closureNudged ? this.fabricatedToolNames(contentText(res.content ?? "")) : [];
5409
5590
  for (const n of fabricated) this.fabricatedClaims.add(n);
5410
- if (fabricated.length && !canNudge()) log8.warn(`fabricated tool use: text names ${fabricated.join(", ")} but no step/budget left to demand the call \u2014 finishing with the existing answer`);
5591
+ if (fabricated.length && !canNudge()) log9.warn(`fabricated tool use: text names ${fabricated.join(", ")} but no step/budget left to demand the call \u2014 finishing with the existing answer`);
5411
5592
  if (fabricated.length && canNudge()) {
5412
5593
  closureNudged = true;
5413
- log8.warn(`fabricated tool use: text names ${fabricated.join(", ")} but the run invoked ${this.invokedTools.size ? [...this.invokedTools].join(", ") : "nothing"} \u2014 demanding the real call`);
5594
+ log9.warn(`fabricated tool use: text names ${fabricated.join(", ")} but the run invoked ${this.invokedTools.size ? [...this.invokedTools].join(", ") : "nothing"} \u2014 demanding the real call`);
5414
5595
  this.transcript.push({
5415
5596
  role: "user",
5416
5597
  content: `You referred to ${fabricated.map((n) => `\`${n}\``).join(", ")} in your reply, but you never actually called ${fabricated.length > 1 ? "those tools" : "that tool"} \u2014 no tool call was made, so any result you reported for ${fabricated.length > 1 ? "them" : "it"} was invented. Either call ${fabricated.length > 1 ? "them" : "it"} for real now, or answer without ${fabricated.length > 1 ? "them" : "it"} and do NOT claim any outcome \u2014 including a failure or a timeout \u2014 for a call you did not make.`
@@ -5419,14 +5600,14 @@ var Agent = class _Agent {
5419
5600
  }
5420
5601
  if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && canNudge()) {
5421
5602
  closureNudged = true;
5422
- log8.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
5603
+ log9.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
5423
5604
  this.transcript.push({
5424
5605
  role: "user",
5425
5606
  content: "You ran tools but did not reply. Answer the user now using the tool results above; if a tool failed, say so plainly. Do NOT re-run those tools, do NOT narrate what you did, and do NOT ask for permission to continue."
5426
5607
  });
5427
5608
  continue;
5428
5609
  }
5429
- log8.verbose(`completed in ${steps} step(s)`);
5610
+ log9.verbose(`completed in ${steps} step(s)`);
5430
5611
  const drained = await this.jobs.drain(o.signal);
5431
5612
  if (o.signal?.aborted) return kill("aborted");
5432
5613
  if (drained.length) lastProgressAt = Date.now();
@@ -5460,7 +5641,7 @@ var Agent = class _Agent {
5460
5641
  if (this.ctx.needsInput) {
5461
5642
  const question = this.ctx.needsInput;
5462
5643
  this.ctx.needsInput = void 0;
5463
- log8.warn(`needs_input: ${question.question} (steps=${steps})`);
5644
+ log9.warn(`needs_input: ${question.question} (steps=${steps})`);
5464
5645
  await this.jobs.drain();
5465
5646
  const text = [
5466
5647
  `OPEN QUESTION (run paused \u2014 no human was reachable): ${question.question}`,
@@ -5471,7 +5652,7 @@ var Agent = class _Agent {
5471
5652
  if (this.ctx.endTurn) {
5472
5653
  this.ctx.endTurn = void 0;
5473
5654
  if (this.drainInjections()) continue;
5474
- log8.verbose(`turn ended by a tool (steps=${steps})`);
5655
+ log9.verbose(`turn ended by a tool (steps=${steps})`);
5475
5656
  await this.jobs.drain(o.signal);
5476
5657
  return { text: "", steps, finishReason: "stop", messages: this.transcript, usage, usageEstimated };
5477
5658
  }
@@ -5509,12 +5690,12 @@ var Agent = class _Agent {
5509
5690
  if (this.toolHoldActive()) {
5510
5691
  const now3 = Date.now();
5511
5692
  const who = this.jobs.holding().map((j) => `${j.kind} "${j.label}" (${Math.round((now3 - j.startedAt) / 1e3)}s, quiet ${Math.round((now3 - j.lastActivityAt) / 1e3)}s)`).join(", ");
5512
- log8.warn(`stall watchdog: no stream output for ${stallMs}ms \u2014 held by ${who}`);
5693
+ log9.warn(`stall watchdog: no stream output for ${stallMs}ms \u2014 held by ${who}`);
5513
5694
  this.options.host?.notify?.({ kind: "tool_hold", message: `waiting on ${who}` });
5514
5695
  return poke();
5515
5696
  }
5516
5697
  const inFlight = this.jobs.list().filter((j) => j.status === "running");
5517
- log8.warn(`stall watchdog: no stream output for ${stallMs}ms and no tool holding the stream \u2014 aborting the request` + (inFlight.length ? ` (${inFlight.length} detached job(s) running, not awaited by the stream: ${inFlight.map((j) => `${j.id}/${j.kind}`).join(", ")})` : ""));
5698
+ log9.warn(`stall watchdog: no stream output for ${stallMs}ms and no tool holding the stream \u2014 aborting the request` + (inFlight.length ? ` (${inFlight.length} detached job(s) running, not awaited by the stream: ${inFlight.map((j) => `${j.id}/${j.kind}`).join(", ")})` : ""));
5518
5699
  fired = true;
5519
5700
  ac.abort(stallError());
5520
5701
  }, stallMs);
@@ -5684,13 +5865,13 @@ var Agent = class _Agent {
5684
5865
  const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
5685
5866
  if (decision?.block) {
5686
5867
  const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
5687
- log8.debug(`${tc.function.name} -> ${blocked}`);
5868
+ log9.debug(`${tc.function.name} -> ${blocked}`);
5688
5869
  await hooks?.postToolUse?.(call, blocked, meta);
5689
5870
  return blocked;
5690
5871
  }
5691
5872
  this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
5692
5873
  if (earlyError) {
5693
- log8.debug(`${tc.function.name} -> ${earlyError}`);
5874
+ log9.debug(`${tc.function.name} -> ${earlyError}`);
5694
5875
  await hooks?.postToolUse?.(call, earlyError, meta);
5695
5876
  this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
5696
5877
  return earlyError;
@@ -5699,12 +5880,12 @@ var Agent = class _Agent {
5699
5880
  let images;
5700
5881
  let threw = false;
5701
5882
  try {
5702
- log8.debug(`${tc.function.name}(${tc.function.arguments})`);
5883
+ log9.debug(`${tc.function.name}(${tc.function.arguments})`);
5703
5884
  this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
5704
5885
  try {
5705
5886
  hooks.onToolOutput(call, chunk, meta);
5706
5887
  } catch (e) {
5707
- log8.debug(`onToolOutput hook error: ${e}`);
5888
+ log9.debug(`onToolOutput hook error: ${e}`);
5708
5889
  }
5709
5890
  } : void 0;
5710
5891
  const raw = await tool.run(args, this.ctx);
@@ -5716,7 +5897,7 @@ var Agent = class _Agent {
5716
5897
  }
5717
5898
  } catch (e) {
5718
5899
  const msg = e instanceof Error ? e.message : String(e);
5719
- log8.debug(`${tc.function.name} -> error: ${msg}`);
5900
+ log9.debug(`${tc.function.name} -> error: ${msg}`);
5720
5901
  result = `Error: ${msg}`;
5721
5902
  threw = true;
5722
5903
  } finally {
@@ -5993,7 +6174,7 @@ function fitTokenBudget(messages, maxTokens, keepRecentImages = 1, vision = true
5993
6174
  const ids = callIdSet(messages.slice(from));
5994
6175
  while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
5995
6176
  if (total > maxTokens)
5996
- log8.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
6177
+ log9.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
5997
6178
  return [...head, ...messages.slice(from)];
5998
6179
  }
5999
6180
  function compact(m, max, focus) {
@@ -6678,7 +6859,7 @@ init_tools_web();
6678
6859
  init_tools();
6679
6860
  init_tools_structured();
6680
6861
  init_logging();
6681
- var log10 = forComponent("scratch");
6862
+ var log11 = forComponent("scratch");
6682
6863
  var SCRATCH_DIR = "/scratch";
6683
6864
  function shortArgs(args) {
6684
6865
  try {
@@ -6718,7 +6899,7 @@ var Scratch = class {
6718
6899
  await (this.dirReady ??= mkdirp(this.fs, dir));
6719
6900
  await this.fs.writeFile(path, header + raw);
6720
6901
  } catch (e) {
6721
- log10.debug("scratch write failed; returning raw", e);
6902
+ log11.debug("scratch write failed; returning raw", e);
6722
6903
  return raw;
6723
6904
  }
6724
6905
  const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
@@ -6748,7 +6929,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
6748
6929
  await (this.dirReady ??= mkdirp(this.fs, dir));
6749
6930
  await this.fs.writeFile(path, header + full);
6750
6931
  } catch (e) {
6751
- log10.debug("scratch spill failed; cropping lossy", e);
6932
+ log11.debug("scratch spill failed; cropping lossy", e);
6752
6933
  return full.slice(0, pageBytes) + `
6753
6934
 
6754
6935
  [output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
@@ -6794,7 +6975,7 @@ Question: ${q}`);
6794
6975
  const answer = (res.text ?? "").trim();
6795
6976
  return answer || "(no answer found in scratch)";
6796
6977
  } catch (e) {
6797
- log10.debug("Ask peek failed", e);
6978
+ log11.debug("Ask peek failed", e);
6798
6979
  return `Error querying scratch: ${e?.message ?? e}`;
6799
6980
  }
6800
6981
  }
@@ -6803,7 +6984,7 @@ Question: ${q}`);
6803
6984
 
6804
6985
  // src/lessons.ts
6805
6986
  init_logging();
6806
- var log11 = forComponent("Lessons");
6987
+ var log12 = forComponent("Lessons");
6807
6988
  var LessonOptionsDefaults = class {
6808
6989
  minRepeats = 2;
6809
6990
  };
@@ -6828,8 +7009,8 @@ function lessonCapture(options) {
6828
7009
  counts.set(lesson.slug, n);
6829
7010
  if (n < o.minRepeats) return;
6830
7011
  written.add(lesson.slug);
6831
- await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log11.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
6832
- log11.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
7012
+ await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log12.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
7013
+ log12.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
6833
7014
  }
6834
7015
  };
6835
7016
  }
@@ -6837,7 +7018,7 @@ function lessonCapture(options) {
6837
7018
  // src/reflect.ts
6838
7019
  init_llm();
6839
7020
  init_logging();
6840
- var log12 = forComponent("Reflect");
7021
+ var log13 = forComponent("Reflect");
6841
7022
  async function reflectOnRun(o) {
6842
7023
  const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
6843
7024
  if (!digest.trim()) return null;
@@ -6853,7 +7034,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
6853
7034
  const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
6854
7035
  text = r?.content ?? "";
6855
7036
  } catch (e) {
6856
- log12.warn(`reflection call failed: ${e?.message ?? e}`);
7037
+ log13.warn(`reflection call failed: ${e?.message ?? e}`);
6857
7038
  return null;
6858
7039
  }
6859
7040
  const m = text.match(/LESSON:\s*(.+)/i);
@@ -6863,10 +7044,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
6863
7044
  try {
6864
7045
  await writeFact(o.fs, o.dir, slug2, lesson);
6865
7046
  } catch (e) {
6866
- log12.warn(`could not persist lesson: ${e?.message ?? e}`);
7047
+ log13.warn(`could not persist lesson: ${e?.message ?? e}`);
6867
7048
  return null;
6868
7049
  }
6869
- log12.debug(`reflection persisted ${slug2}`);
7050
+ log13.debug(`reflection persisted ${slug2}`);
6870
7051
  return slug2;
6871
7052
  }
6872
7053
  function digestRun(messages, maxChars) {
@@ -6916,7 +7097,7 @@ function forComponent2(name) {
6916
7097
  }
6917
7098
 
6918
7099
  // node_modules/@bod.ee/voice/src/core/emotion.ts
6919
- var log13 = forComponent2("Emotion");
7100
+ var log14 = forComponent2("Emotion");
6920
7101
  var EMOTIONS = [
6921
7102
  // primary (best results)
6922
7103
  "neutral",
@@ -7070,7 +7251,7 @@ var EmotionStream = class {
7070
7251
  if (tag) {
7071
7252
  this.pending = tag;
7072
7253
  if (this.show) display += m[0];
7073
- } else log13.debug(`dropping unknown emotion tag ${m[0]}`);
7254
+ } else log14.debug(`dropping unknown emotion tag ${m[0]}`);
7074
7255
  last = m.index + m[0].length;
7075
7256
  }
7076
7257
  this.emit(body.slice(last), (s, d, p) => {
@@ -7217,7 +7398,7 @@ function computeHoldMs(ctx) {
7217
7398
  }
7218
7399
 
7219
7400
  // node_modules/@bod.ee/voice/src/core/engine.ts
7220
- var log14 = forComponent2("VoiceEngine");
7401
+ var log15 = forComponent2("VoiceEngine");
7221
7402
  var realClock = {
7222
7403
  now: () => performance.now(),
7223
7404
  setTimeout: (fn, ms) => setTimeout(fn, ms),
@@ -7605,8 +7786,8 @@ var VoiceEngine = class _VoiceEngine {
7605
7786
  this.tts.warmup?.();
7606
7787
  this.setState("listening");
7607
7788
  if (this.options.bargeInMode === "aec" && !this.stt.usingAec)
7608
- log14.warn("bargeInMode=aec but mic does not report echoCancellation applied \u2014 STT may transcribe the assistant");
7609
- log14.debug(`voice I/O up (${this.usingAec ? "AEC" : "heuristic echo"} capture, mode=${this.options.bargeInMode}, mic reports aec=${this.stt.usingAec})`);
7789
+ log15.warn("bargeInMode=aec but mic does not report echoCancellation applied \u2014 STT may transcribe the assistant");
7790
+ log15.debug(`voice I/O up (${this.usingAec ? "AEC" : "heuristic echo"} capture, mode=${this.options.bargeInMode}, mic reports aec=${this.stt.usingAec})`);
7610
7791
  }
7611
7792
  /** The RESOLVED barge-in tier (the single seam every tier branch reads). `bargeInMode` overrides
7612
7793
  * the mic's self-report — see the option doc for why the report is not trustworthy on mobile. */
@@ -7631,7 +7812,7 @@ var VoiceEngine = class _VoiceEngine {
7631
7812
  this.options.onDiag({ t: this.clock.now(), kind, ...fields });
7632
7813
  } catch (e) {
7633
7814
  this.diagOn = false;
7634
- log14.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
7815
+ log15.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
7635
7816
  }
7636
7817
  }
7637
7818
  idleWaiters = [];
@@ -7770,7 +7951,7 @@ var VoiceEngine = class _VoiceEngine {
7770
7951
  }
7771
7952
  for (const w of this.words(this.reply)) this.echoWords.add(w);
7772
7953
  this.tts.speak(forSpeech(speech), true);
7773
- if (!this.spokeDeltas && this.turnStartAt) log14.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
7954
+ if (!this.spokeDeltas && this.turnStartAt) log15.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
7774
7955
  this.spokeDeltas = true;
7775
7956
  this.setState("speaking");
7776
7957
  if (this.options.revealMode !== "off" && display) {
@@ -7883,13 +8064,13 @@ var VoiceEngine = class _VoiceEngine {
7883
8064
  this.drainTimer = null;
7884
8065
  this.speaking = false;
7885
8066
  this.stopWordReveal(true);
7886
- if (this.turnStartAt) log14.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
8067
+ if (this.turnStartAt) log15.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
7887
8068
  this.echoUntil = this.clock.now() + 2500;
7888
8069
  if (!this.usingAec) this.stt.reset();
7889
8070
  try {
7890
8071
  this.options.onSpeechEnd();
7891
8072
  } catch (e) {
7892
- log14.warn(`onSpeechEnd threw: ${e instanceof Error ? e.message : e}`);
8073
+ log15.warn(`onSpeechEnd threw: ${e instanceof Error ? e.message : e}`);
7893
8074
  }
7894
8075
  this.setState("listening");
7895
8076
  if (this.uttQueue.length) this.pumpQueue();
@@ -7963,7 +8144,7 @@ var VoiceEngine = class _VoiceEngine {
7963
8144
  if (!host) return pick;
7964
8145
  const ctx = { text: this.lastDispatchText, question: this.lastDispatchWasQuestion, barged: this.bargedBeforeDispatch };
7965
8146
  const fallback = (e) => {
7966
- log14.warn(`pickAck failed \u2014 using the default pools: ${e instanceof Error ? e.message : e}`);
8147
+ log15.warn(`pickAck failed \u2014 using the default pools: ${e instanceof Error ? e.message : e}`);
7967
8148
  pick.picked = false;
7968
8149
  return void 0;
7969
8150
  };
@@ -8053,7 +8234,7 @@ var VoiceEngine = class _VoiceEngine {
8053
8234
  this.ackProbe = null;
8054
8235
  const queued = this.uttQueue.splice(0);
8055
8236
  const droppedQueued = queued.length;
8056
- if (droppedQueued) log14.info(`barge-in cut ${droppedQueued} queued worker utterance(s) \u2014 handed to the host`);
8237
+ if (droppedQueued) log15.info(`barge-in cut ${droppedQueued} queued worker utterance(s) \u2014 handed to the host`);
8057
8238
  this.overlapKeptUntil = 0;
8058
8239
  if (!this.speaking && !this.drainTimer) {
8059
8240
  if (droppedQueued) this.lastInterrupted = { full: "", heard: "", queued, queuedTurn: false };
@@ -8282,7 +8463,7 @@ var VoiceEngine = class _VoiceEngine {
8282
8463
  if (flat && this.lastDispatchFlat && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS && (this.lastDispatchFlat.startsWith(flat) || flat.startsWith(this.lastDispatchFlat))) return;
8283
8464
  this.specSpent = true;
8284
8465
  this.specText = this.specPartial;
8285
- log14.debug(`speculate: "${this.specText.slice(0, 60)}"`);
8466
+ log15.debug(`speculate: "${this.specText.slice(0, 60)}"`);
8286
8467
  this.diag("speculate", { text: this.specText });
8287
8468
  this.options.onSpeculate(this.specText);
8288
8469
  }, this.options.speculativeMs);
@@ -8351,7 +8532,7 @@ var VoiceEngine = class _VoiceEngine {
8351
8532
  };
8352
8533
  this.tts.newContext();
8353
8534
  this.tts.speak(phrase, false);
8354
- log14.debug(`backchannel: "${phrase}"`);
8535
+ log15.debug(`backchannel: "${phrase}"`);
8355
8536
  this.diag("backchannel", { phrase });
8356
8537
  this.options.onBackchannel(phrase);
8357
8538
  }
@@ -8384,7 +8565,7 @@ var VoiceEngine = class _VoiceEngine {
8384
8565
  });
8385
8566
  if (idx < 0) return text;
8386
8567
  toks.splice(idx, 1);
8387
- log14.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
8568
+ log15.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
8388
8569
  return toks.join(" ");
8389
8570
  }
8390
8571
  /** Merge a resumed utterance into the pending one, deduping any word-overlap. Soniox re-finalizes
@@ -8570,7 +8751,7 @@ var VoiceEngine = class _VoiceEngine {
8570
8751
  }
8571
8752
  const bargeHold = !this.looksIncomplete(this.pendingUtt) && this.unfinishedBargeLine(this.pendingUtt);
8572
8753
  if (this.options.incompleteMergeMs && (bargeHold || this.looksIncomplete(this.pendingUtt))) {
8573
- log14.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
8754
+ log15.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
8574
8755
  this.diag("hold", { reason: bargeHold ? "barge_unfinished" : "incomplete", tail: this.pendingUtt.slice(-40) });
8575
8756
  this.options.onHold();
8576
8757
  if (this.options.holdFiller && !this.speaking) this.speakFiller(this.options.holdFiller);
@@ -8609,13 +8790,13 @@ var VoiceEngine = class _VoiceEngine {
8609
8790
  const answered = this.answeredSinceDispatch();
8610
8791
  const part = flat !== this.lastDispatchFlat && !answered && this.echoesDispatch(text);
8611
8792
  if (part || flat && flat === this.lastDispatchFlat && !answered && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS) {
8612
- log14.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
8793
+ log15.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
8613
8794
  this.diag("utterance_drop", { reason: "dup_final", text: text.slice(0, 60) });
8614
8795
  return;
8615
8796
  }
8616
8797
  const refinal = this.refinalizes(text);
8617
8798
  if (refinal && this.repliedSinceDispatch) {
8618
- log14.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
8799
+ log15.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
8619
8800
  this.diag("utterance_drop", { reason: "refinalized", text: text.slice(0, 60) });
8620
8801
  this.lastDispatchFlat = flat;
8621
8802
  this.lastDispatchWords = text.trim().split(/\s+/);
@@ -8894,7 +9075,7 @@ var SpokenSplitter = class {
8894
9075
  };
8895
9076
 
8896
9077
  // node_modules/@bod.ee/voice/src/adapters/soniox.ts
8897
- var log15 = forComponent2("SonioxSTT");
9078
+ var log16 = forComponent2("SonioxSTT");
8898
9079
  var now = () => performance.now();
8899
9080
  var SonioxSTTOptions = class {
8900
9081
  auth = "";
@@ -8939,7 +9120,7 @@ var SonioxSTT = class {
8939
9120
  this.onDiag({ t: now(), kind, ...fields });
8940
9121
  } catch (e) {
8941
9122
  this.diagOn = false;
8942
- log15.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
9123
+ log16.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
8943
9124
  }
8944
9125
  }
8945
9126
  lastChunkAt = 0;
@@ -8981,11 +9162,11 @@ var SonioxSTT = class {
8981
9162
  this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
8982
9163
  this.ws.onclose = (ev) => {
8983
9164
  if (this.stopped) return;
8984
- log15.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
9165
+ log16.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
8985
9166
  this.diag("stt_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: true });
8986
9167
  this.reset();
8987
9168
  this.connectWs().catch((e) => {
8988
- log15.error(`soniox reconnect failed: ${e.message}`);
9169
+ log16.error(`soniox reconnect failed: ${e.message}`);
8989
9170
  this.diag("stt_reconnect_failed", { message: e.message });
8990
9171
  });
8991
9172
  };
@@ -8997,7 +9178,7 @@ var SonioxSTT = class {
8997
9178
  this.endpointTimer = setInterval(() => {
8998
9179
  const combined = (this.finalText + this.partialText).trim();
8999
9180
  if (!combined || now() - this.lastChangeAt < this.options.silenceEndpointMs) return;
9000
- if (this.firstTokenAt) log15.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
9181
+ if (this.firstTokenAt) log16.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
9001
9182
  this.reset();
9002
9183
  this.onUtterance(combined, now());
9003
9184
  }, 120);
@@ -9009,7 +9190,7 @@ var SonioxSTT = class {
9009
9190
  if (this.stopped) return;
9010
9191
  const ref = this.lastChunkAt || this.startedChunksAt;
9011
9192
  if (now() - ref > noAudioMs) {
9012
- log15.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
9193
+ log16.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
9013
9194
  this.diag("stt_watchdog_fatal", { noAudioMs });
9014
9195
  this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
9015
9196
  this.stop();
@@ -9032,7 +9213,7 @@ var SonioxSTT = class {
9032
9213
  handle(m) {
9033
9214
  if (m.error_message) {
9034
9215
  this.diag("stt_error", { message: String(m.error_message), code: m.error_code });
9035
- return log15.error(`soniox: ${m.error_message}`);
9216
+ return log16.error(`soniox: ${m.error_message}`);
9036
9217
  }
9037
9218
  let endpoint = false;
9038
9219
  for (const t of m.tokens ?? []) {
@@ -9049,7 +9230,7 @@ var SonioxSTT = class {
9049
9230
  this.onPartial(combined);
9050
9231
  if (endpoint && this.finalText.trim()) {
9051
9232
  const utterance = this.finalText.trim();
9052
- if (this.firstTokenAt) log15.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
9233
+ if (this.firstTokenAt) log16.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
9053
9234
  this.reset();
9054
9235
  this.onUtterance(utterance, now());
9055
9236
  }
@@ -9071,7 +9252,7 @@ var SonioxSTT = class {
9071
9252
  };
9072
9253
 
9073
9254
  // node_modules/@bod.ee/voice/src/adapters/cartesia.ts
9074
- var log16 = forComponent2("CartesiaTTS");
9255
+ var log17 = forComponent2("CartesiaTTS");
9075
9256
  var now2 = () => performance.now();
9076
9257
  var CartesiaTTSOptions = class {
9077
9258
  auth = "";
@@ -9107,7 +9288,7 @@ var CartesiaTTS = class _CartesiaTTS {
9107
9288
  this.onDiag({ t: now2(), kind, ...fields });
9108
9289
  } catch (e) {
9109
9290
  this.diagOn = false;
9110
- log16.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
9291
+ log17.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
9111
9292
  }
9112
9293
  }
9113
9294
  firstAudioAt = 0;
@@ -9142,11 +9323,11 @@ var CartesiaTTS = class _CartesiaTTS {
9142
9323
  this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
9143
9324
  });
9144
9325
  this.ws.onclose = (ev) => {
9145
- log16.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
9326
+ log17.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
9146
9327
  this.diag("tts_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: !this.closed });
9147
9328
  if (!this.closed) {
9148
9329
  this.connecting = this.doConnect().catch((e) => {
9149
- log16.error(`cartesia reconnect failed: ${e.message}`);
9330
+ log17.error(`cartesia reconnect failed: ${e.message}`);
9150
9331
  this.diag("tts_reconnect_failed", { message: e.message });
9151
9332
  });
9152
9333
  }
@@ -9174,12 +9355,12 @@ var CartesiaTTS = class _CartesiaTTS {
9174
9355
  this.down = true;
9175
9356
  this.downAt = now2();
9176
9357
  this.consecutiveOk = 0;
9177
- log16.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
9358
+ log17.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
9178
9359
  this.diag("tts_breaker_open", { errors: this.consecutiveErrors });
9179
9360
  this.onDone();
9180
9361
  this.startProbe();
9181
9362
  } else if (!this.down) {
9182
- (/No valid transcripts/i.test(m.message || "") ? log16.debug : log16.warn)(`cartesia: ${JSON.stringify(m)}`);
9363
+ (/No valid transcripts/i.test(m.message || "") ? log17.debug : log17.warn)(`cartesia: ${JSON.stringify(m)}`);
9183
9364
  }
9184
9365
  }
9185
9366
  };
@@ -9194,7 +9375,7 @@ var CartesiaTTS = class _CartesiaTTS {
9194
9375
  this.stopProbe();
9195
9376
  const downMs = this.downAt ? now2() - this.downAt : 0;
9196
9377
  this.diag("tts_breaker_close", { downMs: Math.round(downMs) });
9197
- (downMs < 2e3 ? log16.debug : log16.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
9378
+ (downMs < 2e3 ? log17.debug : log17.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
9198
9379
  }
9199
9380
  /** Ensure the WS is open before sending — reconnects if idle-closed. */
9200
9381
  async ensureConnected() {
@@ -9277,13 +9458,13 @@ function base64ToBytes(b64) {
9277
9458
  }
9278
9459
 
9279
9460
  // node_modules/@bod.ee/voice/src/adapters/fish.ts
9280
- var log17 = forComponent2("FishTTS");
9461
+ var log18 = forComponent2("FishTTS");
9281
9462
 
9282
9463
  // node_modules/@bod.ee/voice/src/adapters/grok.ts
9283
- var log18 = forComponent2("GrokTTS");
9464
+ var log19 = forComponent2("GrokTTS");
9284
9465
 
9285
9466
  // node_modules/@bod.ee/voice/src/adapters/grok-stt.ts
9286
- var log19 = forComponent2("GrokSTT");
9467
+ var log20 = forComponent2("GrokSTT");
9287
9468
 
9288
9469
  // src/voice/engine.ts
9289
9470
  init_logging();
@@ -9299,7 +9480,7 @@ import { AIClient, DecideClient, JevAdapter, LlmDecideAdapter } from "ai.libx.js
9299
9480
 
9300
9481
  // src/voice/overlap.ts
9301
9482
  init_logging();
9302
- var log20 = forComponent("Overlap");
9483
+ var log21 = forComponent("Overlap");
9303
9484
  var norm2 = (s) => s.toLowerCase().replace(/[’']/g, "'").replace(/[^\p{L}\p{N}' -]+/gu, " ").replace(/\s+/g, " ").trim();
9304
9485
  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/;
9305
9486
  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(" "));
@@ -9379,8 +9560,8 @@ var OverlapJudge = class {
9379
9560
  const t0 = this.o.now();
9380
9561
  const done = (verdict, by) => {
9381
9562
  const d = { verdict, by, ms: Math.round(this.o.now() - t0) };
9382
- if (by === "late" || by === "error" || by === "none") log20.warn(`overlap "${heard.slice(0, 60)}": no verdict (${by}, ${d.ms}ms) \u2192 cut`);
9383
- else log20.debug(`overlap "${heard.slice(0, 60)}": ${verdict} by ${by} ${d.ms}ms`);
9563
+ if (by === "late" || by === "error" || by === "none") log21.warn(`overlap "${heard.slice(0, 60)}": no verdict (${by}, ${d.ms}ms) \u2192 cut`);
9564
+ else log21.debug(`overlap "${heard.slice(0, 60)}": ${verdict} by ${by} ${d.ms}ms`);
9384
9565
  this.o.onDecision(heard, d);
9385
9566
  return d;
9386
9567
  };
@@ -9397,7 +9578,7 @@ var OverlapJudge = class {
9397
9578
  });
9398
9579
  try {
9399
9580
  const v = await Promise.race([this.o.model(heard, ctl.signal).catch((e) => {
9400
- if (!ctl.signal.aborted) log20.warn(`overlap model failed: ${e.message}`);
9581
+ if (!ctl.signal.aborted) log21.warn(`overlap model failed: ${e.message}`);
9401
9582
  return "error";
9402
9583
  }), late]);
9403
9584
  if (v === "late" || v === "error") return done("cut", v);
@@ -9413,14 +9594,14 @@ function overlapModelFor(ai, env = process.env) {
9413
9594
  const m = fastOverlapModel(ai, model);
9414
9595
  const t0 = performance.now();
9415
9596
  void m("yeah", AbortSignal.timeout(1e4)).then(
9416
- () => log20.debug(`fast model warm in ${Math.round(performance.now() - t0)}ms`),
9417
- (e) => log20.warn(`fast model warm-up failed (the first overlap verdict may be late): ${e?.message ?? e}`)
9597
+ () => log21.debug(`fast model warm in ${Math.round(performance.now() - t0)}ms`),
9598
+ (e) => log21.warn(`fast model warm-up failed (the first overlap verdict may be late): ${e?.message ?? e}`)
9418
9599
  );
9419
9600
  return m;
9420
9601
  }
9421
9602
 
9422
9603
  // src/voice/instinct.ts
9423
- var log21 = forComponent("Instinct");
9604
+ var log22 = forComponent("Instinct");
9424
9605
  var ACK_SITUATIONS = {
9425
9606
  hard_question: `a question that takes real thought, judgment or looking something up ("what's the biggest takeaway?", "how should I structure this?")`,
9426
9607
  simple_question: 'a quick question with an instant, obvious answer ("how are you?", "what time is it?", "can you hear me?")',
@@ -9470,7 +9651,7 @@ function interruptionNote(cut, intent) {
9470
9651
 
9471
9652
  // src/voice/unsaid.ts
9472
9653
  init_logging();
9473
- var log22 = forComponent("Unsaid");
9654
+ var log23 = forComponent("Unsaid");
9474
9655
  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(" "));
9475
9656
  var contentStems = (s) => (s.toLowerCase().match(/[a-z0-9']+/g) ?? []).filter((w) => w.length >= 4 && !STOP2.has(w)).map((w) => w.slice(0, 4));
9476
9657
  var SAID_OVERLAP = 0.6;
@@ -9510,7 +9691,7 @@ async function unsaidClauses(pieces, spoken, decider, timeoutMs = 5e3) {
9510
9691
  return p < CONVEYED_MIN;
9511
9692
  }).map((x) => x.c);
9512
9693
  } catch (e) {
9513
- log22.warn(`unsaid judge failed \u2014 only near-zero-overlap clauses count as unsaid: ${e.message}`);
9694
+ log23.warn(`unsaid judge failed \u2014 only near-zero-overlap clauses count as unsaid: ${e.message}`);
9514
9695
  return fallback();
9515
9696
  } finally {
9516
9697
  clearTimeout(timer);
@@ -9631,7 +9812,7 @@ var RESULT_CLAIM_RE = new RegExp(LEAD_TAGS + String.raw`(?:(?:all\s+)?done|finis
9631
9812
  var DONE_CLAIM_RE = new RegExp(LEAD_TAGS + String.raw`(?:(?:all\s+)?done|finished|it['’]?s\s+(?:done|finished)|completed?)\b`, "i");
9632
9813
 
9633
9814
  // src/voice/channelLang.ts
9634
- var log23 = forComponent("ChannelBanks");
9815
+ var log24 = forComponent("ChannelBanks");
9635
9816
  var LINE_KINDS = ["backchannel", "filler", "bridge"];
9636
9817
  var BANK_KEYS = [...LINE_KINDS, "phatic"];
9637
9818
  var EN_PHATIC = [
@@ -9695,8 +9876,17 @@ function isPhatic(text, bank) {
9695
9876
  return !!q && (t === q || t.startsWith(q + " "));
9696
9877
  });
9697
9878
  }
9879
+ var CLOSER = new Set("ok okay k cool great nice perfect awesome amazing lovely excellent alright fine good thanks thank thx you so much very a lot bye goodbye cheers later see ya night got it i that that's thats is it's its all for now sounds understood noted makes sense oh ah wow right then just \u05EA\u05D5\u05D3\u05D4 \u05E8\u05D1\u05D4 \u05E1\u05D1\u05D1\u05D4 \u05D0\u05D5\u05E7\u05D9\u05D9 \u05D0\u05D5\u05E7\u05D9 \u05D1\u05E1\u05D3\u05E8 \u05D9\u05D5\u05E4\u05D9 \u05D0\u05D7\u05DC\u05D4 \u05DE\u05E2\u05D5\u05DC\u05D4 \u05DE\u05E6\u05D5\u05D9\u05DF \u05DE\u05D2\u05E0\u05D9\u05D1 \u05E0\u05D4\u05D3\u05E8 \u05D1\u05D9\u05D9 \u05DC\u05D4\u05EA\u05E8\u05D0\u05D5\u05EA \u05D8\u05D5\u05D1 \u05D6\u05D4\u05D5 \u05D6\u05D4 \u05D4\u05DB\u05DC \u05D1\u05D9\u05E0\u05EA\u05D9\u05D9\u05DD \u05D4\u05D1\u05E0\u05EA\u05D9 \u05DE\u05DE\u05E9 \u05DC\u05DA \u05D0\u05D6".split(" "));
9880
+ function turnIntent(text) {
9881
+ const t = text.trim();
9882
+ if (/\?[\s"')\]]*$/.test(t)) return "ask";
9883
+ const w = norm3(t).split(" ").filter(Boolean);
9884
+ const asks = requestOpener(t) && !/^ת/.test(lead(t)[0] ?? "") || pendingQuestion(t);
9885
+ if (w.length && w.length <= 8 && w.every((x) => CLOSER.has(x)) && !asks) return "closer";
9886
+ return asks || requestOpener(t) ? "ask" : "say";
9887
+ }
9698
9888
  var SKIP = new Set("um uh uhm erm hmm mm oh ok okay so no yes yeah yep well hey hi and but right alright actually like now then wait hang on me speaker \u05D0\u05D5\u05E7\u05D9\u05D9 \u05D8\u05D5\u05D1 \u05D0\u05D6 \u05DC\u05D0 \u05DB\u05DF \u05E8\u05D2\u05E2 \u05D0\u05D4 \u05D0\u05DE \u05D9\u05D0\u05DC\u05DC\u05D4 \u05D5".split(" "));
9699
- var ASK = "what whats how why where when who which whose is are was were do does did can could would will should shall have has may isnt arent dont doesnt didnt cant wont check open show tell find search look send run go lets please take give make write explain select focus order try compare list use set activate highlight outline capture start stop read play call book create add remove delete put type click close switch help get summarize translate mark keep remind schedule \u05DE\u05D4 \u05D0\u05D9\u05DA \u05DC\u05DE\u05D4 \u05D0\u05D9\u05E4\u05D4 \u05DE\u05EA\u05D9 \u05DE\u05D9 \u05D4\u05D0\u05DD \u05DB\u05DE\u05D4 \u05EA\u05DF \u05EA\u05E0\u05D9 \u05EA\u05D2\u05D9\u05D3 \u05EA\u05D2\u05D9\u05D3\u05D9 \u05D1\u05D5\u05D0 \u05D1\u05D5\u05D0\u05D9".split(" ");
9889
+ var ASK = "what whats how why where when who which whose is are was were do does did can could would will should shall have has may isnt arent dont doesnt didnt cant wont check open show tell find search look send run go lets please take give make write explain select focus order try compare list use set activate highlight outline capture start stop read play call book create add remove delete put type click close switch help get name recommend suggest summarize translate mark keep remind schedule \u05DE\u05D4 \u05D0\u05D9\u05DA \u05DC\u05DE\u05D4 \u05D0\u05D9\u05E4\u05D4 \u05DE\u05EA\u05D9 \u05DE\u05D9 \u05D4\u05D0\u05DD \u05DB\u05DE\u05D4 \u05EA\u05DF \u05EA\u05E0\u05D9 \u05EA\u05D2\u05D9\u05D3 \u05EA\u05D2\u05D9\u05D3\u05D9 \u05D1\u05D5\u05D0 \u05D1\u05D5\u05D0\u05D9".split(" ");
9700
9890
  var WANT = /^(i (want|need|would like|d like|wanna)|אני (רוצה|צריך|צריכה|מבקש|מבקשת))(\s|$)/u;
9701
9891
  var lead = (text) => {
9702
9892
  const w = text.toLowerCase().replace(/[^\p{L}\p{N}\s]+/gu, "").split(/\s+/).filter(Boolean);
@@ -9776,11 +9966,11 @@ function cleanLine(s, lang, maxWords = 6) {
9776
9966
  const raw = s.trim().split("\n")[0];
9777
9967
  const l = raw.replace(/^[-*•\d.)\s]+/, "").replace(/^["'“”«»]+|["'“”«»]+$/g, "").replace(new RegExp("^\\p{L}+:\\s*", "u"), "").trim();
9778
9968
  if (!l || isLeakSuspect(raw) || isLeakSuspect(l)) {
9779
- if (l) log23.debug(`line "${raw}" reads as meta \u2014 rejected`);
9969
+ if (l) log24.debug(`line "${raw}" reads as meta \u2014 rejected`);
9780
9970
  return "";
9781
9971
  }
9782
9972
  if (lang && !fitsLanguage(l, lang)) {
9783
- log23.debug(`line "${l}" is not ${lang} \u2014 rejected`);
9973
+ log24.debug(`line "${l}" is not ${lang} \u2014 rejected`);
9784
9974
  return "";
9785
9975
  }
9786
9976
  return l.split(/\s+/).length > maxWords || l.length > 60 ? "" : l;
@@ -9794,7 +9984,7 @@ function validateBank(raw, lang, minPerKind = 3) {
9794
9984
  const seen = /* @__PURE__ */ new Set();
9795
9985
  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)));
9796
9986
  if (out[k].length < minPerKind) {
9797
- log23.warn(`bank ${lang}: only ${out[k].length} valid ${k} lines \u2014 rejected`);
9987
+ log24.warn(`bank ${lang}: only ${out[k].length} valid ${k} lines \u2014 rejected`);
9798
9988
  return null;
9799
9989
  }
9800
9990
  }
@@ -9870,11 +10060,11 @@ var ChannelBanks = class {
9870
10060
  const f = this.file(lang);
9871
10061
  let cached = null;
9872
10062
  if (f) cached = await readFile(f, "utf8").then((s) => JSON.parse(s)).catch((e) => {
9873
- if (e.code !== "ENOENT") log23.warn(`bank cache ${f} unreadable \u2014 regenerating: ${e.message}`);
10063
+ if (e.code !== "ENOENT") log24.warn(`bank cache ${f} unreadable \u2014 regenerating: ${e.message}`);
9874
10064
  return null;
9875
10065
  });
9876
10066
  if (cached && cached.v !== BANK_VERSION) {
9877
- log23.debug(`bank ${lang}: cache v${cached.v ?? 1} predates v${BANK_VERSION} \u2014 regenerating`);
10067
+ log24.debug(`bank ${lang}: cache v${cached.v ?? 1} predates v${BANK_VERSION} \u2014 regenerating`);
9878
10068
  cached = null;
9879
10069
  }
9880
10070
  const valid = cached && validateBank(cached.bank, lang, 0);
@@ -9882,7 +10072,7 @@ var ChannelBanks = class {
9882
10072
  if (valid) {
9883
10073
  for (const k of BANK_KEYS) if (!valid[k].length) valid[k] = [...this.o.fallback[lang]?.[k] ?? []];
9884
10074
  this.banks.set(lang, valid);
9885
- log23.debug(`bank ${lang}: cache hit (${BANK_KEYS.map((k) => valid[k].length).join("/")})${complete ? "" : " \u2014 missing kinds, refreshing"}`);
10075
+ log24.debug(`bank ${lang}: cache hit (${BANK_KEYS.map((k) => valid[k].length).join("/")})${complete ? "" : " \u2014 missing kinds, refreshing"}`);
9886
10076
  }
9887
10077
  if (complete && this.o.now() - cached.at < this.o.refreshMs) return;
9888
10078
  await this.generate(lang, valid);
@@ -9901,11 +10091,11 @@ var ChannelBanks = class {
9901
10091
  bank[k] = [...fresh[k], ...have?.[k] ?? []].filter((x) => !seen.has(norm3(x)) && seen.add(norm3(x))).slice(0, this.o.maxPerKind);
9902
10092
  }
9903
10093
  this.banks.set(lang, bank);
9904
- log23.debug(`bank ${lang}: ${have ? "refreshed" : "generated"} (${BANK_KEYS.map((k) => bank[k].length).join("/")}) in ${Math.round(this.o.now() - t0)}ms`);
10094
+ log24.debug(`bank ${lang}: ${have ? "refreshed" : "generated"} (${BANK_KEYS.map((k) => bank[k].length).join("/")}) in ${Math.round(this.o.now() - t0)}ms`);
9905
10095
  const f = this.file(lang);
9906
- if (f) await mkdir(this.o.cacheDir, { recursive: true }).then(() => writeFile(f, JSON.stringify({ at: this.o.now(), v: BANK_VERSION, bank }))).catch((e) => log23.warn(`bank cache write ${f} failed: ${e.message}`));
10096
+ if (f) await mkdir(this.o.cacheDir, { recursive: true }).then(() => writeFile(f, JSON.stringify({ at: this.o.now(), v: BANK_VERSION, bank }))).catch((e) => log24.warn(`bank cache write ${f} failed: ${e.message}`));
9907
10097
  } catch (e) {
9908
- log23.warn(`bank ${lang}: generation failed \u2014 ${have ? "keeping the cached bank" : lang in this.o.fallback ? "fallback bank" : "silent"}: ${e.message}`);
10098
+ log24.warn(`bank ${lang}: generation failed \u2014 ${have ? "keeping the cached bank" : lang in this.o.fallback ? "fallback bank" : "silent"}: ${e.message}`);
9909
10099
  } finally {
9910
10100
  clearTimeout(t);
9911
10101
  if (!this.banks.has(lang)) setTimeout(() => this.pending.delete(lang), this.o.retryMs).unref?.();
@@ -10098,7 +10288,7 @@ var TurnGate = class {
10098
10288
  };
10099
10289
 
10100
10290
  // src/duplex.ts
10101
- var log24 = forComponent("DuplexAgent");
10291
+ var log25 = forComponent("DuplexAgent");
10102
10292
  function describeCall(call) {
10103
10293
  const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
10104
10294
  const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
@@ -10433,7 +10623,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10433
10623
  const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
10434
10624
  if (m) {
10435
10625
  this.fabricationCut = true;
10436
- log24.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
10626
+ log25.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
10437
10627
  this.reflexBuf = this.reflexBuf.slice(0, Math.max(m.index, this.reflexForwarded));
10438
10628
  this.pumpReflex(true);
10439
10629
  return;
@@ -10575,7 +10765,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10575
10765
  this.leakHit = true;
10576
10766
  this.leakCuts.push(leak.trim().slice(0, 40));
10577
10767
  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";
10578
- log24.warn(`reflex ${why} ("${leak.trim().slice(0, 40)}\u2026") \u2014 cut before TTS`);
10768
+ log25.warn(`reflex ${why} ("${leak.trim().slice(0, 40)}\u2026") \u2014 cut before TTS`);
10579
10769
  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" });
10580
10770
  const next = this.stepMarks.find((k) => k > at);
10581
10771
  if (next === void 0) {
@@ -10616,7 +10806,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10616
10806
  this.notify("diag", "leak_judge", { text: s.slice(0, 80), p: Math.round(p * 100) / 100, ms: Math.round(performance.now() - t0) });
10617
10807
  return p >= LEAK_JUDGE_THRESHOLD;
10618
10808
  }, (e) => {
10619
- log24.warn(`leak judge failed \u2014 speaking the sentence: ${e.message}`);
10809
+ log25.warn(`leak judge failed \u2014 speaking the sentence: ${e.message}`);
10620
10810
  return false;
10621
10811
  }).then((leak) => {
10622
10812
  clearTimeout(timer);
@@ -10688,7 +10878,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10688
10878
  let i = t.length - 1;
10689
10879
  while (i >= this.turnBase && !(t[i].role === "assistant" && typeof t[i].content === "string" && t[i].content.includes(key))) i--;
10690
10880
  if (i < this.turnBase) {
10691
- log24.warn("analysis leak cut, but its text was not found in the transcript to scrub");
10881
+ log25.warn("analysis leak cut, but its text was not found in the transcript to scrub");
10692
10882
  continue;
10693
10883
  }
10694
10884
  const m = t[i];
@@ -10745,7 +10935,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10745
10935
  const res = await this.sendWithContext(content, context, note);
10746
10936
  const fb = this.options.reflexFallbackModel, model = this.voice.options.model;
10747
10937
  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;
10748
- log24.warn(`reflex ${model} rate-limited \u2014 re-running the turn on ${fb}`);
10938
+ log25.warn(`reflex ${model} rate-limited \u2014 re-running the turn on ${fb}`);
10749
10939
  this.notify("diag", "reflex_fallback", { from: model, to: fb });
10750
10940
  this.voice.transcript.length = base;
10751
10941
  this.voice.options.model = fb;
@@ -10794,7 +10984,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10794
10984
  try {
10795
10985
  await this.sendWithContext(reminder, this.lastContext);
10796
10986
  } catch (e) {
10797
- log24.warn(`follow-through nudge failed: ${e instanceof Error ? e.message : e}`);
10987
+ log25.warn(`follow-through nudge failed: ${e instanceof Error ? e.message : e}`);
10798
10988
  } finally {
10799
10989
  const i = this.voice.transcript.findIndex((m, k) => k >= base && m.role === "user" && m.content === reminder);
10800
10990
  if (i >= 0) this.voice.transcript.splice(i, 1);
@@ -10855,7 +11045,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10855
11045
  return {
10856
11046
  preToolUse: (call) => {
10857
11047
  if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
10858
- log24.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
11048
+ log25.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
10859
11049
  this.abortSpeculation();
10860
11050
  return { block: true, reason: "Speculative turn aborted." };
10861
11051
  }
@@ -10937,7 +11127,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10937
11127
  try {
10938
11128
  await this.sendWithContext(reminder, this.lastContext);
10939
11129
  } catch (e) {
10940
- log24.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
11130
+ log25.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
10941
11131
  } finally {
10942
11132
  this.nudging = false;
10943
11133
  const i = this.voice.transcript.findIndex((m, k) => k >= base && m.role === "user" && m.content === reminder);
@@ -10993,7 +11183,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
10993
11183
  const accepted = this.offerOpen && this.parkedRedeliver.length > 0 && this.parkedRedeliver.every((p) => p.task === this.offer.task) && (isOfferAcceptance(content) || isTrivialBarge(content));
10994
11184
  const judge = accepted ? void 0 : this.options.bargeIntent;
10995
11185
  return (accepted ? Promise.resolve("resume") : judge ? judge(content).catch((e) => {
10996
- log24.warn(`bargeIntent judge failed \u2014 using the allowlist: ${e.message}`);
11186
+ log25.warn(`bargeIntent judge failed \u2014 using the allowlist: ${e.message}`);
10997
11187
  return fallback();
10998
11188
  }) : Promise.resolve(fallback())).then((intent) => {
10999
11189
  this.notify("diag", "barge_intent", { intent, text: content.slice(0, 80) });
@@ -11196,7 +11386,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
11196
11386
  try {
11197
11387
  res = await this.sendWithContext(spec.text, this.lastContext);
11198
11388
  } catch (e) {
11199
- log24.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
11389
+ log25.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
11200
11390
  } finally {
11201
11391
  this.voice.options.signal = prevSignal;
11202
11392
  }
@@ -11210,7 +11400,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
11210
11400
  if (d === "abort") {
11211
11401
  if (this.voice.transcript.length > base) this.voice.transcript.length = base;
11212
11402
  this.speculativeAbortedCalls++;
11213
- log24.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
11403
+ log25.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
11214
11404
  if (this.spec === spec) this.spec = void 0;
11215
11405
  return res ?? empty;
11216
11406
  }
@@ -11596,7 +11786,7 @@ Another agent just implemented the above. Independently check the CURRENT state
11596
11786
  this.notify("task_verify", `task ${id}: verifying`, { id });
11597
11787
  const cres = await new Agent(checkerOpts).run(checkBrief);
11598
11788
  if (cres.finishReason !== "stop") {
11599
- log24.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
11789
+ log25.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
11600
11790
  this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
11601
11791
  }
11602
11792
  const sum = (a = 0, b = 0) => a + b;
@@ -11732,7 +11922,7 @@ Another agent just implemented the above. Independently check the CURRENT state
11732
11922
  rec.status = "done";
11733
11923
  rec.result = res.text;
11734
11924
  const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
11735
- log24.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
11925
+ log25.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
11736
11926
  this.notify("task_done", `task ${id} (${rec.label}) completed`, {
11737
11927
  id,
11738
11928
  text: res.text,
@@ -11766,14 +11956,14 @@ Another agent just implemented the above. Independently check the CURRENT state
11766
11956
  foldIfSuperseded(rec, note) {
11767
11957
  if (rec.deliveryParked !== "moved_on") return false;
11768
11958
  if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
11769
- log24.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
11959
+ log25.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
11770
11960
  return true;
11771
11961
  }
11772
11962
  failTask(rec, msg) {
11773
11963
  this.dropAsk(rec.id);
11774
11964
  rec.status = "error";
11775
11965
  rec.result = msg;
11776
- log24.warn(`task ${rec.id} failed: ${msg}`);
11966
+ log25.warn(`task ${rec.id} failed: ${msg}`);
11777
11967
  this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`, { id: rec.id });
11778
11968
  if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
11779
11969
  this.dropParked(rec);
@@ -12077,7 +12267,7 @@ Another agent just implemented the above. Independently check the CURRENT state
12077
12267
  init_llm();
12078
12268
  init_logging();
12079
12269
  init_models();
12080
- var log25 = forComponent("ModelRouter");
12270
+ var log26 = forComponent("ModelRouter");
12081
12271
  var ModelRouterOptions = class {
12082
12272
  classifier = jevClassifier();
12083
12273
  /** Capability tiers → model id (any provider). basic/standard run the main session; premium is only ever an offload. */
@@ -12161,10 +12351,10 @@ var ModelRouter = class {
12161
12351
  if (tier === "premium") d.offload = this.state.offload && (c.sameThread ?? 0) >= o.sameThreadAt ? "continue" : "fresh";
12162
12352
  } catch (e) {
12163
12353
  const msg = e instanceof Error ? e.message : String(e);
12164
- log25.warn(`classifier failed, falling back to standard: ${msg}`);
12354
+ log26.warn(`classifier failed, falling back to standard: ${msg}`);
12165
12355
  d = { tier: "standard", model: o.models.standard, probs: {}, fallback: msg, classifyMs: Math.round(performance.now() - t0) };
12166
12356
  }
12167
- log25.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`);
12357
+ log26.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`);
12168
12358
  return d;
12169
12359
  }
12170
12360
  /** Run one user turn on `agent` with routing. Returns a RunResult carrying `costUsd` + `routing`. */
@@ -12216,7 +12406,7 @@ ${text}`;
12216
12406
  const answer = res.text || `(premium sub-agent finished with no reply; finishReason=${res.finishReason})`;
12217
12407
  agent.transcript.push({ role: "user", content }, { role: "assistant", content: answer });
12218
12408
  this.state.offload = { messages: child.transcript, mainLen: agent.transcript.length, question: text };
12219
- log25.info(`offload ${cont ? "continue" : "fresh"} done \xB7 ${res.finishReason} \xB7 ${res.steps} steps`);
12409
+ log26.info(`offload ${cont ? "continue" : "fresh"} done \xB7 ${res.finishReason} \xB7 ${res.steps} steps`);
12220
12410
  const costUsd = o.costOf(main, briefUsage) + o.costOf(o.models.premium, res.usage);
12221
12411
  return { ...res, text: answer, messages: agent.transcript, usage: addUsage(briefUsage, res.usage), costUsd, routing: d };
12222
12412
  }
@@ -12232,7 +12422,7 @@ The request:
12232
12422
  ${text}`);
12233
12423
  return { text: r.text.trim() || text, usage: r.usage };
12234
12424
  } catch (e) {
12235
- log25.warn(`brief failed, sending the raw request: ${e instanceof Error ? e.message : String(e)}`);
12425
+ log26.warn(`brief failed, sending the raw request: ${e instanceof Error ? e.message : String(e)}`);
12236
12426
  return { text };
12237
12427
  } finally {
12238
12428
  agent.transcript = snap;
@@ -12277,7 +12467,7 @@ async function jevAvailable(opts = {}) {
12277
12467
  const { env } = await loadJev(opts);
12278
12468
  return JEV_KEYS.some((k) => env[k]?.trim());
12279
12469
  } catch (e) {
12280
- log25.debug(`jev unavailable: ${e instanceof Error ? e.message : String(e)}`);
12470
+ log26.debug(`jev unavailable: ${e instanceof Error ? e.message : String(e)}`);
12281
12471
  return false;
12282
12472
  }
12283
12473
  }
@@ -12562,7 +12752,7 @@ init_models();
12562
12752
 
12563
12753
  // src/voice/progress.ts
12564
12754
  init_logging();
12565
- var log26 = forComponent("Progress");
12755
+ var log27 = forComponent("Progress");
12566
12756
  var HE_RESULT_CLAIM = /(?:^|\s)(?:מצאתי|סיימתי|בוצע|הנה|אין)(?=\s|$|[.,!…])/u;
12567
12757
  var TOPICS = [
12568
12758
  [/toolsearch|^mcp__.*__(list_commands|call_info)$/, null],
@@ -12707,7 +12897,7 @@ var Progress = class {
12707
12897
  }, this.o.generateBudgetMs);
12708
12898
  });
12709
12899
  const raw = await Promise.race([this.o.generate(tool, argsHint(args), lang, ac.signal).catch((e) => {
12710
- log26.warn(`progress line failed: ${e?.message ?? e}`);
12900
+ log27.warn(`progress line failed: ${e?.message ?? e}`);
12711
12901
  return null;
12712
12902
  }), late]).finally(() => clearTimeout(t));
12713
12903
  const l = cleanLine(raw ?? "", lang, 5);
@@ -12729,7 +12919,7 @@ init_logging();
12729
12919
 
12730
12920
  // src/voice/backchannelPlan.ts
12731
12921
  init_logging();
12732
- var log27 = forComponent("BackchannelPlan");
12922
+ var log28 = forComponent("BackchannelPlan");
12733
12923
  var silentTurn = (p) => p.density === "none" || p.turn === "short_command" || p.turn === "question";
12734
12924
  var cueWords = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}'\s]+/gu, " ").split(/\s+/).filter(Boolean);
12735
12925
  function matchCue(plan, heard, from = 0) {
@@ -12747,7 +12937,7 @@ function matchCue(plan, heard, from = 0) {
12747
12937
  }
12748
12938
 
12749
12939
  // src/voice/secondChannel.ts
12750
- var log28 = forComponent("SecondChannel");
12940
+ var log29 = forComponent("SecondChannel");
12751
12941
  var CHANNEL_MODES = {
12752
12942
  turn: { kinds: ["bridge"], bridgeDelayMs: 1500 },
12753
12943
  normal: {},
@@ -12832,6 +13022,9 @@ var SecondChannelOptions = class {
12832
13022
  /** Duplex v3 (§3 first sound): a bridge skips the decider (the veto stays for backchannels/fillers) and speaks a
12833
13023
  * canned line at once; a generated one is started at dispatch and used only if it already landed. */
12834
13024
  instantBridge = false;
13025
+ /** Duplex v3 §3 late bridge: a turn that expects an answer (not a closer) and got no bridge yet gets one canned bridge
13026
+ * this long after the user's last word if channel-1 is still silent — the short turns the 0ms rule skips ("Hi"). 0 = off. */
13027
+ lateBridgeMs = 0;
12835
13028
  /** No new interim for this long → a pause (hosts may also call onPause). */
12836
13029
  fillerPauseMs = 700;
12837
13030
  fillerMinWords = 4;
@@ -12861,15 +13054,15 @@ function withBudget(p, ms, what, ac) {
12861
13054
  let t;
12862
13055
  const late = new Promise((r) => {
12863
13056
  t = setTimeout(() => {
12864
- log28.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
13057
+ log29.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
12865
13058
  ac?.abort();
12866
13059
  r(null);
12867
13060
  }, ms);
12868
13061
  });
12869
13062
  return Promise.race([p.catch((e) => {
12870
13063
  const msg = String(e?.message ?? e);
12871
- if (e?.name === "AbortError" || /aborted/i.test(msg)) log28.debug(`${what}: aborted after its budget`);
12872
- else log28.warn(`${what} failed \u2014 falling back: ${msg}`);
13064
+ if (e?.name === "AbortError" || /aborted/i.test(msg)) log29.debug(`${what}: aborted after its budget`);
13065
+ else log29.warn(`${what} failed \u2014 falling back: ${msg}`);
12873
13066
  return null;
12874
13067
  }), late]).finally(() => clearTimeout(t));
12875
13068
  }
@@ -12895,6 +13088,11 @@ var SecondChannel = class {
12895
13088
  pauseTimer = null;
12896
13089
  bcTimer = null;
12897
13090
  bridgeTimer = null;
13091
+ lateTimer = null;
13092
+ late = false;
13093
+ // the bridge moment being considered is the late one (no length rule)
13094
+ lastWordAgo = 0;
13095
+ // the dispatched turn: ms since the user's last word (anchors lateBridgeMs)
12898
13096
  lastAt = -Infinity;
12899
13097
  lastKind = null;
12900
13098
  live = null;
@@ -12943,7 +13141,7 @@ var SecondChannel = class {
12943
13141
  if (host) this.pinned = true;
12944
13142
  const lang = primaryLang(tag);
12945
13143
  if (!lang || lang === this.lang) return;
12946
- log28.debug(`language ${this.lang} \u2192 ${lang}`);
13144
+ log29.debug(`language ${this.lang} \u2192 ${lang}`);
12947
13145
  this.lang = lang;
12948
13146
  this.recent = [];
12949
13147
  this.picks.clear();
@@ -13013,9 +13211,11 @@ var SecondChannel = class {
13013
13211
  if (s === this.main) return;
13014
13212
  this.main = s;
13015
13213
  if (s === "first_audio" || s === "speaking") this.live = null;
13016
- if (s !== "queued" && s !== "thinking" && this.bridgeTimer) {
13017
- clearTimeout(this.bridgeTimer);
13018
- this.bridgeTimer = null;
13214
+ if (s !== "queued" && s !== "thinking") {
13215
+ for (const k of ["bridgeTimer", "lateTimer"]) if (this[k]) {
13216
+ clearTimeout(this[k]);
13217
+ this[k] = null;
13218
+ }
13019
13219
  }
13020
13220
  if (s === "idle") {
13021
13221
  this.heard = "";
@@ -13036,7 +13236,7 @@ var SecondChannel = class {
13036
13236
  this.userSpeaking = false;
13037
13237
  this.bridged = false;
13038
13238
  if (this.lastKind === "bridge") this.lastKind = null;
13039
- for (const k of ["pauseTimer", "bcTimer", "bridgeTimer"]) {
13239
+ for (const k of ["pauseTimer", "bcTimer", "bridgeTimer", "lateTimer"]) {
13040
13240
  if (this[k]) clearTimeout(this[k]);
13041
13241
  this[k] = null;
13042
13242
  }
@@ -13047,11 +13247,21 @@ var SecondChannel = class {
13047
13247
  void this.consider("working");
13048
13248
  }, wait);
13049
13249
  else void this.consider("working");
13250
+ const late = this.o.lateBridgeMs - this.lastWordAgo;
13251
+ if (this.o.lateBridgeMs > 0) this.lateTimer = setTimeout(() => {
13252
+ this.lateTimer = null;
13253
+ if (this.bridged || this.busy || turnIntent(this.heard) === "closer" || this.blocked("bridge")) return;
13254
+ this.late = true;
13255
+ void this.consider("working").finally(() => {
13256
+ this.late = false;
13257
+ });
13258
+ }, Math.max(late, wait + 1));
13050
13259
  }
13051
13260
  }
13052
13261
  /** The user's final line was dispatched to the main agent (its words are the bridge's micro-context). */
13053
- onUserTurn(text) {
13262
+ onUserTurn(text, lastWordAgoMs = 0) {
13054
13263
  this.heard = text.trim();
13264
+ this.lastWordAgo = Math.max(0, lastWordAgoMs);
13055
13265
  this.floorWords = this.carried + words2(this.heard).length;
13056
13266
  this.lastUser = this.heard;
13057
13267
  this.detect(this.heard);
@@ -13076,7 +13286,7 @@ var SecondChannel = class {
13076
13286
  try {
13077
13287
  this.o.cancel();
13078
13288
  } catch (e) {
13079
- log28.warn(`cancel threw: ${e.message}`);
13289
+ log29.warn(`cancel threw: ${e.message}`);
13080
13290
  }
13081
13291
  }
13082
13292
  }
@@ -13119,12 +13329,19 @@ var SecondChannel = class {
13119
13329
  }
13120
13330
  /** Why this turn gets no bridge by rule ('' = it may) — language, a quick/phatic turn, or blocked(). */
13121
13331
  bridgeBlock() {
13122
- const w = words2(this.heard);
13332
+ const w = words2(this.heard), intent = turnIntent(this.heard);
13123
13333
  if (!this.lang) return "language unknown";
13124
- if (w.length <= 3 || w.length <= 6 && isPhatic(this.heard, this.bank)) return w.length <= 3 ? "quick turn" : "phatic";
13125
- if (w.length < this.o.bridgeMinWords) return `under ${this.o.bridgeMinWords} words`;
13334
+ if (intent === "closer") return "no answer due";
13335
+ if (this.late) return this.blocked("bridge");
13336
+ const why = intent === "ask" ? w.length <= 6 && isPhatic(this.heard, this.bank) ? "phatic" : "" : this.shortTurn(w.length);
13337
+ if (why) return this.o.lateBridgeMs > 0 ? `${why} (late bridge armed)` : why;
13126
13338
  return this.blocked("bridge");
13127
13339
  }
13340
+ shortTurn(n) {
13341
+ if (n <= 3 || n <= 6 && isPhatic(this.heard, this.bank)) return n <= 3 ? "quick turn" : "phatic";
13342
+ if (n < this.o.bridgeMinWords) return `under ${this.o.bridgeMinWords} words`;
13343
+ return "";
13344
+ }
13128
13345
  bcTimes = [];
13129
13346
  capped() {
13130
13347
  if (!this.o.maxPerWindow) return false;
@@ -13136,8 +13353,9 @@ var SecondChannel = class {
13136
13353
  rule(trigger, pauseMs = 0, breath = false, cue = null) {
13137
13354
  const w = words2(this.heard);
13138
13355
  if (!this.lang) return "silence";
13356
+ if (trigger === "working") return this.bridgeBlock() ? "silence" : "bridge";
13139
13357
  if (w.length <= 3 || w.length <= 6 && isPhatic(this.heard, this.bank)) return "silence";
13140
- const k = trigger === "interim" ? this.backchannelMoment(w.length, breath) && (!this.o.anticipate || !this.plan || !silentTurn(this.plan) && (!!cue || !this.o.cueOnly && this.plan.density !== "low")) ? "backchannel" : null : trigger === "pause" ? pauseMs >= this.o.fillerPauseMs && w.length >= this.o.fillerMinWords ? "filler" : null : w.length >= this.o.bridgeMinWords ? "bridge" : null;
13358
+ const k = trigger === "interim" ? this.backchannelMoment(w.length, breath) && (!this.o.anticipate || !this.plan || !silentTurn(this.plan) && (!!cue || !this.o.cueOnly && this.plan.density !== "low")) ? "backchannel" : null : trigger === "pause" ? pauseMs >= this.o.fillerPauseMs && w.length >= this.o.fillerMinWords ? "filler" : null : null;
13141
13359
  return k && this.allowed(k) ? k : "silence";
13142
13360
  }
13143
13361
  /** Plan (or, with `heard`, revise) the backchannels of the user's turn — async, never on the hot path. */
@@ -13180,7 +13398,7 @@ var SecondChannel = class {
13180
13398
  const opts = this.options;
13181
13399
  if (opts.mode === "chatty" && !opts.aec && !this.degradedLogged) {
13182
13400
  this.degradedLogged = true;
13183
- log28.info("chatty mode without echo cancellation \u2014 normal timing (our lines would re-trigger the mic)");
13401
+ log29.info("chatty mode without echo cancellation \u2014 normal timing (our lines would re-trigger the mic)");
13184
13402
  }
13185
13403
  const rule = this.rule(trigger, pauseMs, breath, cue);
13186
13404
  if (rule === "silence") {
@@ -13199,7 +13417,7 @@ var SecondChannel = class {
13199
13417
  const d = await withBudget(this.o.decide(this.tail(), { ...this.state(pauseMs), rule }, ac.signal), this.o.decisionBudgetMs, "channel decision", ac);
13200
13418
  choice = d?.choice;
13201
13419
  if (choice && choice in CHANNEL_ACTIONS && (choice === "silence" || this.allowed(choice))) kind = choice;
13202
- else if (choice && choice in CHANNEL_ACTIONS) log28.debug(`decider chose ${choice} for a ${rule} moment \u2014 the rule stands`);
13420
+ else if (choice && choice in CHANNEL_ACTIONS) log29.debug(`decider chose ${choice} for a ${rule} moment \u2014 the rule stands`);
13203
13421
  }
13204
13422
  if (kind === "silence") return ev({ choice, acted: "silence" });
13205
13423
  if (stale()) return ev({ choice, kind, acted: "stale" });
@@ -13237,7 +13455,7 @@ var SecondChannel = class {
13237
13455
  void withBudget(this.o.generate(ctx, ac.signal), Math.max(this.o.generateBudgetMs, this.o.bridgeDelayMs), "bridge generate", ac).then((raw) => {
13238
13456
  const text = cleanLine(raw ?? "", ctx.lang);
13239
13457
  if (text && gen === this.gen) this.bridgeDraft = { gen, text };
13240
- }).catch((e) => log28.debug(`bridge prefetch failed: ${e?.message ?? e}`));
13458
+ }).catch((e) => log29.debug(`bridge prefetch failed: ${e?.message ?? e}`));
13241
13459
  }
13242
13460
  async line(kind, react = true) {
13243
13461
  if (kind === "bridge" && this.o.instantBridge) {
@@ -13253,7 +13471,7 @@ var SecondChannel = class {
13253
13471
  if (raw === null) ac.abort();
13254
13472
  const text = cleanLine(raw ?? "", this.lang);
13255
13473
  if (text && !this.recent.some((r) => norm3(r) === norm3(text))) return text;
13256
- if (text) log28.debug(`generated "${text}" repeats a recent line \u2014 canned`);
13474
+ if (text) log29.debug(`generated "${text}" repeats a recent line \u2014 canned`);
13257
13475
  }
13258
13476
  return this.pick(kind);
13259
13477
  }
@@ -13275,7 +13493,7 @@ var SecondChannel = class {
13275
13493
  try {
13276
13494
  r = this.o.speak(text, kind);
13277
13495
  } catch (e) {
13278
- log28.warn(`speak threw \u2014 line dropped: ${e.message}`);
13496
+ log29.warn(`speak threw \u2014 line dropped: ${e.message}`);
13279
13497
  return false;
13280
13498
  }
13281
13499
  if (r === false) return false;
@@ -13288,7 +13506,7 @@ var SecondChannel = class {
13288
13506
  this.o.show(text, kind);
13289
13507
  const p = Promise.resolve(r).then(() => {
13290
13508
  }, (e) => {
13291
- log28.warn(`channel line playback failed: ${e?.message ?? e}`);
13509
+ log29.warn(`channel line playback failed: ${e?.message ?? e}`);
13292
13510
  }).finally(() => {
13293
13511
  if (this.inflight === p) this.inflight = null;
13294
13512
  });
@@ -13346,7 +13564,7 @@ function closedQuestion(reply) {
13346
13564
  }
13347
13565
 
13348
13566
  // src/voice/call.ts
13349
- var log29 = forComponent("VoiceCall");
13567
+ var log30 = forComponent("VoiceCall");
13350
13568
  var VoiceCallOptions = class {
13351
13569
  /** Host runs MAIN with exactly `text` (a replace: roll the `replaces` turn back first). */
13352
13570
  sendTurn = () => {
@@ -13404,8 +13622,11 @@ var VoiceCall = class {
13404
13622
  aec: o.aec,
13405
13623
  // v3 §3: first sound <1.2s p50 from the last word — the endpoint takes ~0.6s and the bridge's TTS ~0.2s, so the bridge
13406
13624
  // goes 300ms after the turn if MAIN is still silent (it always is that early: warm MAIN's first token is ≥1s), no veto
13625
+ // A short question gets that bridge too (by intent, not length); any other turn expecting an answer that got none
13626
+ // gets a late one at 1.3s from the last word while MAIN is still silent (API first byte runs 1-8s); a closer, none.
13407
13627
  bridgeDelayMs: 300,
13408
13628
  instantBridge: true,
13629
+ lateBridgeMs: 1300,
13409
13630
  ...this.lang ? { lang: this.lang } : {},
13410
13631
  ...o.channel,
13411
13632
  // the ledger: a turn's bridge moment that stayed silent, and why (its wait then has no first sound until MAIN speaks)
@@ -13569,7 +13790,7 @@ var VoiceCall = class {
13569
13790
  try {
13570
13791
  this.o.cancel();
13571
13792
  } catch (e) {
13572
- log29.warn(`cancel threw: ${e.message}`);
13793
+ log30.warn(`cancel threw: ${e.message}`);
13573
13794
  }
13574
13795
  if (this.cur?.running) {
13575
13796
  this.cur.running = false;
@@ -13629,7 +13850,7 @@ var VoiceCall = class {
13629
13850
  if (this.cur) this.close(this.cur, t.replaces === this.cur.id ? "replaced" : void 0);
13630
13851
  this.follow(t.text);
13631
13852
  this.clearProgress();
13632
- this.cur = { id: t.id, text: t.text, at, finalAt, lastWord, running: true, streamed: false, sounded: false, worded: false, done: false, marks: { sent: now3 }, closed: false };
13853
+ this.cur = { id: t.id, text: t.text, at, finalAt, lastWord, running: true, streamed: false, sounded: false, worded: false, done: false, marks: { sent: now3 }, closed: false, due: turnIntent(t.text) !== "closer" };
13633
13854
  const early = this.earlyHeard;
13634
13855
  this.earlyHeard = null;
13635
13856
  if (early && early.at >= at) {
@@ -13639,7 +13860,7 @@ var VoiceCall = class {
13639
13860
  this.progress.turn();
13640
13861
  this.o.onMetric({ name: "turn", id: t.id, kind: t.kind, text: t.text });
13641
13862
  this.o.sendTurn(t.text, { id: t.id, ...t.replaces ? { replaces: t.replaces } : {} });
13642
- if (t.kind === "new") this.fill.onUserTurn(t.text);
13863
+ if (t.kind === "new") this.fill.onUserTurn(t.text, now3 - at);
13643
13864
  }
13644
13865
  onDrop(text, why) {
13645
13866
  if (!this.gate.holding) {
@@ -13656,7 +13877,7 @@ var VoiceCall = class {
13656
13877
  try {
13657
13878
  r = this.o.speak(text, kind);
13658
13879
  } catch (e) {
13659
- log29.warn(`speak threw: ${e.message}`);
13880
+ log30.warn(`speak threw: ${e.message}`);
13660
13881
  return false;
13661
13882
  }
13662
13883
  if (r === false) return false;
@@ -13678,7 +13899,7 @@ var VoiceCall = class {
13678
13899
  sounded(c, by, at = this.o.now()) {
13679
13900
  if (c.sounded) return;
13680
13901
  c.sounded = true;
13681
- this.o.onMetric({ name: "first_sound", id: c.id, ms: Math.round(at - c.at), by });
13902
+ this.o.onMetric(c.due ? { name: "first_sound", id: c.id, ms: Math.round(at - c.at), by } : { name: "first_sound", id: c.id, by, kind: "no answer due" });
13682
13903
  }
13683
13904
  /** The turn is over (answered + audible, replaced, the next turn, the call's end): its timeline, once. A turn whose MAIN
13684
13905
  * never became audible still reports first_word — with no ms and why (cut / replaced / silent) — so every turn has one. */
@@ -13698,7 +13919,8 @@ var VoiceCall = class {
13698
13919
  mainText: rel(m.mainText),
13699
13920
  mainAudio: rel(m.mainAudio),
13700
13921
  lastWord: c.lastWord,
13701
- end: c.end
13922
+ end: c.end,
13923
+ ...c.due ? {} : { due: false }
13702
13924
  };
13703
13925
  for (const k of Object.keys(tl)) if (tl[k] === void 0) delete tl[k];
13704
13926
  this.o.onMetric({ name: "timeline", id: c.id, text: c.text, timeline: tl });
@@ -13735,6 +13957,7 @@ var VoiceCall = class {
13735
13957
  init_llm();
13736
13958
  import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor as CommandExecutor2, registerHeadlessCommands as registerHeadlessCommands2 } from "@livx.cc/wcli/core";
13737
13959
  export {
13960
+ ADVISE_LIMIT,
13738
13961
  Agent,
13739
13962
  AgentOptions,
13740
13963
  BodDbFilesystem,
@@ -13765,6 +13988,9 @@ export {
13765
13988
  MountFilesystem,
13766
13989
  NodeDiskFilesystem,
13767
13990
  OverlayFilesystem,
13991
+ PLANNER_BLOCKED,
13992
+ PLANNER_REFUSAL,
13993
+ PLAN_FIRST,
13768
13994
  PROGRESS_TEMPLATES,
13769
13995
  PermissionOptions,
13770
13996
  PermissionPolicy,
@@ -13789,6 +14015,9 @@ export {
13789
14015
  VoiceCallOptions,
13790
14016
  VoiceEngine,
13791
14017
  VoiceEngineOptions,
14018
+ WRITE_TOOLS,
14019
+ advisorFor,
14020
+ advisorPrompt,
13792
14021
  applyEditsTool,
13793
14022
  askUserQuestionTool,
13794
14023
  bashTool,
@@ -13805,6 +14034,9 @@ export {
13805
14034
  cronMatches,
13806
14035
  decodeDdgUrl,
13807
14036
  defaultTools,
14037
+ delegatePrompt,
14038
+ delegateTools,
14039
+ delegateWorkerFor,
13808
14040
  diskAgentOptions,
13809
14041
  editTool,
13810
14042
  elideImages,
@@ -13844,6 +14076,7 @@ export {
13844
14076
  loadMemory,
13845
14077
  loadSkills,
13846
14078
  log,
14079
+ makeAdvisor,
13847
14080
  makeAskTool,
13848
14081
  makeContext,
13849
14082
  makeGlobTool,
@@ -13863,6 +14096,7 @@ export {
13863
14096
  messageHasImage,
13864
14097
  messageHasImageRef,
13865
14098
  messageHasInlineImage,
14099
+ meterWorkers,
13866
14100
  mintImageRef,
13867
14101
  mkdirp,
13868
14102
  modelShortLabel,