@livx.cc/agentx 0.99.71 → 0.99.73

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
@@ -18,8 +18,192 @@ var init_logging = __esm({
18
18
  }
19
19
  });
20
20
 
21
+ // src/imageWire.ts
22
+ import { readFileSync, writeFileSync, statSync, mkdtempSync, rmSync } from "fs";
23
+ import { spawnSync } from "child_process";
24
+ import { createHash } from "crypto";
25
+ import { tmpdir } from "os";
26
+ import { join } from "path";
27
+ import { PNG } from "pngjs";
28
+ import jpeg from "jpeg-js";
29
+ function imageSize(b) {
30
+ const u16 = (i) => b[i] << 8 | b[i + 1];
31
+ const le16 = (i) => b[i] | b[i + 1] << 8;
32
+ const u32 = (i) => (b[i] << 24 >>> 0) + (b[i + 1] << 16) + (b[i + 2] << 8) + b[i + 3];
33
+ if (b[0] === 137 && b[1] === 80 && b.length >= 24) return { w: u32(16), h: u32(20) };
34
+ if (b[0] === 71 && b[1] === 73 && b.length >= 10) return { w: le16(6), h: le16(8) };
35
+ if (b[0] === 255 && b[1] === 216) {
36
+ for (let i = 2; i + 9 < b.length; ) {
37
+ if (b[i] !== 255) return null;
38
+ const m = b[i + 1];
39
+ if (m >= 192 && m <= 207 && m !== 196 && m !== 200 && m !== 204) return { w: u16(i + 7), h: u16(i + 5) };
40
+ i += 2 + u16(i + 2);
41
+ }
42
+ return null;
43
+ }
44
+ if (b.length >= 30 && String.fromCharCode(...b.subarray(8, 12)) === "WEBP") {
45
+ const kind = String.fromCharCode(...b.subarray(12, 16));
46
+ if (kind === "VP8X") return { w: 1 + (b[24] | b[25] << 8 | b[26] << 16), h: 1 + (b[27] | b[28] << 8 | b[29] << 16) };
47
+ if (kind === "VP8 ") return { w: le16(26) & 16383, h: le16(28) & 16383 };
48
+ if (kind === "VP8L") {
49
+ const v = b[21] | b[22] << 8 | b[23] << 16 | b[24] << 24;
50
+ return { w: (v & 16383) + 1, h: (v >> 14 & 16383) + 1 };
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+ function imageNote(what, bytes, dims, why) {
56
+ return `[image ${what}${dims ? `, ${dims.w}x${dims.h}` : ""}, ${fmtBytes(bytes)} \u2014 not sent: ${why}]`;
57
+ }
58
+ function downscaleJs(buf, maxSide) {
59
+ let src;
60
+ try {
61
+ if (buf[0] === 137 && buf[1] === 80) src = PNG.sync.read(buf);
62
+ else if (buf[0] === 255 && buf[1] === 216) src = jpeg.decode(buf, { useTArray: true, maxMemoryUsageInMB: 1024 });
63
+ else return null;
64
+ } catch (e) {
65
+ log2.warn("pure-JS image decode failed", e);
66
+ return null;
67
+ }
68
+ const { width: sw, height: sh, data: s } = src;
69
+ const k = Math.min(1, maxSide / Math.max(sw, sh));
70
+ const w = Math.max(1, Math.round(sw * k)), h = Math.max(1, Math.round(sh * k));
71
+ const d = new Uint8Array(w * h * 4);
72
+ for (let y = 0; y < h; y++) {
73
+ const y0 = Math.floor(y * sh / h), y1 = Math.max(y0 + 1, Math.floor((y + 1) * sh / h));
74
+ for (let x = 0; x < w; x++) {
75
+ const x0 = Math.floor(x * sw / w), x1 = Math.max(x0 + 1, Math.floor((x + 1) * sw / w));
76
+ let r = 0, g = 0, b = 0, a = 0;
77
+ for (let yy = y0; yy < y1; yy++) for (let xx = x0; xx < x1; xx++) {
78
+ const i = (yy * sw + xx) * 4, al = s[i + 3];
79
+ r += s[i] * al;
80
+ g += s[i + 1] * al;
81
+ b += s[i + 2] * al;
82
+ a += al;
83
+ }
84
+ const o = (y * w + x) * 4, n = (y1 - y0) * (x1 - x0), bg = 255 * (n - a / 255);
85
+ d[o] = (r / 255 + bg) / n;
86
+ d[o + 1] = (g / 255 + bg) / n;
87
+ d[o + 2] = (b / 255 + bg) / n;
88
+ d[o + 3] = 255;
89
+ }
90
+ }
91
+ let out = Buffer.alloc(0);
92
+ for (const q of [80, 60, 40]) {
93
+ out = Buffer.from(jpeg.encode({ width: w, height: h, data: d }, q).data);
94
+ if (out.length <= IMAGE_MAX_BYTES) break;
95
+ }
96
+ return out;
97
+ }
98
+ function downscale(path, maxSide) {
99
+ const dir = mkdtempSync(join(tmpdir(), "agentx-img-"));
100
+ const out = join(dir, "x.jpg");
101
+ try {
102
+ const tries = [
103
+ ["sips", ["-Z", String(maxSide), "-s", "format", "jpeg", "-s", "formatOptions", "80", path, "--out", out]],
104
+ ["magick", [path, "-resize", `${maxSide}x${maxSide}>`, "-quality", "80", out]],
105
+ ["convert", [path, "-resize", `${maxSide}x${maxSide}>`, "-quality", "80", out]]
106
+ ];
107
+ for (const [cmd, args] of tries) {
108
+ const r = spawnSync(cmd, args, { stdio: "ignore", timeout: 15e3 });
109
+ if (r.status !== 0) continue;
110
+ try {
111
+ const o = readFileSync(out);
112
+ if (o.length <= IMAGE_MAX_BYTES) return o;
113
+ break;
114
+ } catch {
115
+ }
116
+ }
117
+ const js = downscaleJs(readFileSync(path), maxSide);
118
+ if (!js) log2.warn(`could not downscale ${path} (tried sips/magick/convert/pure JS)`);
119
+ return js;
120
+ } finally {
121
+ rmSync(dir, { recursive: true, force: true });
122
+ }
123
+ }
124
+ function remember(id, w) {
125
+ cache.delete(id);
126
+ cache.set(id, w);
127
+ if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
128
+ return w;
129
+ }
130
+ function wireOf(key, buf, mime, label, file) {
131
+ const dims = imageSize(buf);
132
+ const fits = buf.length <= IMAGE_MAX_BYTES && (!dims || Math.max(dims.w, dims.h) <= IMAGE_MAX_SIDE);
133
+ let small = null;
134
+ if (!fits) {
135
+ if (file) small = downscale(file, IMAGE_MAX_SIDE);
136
+ else {
137
+ const dir = mkdtempSync(join(tmpdir(), "agentx-img-in-"));
138
+ try {
139
+ const f = join(dir, `in.${mime.split("/")[1] || "img"}`);
140
+ writeFileSync(f, buf);
141
+ small = downscale(f, IMAGE_MAX_SIDE);
142
+ } finally {
143
+ rmSync(dir, { recursive: true, force: true });
144
+ }
145
+ }
146
+ }
147
+ const url = fits ? `data:${mime};base64,${buf.toString("base64")}` : small && small.length <= IMAGE_MAX_BYTES ? `data:image/jpeg;base64,${small.toString("base64")}` : null;
148
+ return { key, url, note: imageNote(label, buf.length, dims, url ? "this model can't see images" : "too large and it couldn't be downscaled") };
149
+ }
150
+ function imageForWire(path, mime, vision) {
151
+ let st;
152
+ try {
153
+ st = statSync(path);
154
+ } catch (e) {
155
+ log2.warn(`image unreadable: ${path}`, e);
156
+ return null;
157
+ }
158
+ const key = `${st.mtimeMs}:${st.size}`;
159
+ let hit = cache.get(path);
160
+ if (!hit || hit.key !== key) hit = remember(path, wireOf(key, readFileSync(path), mime, path, path));
161
+ return vision && hit.url ? { url: hit.url } : { note: hit.note };
162
+ }
163
+ function inlineImageForWire(url, vision) {
164
+ const comma = url.indexOf(",");
165
+ const mime = url.slice(5, url.indexOf(";"));
166
+ if (vision && url.length < 3e5) {
167
+ const b64 = url.slice(comma + 1);
168
+ const dims = imageSize(Buffer.from(b64.slice(0, 4096), "base64")) ?? imageSize(Buffer.from(b64, "base64"));
169
+ if (!dims || Math.max(dims.w, dims.h) <= IMAGE_MAX_SIDE) return { url };
170
+ }
171
+ const id = `inline:${createHash("sha1").update(url).digest("base64")}`;
172
+ let hit = cache.get(id);
173
+ if (!hit) {
174
+ const buf = Buffer.from(url.slice(comma + 1), "base64");
175
+ hit = remember(id, wireOf(id, buf, mime, `(${mime})`));
176
+ }
177
+ return vision && hit.url ? { url: hit.url } : { note: hit.note };
178
+ }
179
+ function imageWireBytes(path, mime, vision) {
180
+ const r = imageForWire(path, mime, vision);
181
+ return !r ? 0 : "url" in r ? r.url.length : r.note.length;
182
+ }
183
+ function scrubBase64Text(s) {
184
+ if (s.length < 2e3) return s;
185
+ const out = s.replace(DATA_URI, (m) => imageNote(m.slice(5, m.indexOf(";")), Math.floor((m.length - m.indexOf(",") - 1) * 3 / 4), null, "base64 is never sent as text")).replace(B64_RUN, (m) => !/[+/]/.test(m) || !/\d/.test(m) ? m : `[base64 data, ${fmtBytes(Math.floor(m.length * 3 / 4))} \u2014 not sent: base64 is never sent as text]`);
186
+ if (out !== s) log2.warn(`scrubbed base64 from request text (${s.length} \u2192 ${out.length} chars)`);
187
+ return out;
188
+ }
189
+ var log2, IMAGE_MAX_SIDE, IMAGE_MAX_BYTES, fmtBytes, cache, CACHE_MAX, DATA_URI, B64_RUN;
190
+ var init_imageWire = __esm({
191
+ "src/imageWire.ts"() {
192
+ "use strict";
193
+ init_logging();
194
+ log2 = forComponent("imageWire");
195
+ IMAGE_MAX_SIDE = 1568;
196
+ IMAGE_MAX_BYTES = 1e6;
197
+ fmtBytes = (n) => n >= 1e6 ? `${(n / 1e6).toFixed(1)} MB` : n >= 1e3 ? `${Math.round(n / 1e3)} KB` : `${n} B`;
198
+ cache = /* @__PURE__ */ new Map();
199
+ CACHE_MAX = 8;
200
+ DATA_URI = /data:image\/[\w.+-]+;base64,[A-Za-z0-9+/=]+/g;
201
+ B64_RUN = /[A-Za-z0-9+/=]{2000,}/g;
202
+ }
203
+ });
204
+
21
205
  // src/llm.ts
22
- import { readFileSync, statSync, realpathSync } from "fs";
206
+ import { statSync as statSync2, realpathSync } from "fs";
23
207
  import { resolve, sep } from "path";
24
208
  import { randomBytes } from "crypto";
25
209
  function passesBackReasoning(model) {
@@ -76,9 +260,9 @@ function mintImageRef(path, mime) {
76
260
  const token = randomBytes(16).toString("hex");
77
261
  let bytes = 0;
78
262
  try {
79
- bytes = Math.ceil(statSync(path).size * 4 / 3);
263
+ bytes = Math.ceil(statSync2(path).size * 4 / 3);
80
264
  } catch (e) {
81
- log2.warn(`image ref stat failed, weighing as 0: ${path}`, e);
265
+ log3.warn(`image ref stat failed, weighing as 0: ${path}`, e);
82
266
  }
83
267
  refRegistry.set(token, { mime, path, bytes });
84
268
  while (refRegistry.size > REF_REGISTRY_MAX) refRegistry.delete(refRegistry.keys().next().value);
@@ -94,7 +278,7 @@ function parseImageRef(url) {
94
278
  const parts = refUrlParts(url);
95
279
  const ref = parts && refRegistry.get(parts.token);
96
280
  if (!ref) {
97
- log2.debug("image ref not in registry \u2014 treating as non-image text");
281
+ log3.debug("image ref not in registry \u2014 treating as non-image text");
98
282
  return null;
99
283
  }
100
284
  return ref;
@@ -113,7 +297,7 @@ function remintImageRefs(messages, roots) {
113
297
  try {
114
298
  allowed.push(realpathSync(resolve(r)));
115
299
  } catch (e) {
116
- log2.debug(`image-ref root unavailable: ${r}`, e);
300
+ log3.debug(`image-ref root unavailable: ${r}`, e);
117
301
  }
118
302
  }
119
303
  if (allowed.length === 0) return messages;
@@ -130,11 +314,11 @@ function remintImageRefs(messages, roots) {
130
314
  try {
131
315
  real = realpathSync(f.path);
132
316
  } catch (e) {
133
- log2.warn(`resumed image ref unreadable, will stub: ${f.path}`, e);
317
+ log3.warn(`resumed image ref unreadable, will stub: ${f.path}`, e);
134
318
  return p;
135
319
  }
136
320
  if (!inside(real)) {
137
- log2.warn(`resumed image ref outside the allowed roots, will stub: ${f.path} -> ${real}`);
321
+ log3.warn(`resumed image ref outside the allowed roots, will stub: ${f.path} -> ${real}`);
138
322
  return p;
139
323
  }
140
324
  touched = true;
@@ -161,7 +345,7 @@ function toolImageRef(content) {
161
345
  try {
162
346
  return parseImageRef(JSON.parse(content).imageRef);
163
347
  } catch (e) {
164
- log2.debug("bad imageRef tool result", e);
348
+ log3.debug("bad imageRef tool result", e);
165
349
  return null;
166
350
  }
167
351
  }
@@ -189,32 +373,41 @@ function imageWindows(messages, keepRecent) {
189
373
  const k = Math.max(0, keepRecent);
190
374
  return /* @__PURE__ */ new Set([...attach.slice(Math.max(0, attach.length - k)), ...tool.slice(Math.max(0, tool.length - k))]);
191
375
  }
192
- function expandImagesForSend(messages, keepRecent = 1, stub = IMAGE_ELIDE_STUB) {
376
+ function expandImagesForSend(messages, keepRecent = 1, stub = IMAGE_ELIDE_STUB, vision = true) {
193
377
  if (!messages.some(bearsImage)) return messages;
194
378
  const keep = imageWindows(messages, keepRecent);
195
379
  let changed = false;
196
380
  const out = messages.map((m, i) => {
197
- const content = keep.has(i) ? expandContent(m.content, stub) : stripContent(m.content, stub);
381
+ const content = keep.has(i) ? expandContent(m.content, vision) : stripContent(m.content, stub);
198
382
  if (content === m.content) return m;
199
383
  changed = true;
200
384
  return { ...m, content };
201
385
  });
202
386
  return changed ? out : messages;
203
387
  }
204
- function sendBytes(messages, keepRecent = 1, stub = IMAGE_ELIDE_STUB) {
388
+ function sendBytes(messages, keepRecent = 1, vision = true, stub = IMAGE_ELIDE_STUB) {
205
389
  let n = 0;
206
- for (const b of sendBytesPer(messages, keepRecent, stub)) n += b;
390
+ for (const b of sendBytesPer(messages, keepRecent, vision, stub)) n += b;
207
391
  return n;
208
392
  }
209
- function sendBytesPer(messages, keepRecent = 1, stub = IMAGE_ELIDE_STUB) {
393
+ function sendBytesPer(messages, keepRecent = 1, vision = true, stub = IMAGE_ELIDE_STUB) {
210
394
  const keep = messages.some(bearsImage) ? imageWindows(messages, keepRecent) : null;
211
- return messages.map((m, i) => wireBytes(m.content, keep == null || keep.has(i), stub) + (m.tool_calls ? JSON.stringify(m.tool_calls).length : 0) + (m.reasoning_content?.length ?? 0));
395
+ return messages.map((m, i) => wireBytes(m.content, keep == null || keep.has(i), vision, stub) + (m.tool_calls ? JSON.stringify(m.tool_calls).length : 0) + (m.reasoning_content?.length ?? 0));
212
396
  }
213
- function wireBytes(content, kept, stub) {
397
+ function wireBytes(content, kept, vision, stub) {
214
398
  const tool = toolImageRef(content);
215
- if (tool) return kept ? tool.bytes : stub.length;
399
+ if (tool) return kept ? imageWireBytes(tool.path, tool.mime, vision) || stub.length : stub.length;
216
400
  if (content == null || typeof content === "string") return contentBytes(content);
217
- if (kept) return contentBytes(content);
401
+ if (kept) {
402
+ let n2 = 0;
403
+ for (const p of content) {
404
+ const url = p.type === "image_url" ? p.image_url?.url ?? "" : "";
405
+ const ref = url ? parseImageRef(url) : null;
406
+ const inline = url.startsWith("data:") ? inlineImageForWire(url, vision) : null;
407
+ n2 += ref ? imageWireBytes(ref.path, ref.mime, vision) : inline ? ("url" in inline ? inline.url : inline.note).length : contentBytes([p]);
408
+ }
409
+ return n2;
410
+ }
218
411
  let n = 0;
219
412
  for (const p of content) {
220
413
  if (p.type === "text") {
@@ -226,31 +419,70 @@ function wireBytes(content, kept, stub) {
226
419
  }
227
420
  return n;
228
421
  }
229
- function readRefDataUrl(ref) {
230
- try {
231
- return `data:${ref.mime};base64,${readFileSync(ref.path).toString("base64")}`;
232
- } catch (e) {
233
- log2.warn(`image ref unreadable, sending stub instead: ${ref.path}`, e);
234
- return null;
235
- }
236
- }
237
- function expandContent(content, stub) {
422
+ function expandContent(content, vision) {
238
423
  const tool = toolImageRef(content);
239
424
  if (tool) {
240
- const url = readRefDataUrl(tool);
241
- return url ? JSON.stringify({ dataUrl: url, image: tool.path }) : stub;
425
+ const w = imageForWire(tool.path, tool.mime, vision);
426
+ if (!w) return IMAGE_ELIDE_STUB;
427
+ return "url" in w ? [{ type: "text", text: `image: ${tool.path}` }, imagePart(w.url)] : w.note;
242
428
  }
243
429
  if (typeof content === "string") return content;
244
430
  let changed = false;
245
431
  const out = content.map((p) => {
246
- if (p.type !== "image_url" || !isImageRefUrl(p.image_url?.url)) return p;
432
+ if (p.type !== "image_url") return p;
433
+ const url = p.image_url?.url ?? "";
434
+ if (url.startsWith("data:")) {
435
+ const w2 = inlineImageForWire(url, vision);
436
+ if ("url" in w2 && w2.url === url) return p;
437
+ changed = true;
438
+ return "url" in w2 ? imagePart(w2.url) : { type: "text", text: w2.note };
439
+ }
440
+ if (!isImageRefUrl(url)) return p;
247
441
  changed = true;
248
- const ref = parseImageRef(p.image_url?.url);
249
- const url = ref && readRefDataUrl(ref);
250
- return url ? imagePart(url) : { type: "text", text: stub };
442
+ const ref = parseImageRef(url);
443
+ if (!ref) return { type: "text", text: IMAGE_ELIDE_STUB };
444
+ const w = imageForWire(ref.path, ref.mime, vision);
445
+ if (!w) return { type: "text", text: IMAGE_ELIDE_STUB };
446
+ return "url" in w ? imagePart(w.url) : { type: "text", text: w.note };
251
447
  });
252
448
  return changed ? out : content;
253
449
  }
450
+ function hoistToolImages(messages) {
451
+ if (!messages.some((m) => m.role === "tool" && Array.isArray(m.content) && m.content.some((p) => p.type === "image_url"))) return messages;
452
+ const out = [];
453
+ let pending = [];
454
+ const flush = () => {
455
+ if (pending.length) out.push({ role: "user", content: [{ type: "text", text: "Image output from the tool call above:" }, ...pending] });
456
+ pending = [];
457
+ };
458
+ for (const m of messages) {
459
+ if (m.role !== "tool") flush();
460
+ if (m.role === "tool" && Array.isArray(m.content) && m.content.some((p) => p.type === "image_url")) {
461
+ const imgs = m.content.filter((p) => p.type === "image_url");
462
+ pending.push(...imgs);
463
+ const text = m.content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
464
+ out.push({ ...m, content: text || `[${imgs.length} image(s) returned; shown below]` });
465
+ } else out.push(m);
466
+ }
467
+ flush();
468
+ return out;
469
+ }
470
+ function scrubBase64(messages) {
471
+ let changed = false;
472
+ const out = messages.map((m) => {
473
+ const c = m.content;
474
+ let next = c;
475
+ if (typeof c === "string") next = scrubBase64Text(c);
476
+ else if (Array.isArray(c)) {
477
+ const parts = c.map((p) => p.type === "text" && p.text ? { ...p, text: scrubBase64Text(p.text) } : p);
478
+ if (parts.some((p, i) => p.text !== c[i].text)) next = parts;
479
+ }
480
+ if (next === c) return m;
481
+ changed = true;
482
+ return { ...m, content: next };
483
+ });
484
+ return changed ? out : messages;
485
+ }
254
486
  function stripContent(content, stub) {
255
487
  if (toolImageRef(content)) return stub;
256
488
  if (typeof content === "string") return content;
@@ -273,12 +505,13 @@ function endsOnAnnouncedAction(text) {
273
505
  if (PROGRESSIVE.test(last)) return true;
274
506
  return clauses(last).some((c) => !NEXT_STEP.test(c) && !SIGN_OFF.test(c) && COMMITMENT.test(c));
275
507
  }
276
- var log2, p_sep, IMAGE_ELIDE_STUB, IMAGE_REF_SCHEME, REF_REGISTRY_MAX, refRegistry, isImageRefUrl, bearsImage, COMMITMENT, PROGRESSIVE, NEXT_STEP, SIGN_OFF;
508
+ var log3, p_sep, IMAGE_ELIDE_STUB, IMAGE_REF_SCHEME, REF_REGISTRY_MAX, refRegistry, isImageRefUrl, bearsImage, COMMITMENT, PROGRESSIVE, NEXT_STEP, SIGN_OFF;
277
509
  var init_llm = __esm({
278
510
  "src/llm.ts"() {
279
511
  "use strict";
280
512
  init_logging();
281
- log2 = forComponent("llm");
513
+ init_imageWire();
514
+ log3 = forComponent("llm");
282
515
  p_sep = (parts) => parts.length > 1 ? "\n" : "";
283
516
  IMAGE_ELIDE_STUB = "[image elided \u2014 re-attach @path or /paste to view again]";
284
517
  IMAGE_REF_SCHEME = "agentx-image:";
@@ -965,7 +1198,7 @@ function makeWebFetchTool(options = {}) {
965
1198
 
966
1199
  ${capped}`;
967
1200
  } catch (e) {
968
- log3.debug(`WebFetch ${u} failed`, e);
1201
+ log4.debug(`WebFetch ${u} failed`, e);
969
1202
  return `Error fetching ${u}: ${e?.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e?.message ?? e}`;
970
1203
  } finally {
971
1204
  clearTimeout(timer);
@@ -1089,7 +1322,7 @@ function makeWebSearchTool(options = {}) {
1089
1322
  if (!res.ok) return `Error: search returned ${res.status} ${res.statusText}`;
1090
1323
  return formatHits(parseDdgHtml(await res.text(), maxResults));
1091
1324
  } catch (e) {
1092
- log3.debug("WebSearch failed", e);
1325
+ log4.debug("WebSearch failed", e);
1093
1326
  return `Error searching: ${e?.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e?.message ?? e}`;
1094
1327
  } finally {
1095
1328
  clearTimeout(timer);
@@ -1100,12 +1333,12 @@ function makeWebSearchTool(options = {}) {
1100
1333
  function makeWebSearchAnthropicTool(opts = {}) {
1101
1334
  return makeWebSearchTool({ provider: "anthropic", name: "WebSearchAnthropic", description: ANTHROPIC_SEARCH_DESC, anthropicApiKey: opts.anthropicApiKey, model: opts.model });
1102
1335
  }
1103
- var log3, _dnsLookup, webFetchTool, webSearchTool, ANTHROPIC_SEARCH_DESC, webSearchAnthropicTool;
1336
+ var log4, _dnsLookup, webFetchTool, webSearchTool, ANTHROPIC_SEARCH_DESC, webSearchAnthropicTool;
1104
1337
  var init_tools_web = __esm({
1105
1338
  "src/tools.web.ts"() {
1106
1339
  "use strict";
1107
1340
  init_logging();
1108
- log3 = forComponent("web");
1341
+ log4 = forComponent("web");
1109
1342
  webFetchTool = makeWebFetchTool();
1110
1343
  webSearchTool = makeWebSearchTool();
1111
1344
  ANTHROPIC_SEARCH_DESC = "High-quality web search via Anthropic's native search index. Returns concise, sourced findings \u2014 one claim per line, each with its source URL. Prefer this over WebSearch when accuracy and citations matter; it is slower (~3\u20138s) and bills the Anthropic API account per call (one cheap-model turn + search fee).";
@@ -1325,7 +1558,7 @@ function realPathOf(fs, path) {
1325
1558
  try {
1326
1559
  return fs.realPath(path);
1327
1560
  } catch (e) {
1328
- log4.debug(`realPath unavailable for ${path} \u2014 inlining image bytes instead`, e);
1561
+ log5.debug(`realPath unavailable for ${path} \u2014 inlining image bytes instead`, e);
1329
1562
  return null;
1330
1563
  }
1331
1564
  }
@@ -1377,7 +1610,7 @@ function toolsByName(names, available) {
1377
1610
  return t;
1378
1611
  });
1379
1612
  }
1380
- var log4, numberLines, bashTool, IMG_MIME, readTool, editTool;
1613
+ var log5, numberLines, bashTool, IMG_MIME, readTool, editTool;
1381
1614
  var init_tools = __esm({
1382
1615
  "src/tools.ts"() {
1383
1616
  "use strict";
@@ -1388,7 +1621,7 @@ var init_tools = __esm({
1388
1621
  init_tools_web();
1389
1622
  init_OverlayFilesystem();
1390
1623
  init_redact();
1391
- log4 = forComponent("tools");
1624
+ log5 = forComponent("tools");
1392
1625
  numberLines = (content, offset = 0, limit) => {
1393
1626
  const lines = content.split("\n");
1394
1627
  const start = Math.max(0, offset);
@@ -1570,12 +1803,12 @@ ${tail || "(no output)"}`;
1570
1803
  }
1571
1804
  ];
1572
1805
  }
1573
- var log5, JobRegistryOptions, secs, JobRegistry, SandboxJobRegistry, NO_JOB, describe;
1806
+ var log6, JobRegistryOptions, secs, JobRegistry, SandboxJobRegistry, NO_JOB, describe;
1574
1807
  var init_tools_jobs = __esm({
1575
1808
  "src/tools.jobs.ts"() {
1576
1809
  "use strict";
1577
1810
  init_logging();
1578
- log5 = forComponent("jobs");
1811
+ log6 = forComponent("jobs");
1579
1812
  JobRegistryOptions = class {
1580
1813
  /** Tail buffer cap per job (chars). */
1581
1814
  maxBuffer = 256 * 1024;
@@ -1754,7 +1987,7 @@ var init_tools_jobs = __esm({
1754
1987
  try {
1755
1988
  onExit({ id: job.id, kind: job.kind, label: job.label, status, reason: job.reason, tail: job.buf.slice(-4e3), result });
1756
1989
  } catch (e) {
1757
- log5.warn(`job onExit handler threw for ${job.id}: ${e}`);
1990
+ log6.warn(`job onExit handler threw for ${job.id}: ${e}`);
1758
1991
  }
1759
1992
  }
1760
1993
  }
@@ -1808,7 +2041,7 @@ var init_tools_jobs = __esm({
1808
2041
  try {
1809
2042
  j.onKill?.(reason);
1810
2043
  } catch (e) {
1811
- log5.warn(`onKill for ${id} threw: ${e}`);
2044
+ log6.warn(`onKill for ${id} threw: ${e}`);
1812
2045
  }
1813
2046
  }
1814
2047
  return true;
@@ -1855,7 +2088,7 @@ var init_tools_jobs = __esm({
1855
2088
  try {
1856
2089
  cpu = await j.cpuTime();
1857
2090
  } catch (e) {
1858
- log5.debug(`cpu probe for ${j.id} failed: ${e}`);
2091
+ log6.debug(`cpu probe for ${j.id} failed: ${e}`);
1859
2092
  }
1860
2093
  if (cpu != null && j.lastCpu != null && cpu > j.lastCpu) j.lastActivityAt = this.options.now();
1861
2094
  const prev = j.lastCpu;
@@ -1863,7 +2096,7 @@ var init_tools_jobs = __esm({
1863
2096
  const verdict = this.verdict(j, now3);
1864
2097
  if (!verdict?.idle || j.status !== "running") continue;
1865
2098
  if (cpu == null) {
1866
- log5.debug(`not reaping ${j.id}: ${verdict.reason} but CPU unknown`);
2099
+ log6.debug(`not reaping ${j.id}: ${verdict.reason} but CPU unknown`);
1867
2100
  continue;
1868
2101
  }
1869
2102
  if (prev == null) continue;
@@ -1877,7 +2110,7 @@ var init_tools_jobs = __esm({
1877
2110
  return null;
1878
2111
  }
1879
2112
  stuck(j, reason, now3) {
1880
- log5.warn(`reaping ${j.id} (${j.kind} "${j.label}"): ${reason} \u2014 age ${secs(now3 - j.startedAt)}, last activity ${secs(now3 - j.lastActivityAt)} ago, idleMs=${j.idleMs}, hardCapMs=${j.hardCapMs}, ${j.bytes} byte(s) output`);
2113
+ log6.warn(`reaping ${j.id} (${j.kind} "${j.label}"): ${reason} \u2014 age ${secs(now3 - j.startedAt)}, last activity ${secs(now3 - j.lastActivityAt)} ago, idleMs=${j.idleMs}, hardCapMs=${j.hardCapMs}, ${j.bytes} byte(s) output`);
1881
2114
  if (j.onKill) j.notify = true;
1882
2115
  this.kill(j.id, reason);
1883
2116
  }
@@ -1891,7 +2124,7 @@ var init_tools_jobs = __esm({
1891
2124
  this.timer = void 0;
1892
2125
  return;
1893
2126
  }
1894
- void this.sweep().catch((e) => log5.warn(`job sweep failed: ${e}`));
2127
+ void this.sweep().catch((e) => log6.warn(`job sweep failed: ${e}`));
1895
2128
  }, this.options.sweepMs);
1896
2129
  this.timer.unref?.();
1897
2130
  }
@@ -2402,7 +2635,7 @@ async function psGroupCpuSeconds(pgid) {
2402
2635
  const out = await new Promise((res, rej) => execFile("ps", ["-A", "-o", "pid=,ppid=,pgid=,time="], { timeout: 5e3, env: { ...process.env, LC_ALL: "C" } }, (e, so) => e ? rej(e) : res(String(so))));
2403
2636
  return sumTreeCpu(out, pgid);
2404
2637
  } catch (e) {
2405
- log8.debug(`ps cpu probe failed for group ${pgid}: ${e}`);
2638
+ log9.debug(`ps cpu probe failed for group ${pgid}: ${e}`);
2406
2639
  return void 0;
2407
2640
  }
2408
2641
  }
@@ -2550,7 +2783,7 @@ function makeRealShellTool(options) {
2550
2783
  proc.stderr?.on("data", collect);
2551
2784
  proc.on("error", (err) => {
2552
2785
  if (err?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
2553
- log8.debug("shell spawn error", err);
2786
+ log9.debug("shell spawn error", err);
2554
2787
  finish(`[exit 1] ${err?.message ?? err}${out ? "\n" + clean(out) : ""}`);
2555
2788
  });
2556
2789
  proc.on("close", (code) => {
@@ -2623,7 +2856,7 @@ ${clean(out) || "(no output yet)"}`;
2623
2856
  }
2624
2857
  ];
2625
2858
  }
2626
- var log8, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, shellStatus, NO_JOB2;
2859
+ var log9, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, shellStatus, NO_JOB2;
2627
2860
  var init_tools_shell = __esm({
2628
2861
  "src/tools.shell.ts"() {
2629
2862
  "use strict";
@@ -2632,7 +2865,7 @@ var init_tools_shell = __esm({
2632
2865
  init_logging();
2633
2866
  init_shell_sandbox();
2634
2867
  init_tools_jobs();
2635
- log8 = forComponent("shell");
2868
+ log9 = forComponent("shell");
2636
2869
  clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
2637
2870
  DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
2638
2871
  SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
@@ -3409,8 +3642,8 @@ ${sections.join("\n\n---\n\n")}`;
3409
3642
 
3410
3643
  // src/subagent.ts
3411
3644
  init_tools();
3412
- import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
3413
- import { join as join3 } from "path";
3645
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
3646
+ import { join as join4 } from "path";
3414
3647
 
3415
3648
  // src/models.ts
3416
3649
  var MODEL_ALIASES = {
@@ -3607,9 +3840,9 @@ function composeHooks(...list) {
3607
3840
  init_OverlayFilesystem();
3608
3841
 
3609
3842
  // src/worktree.ts
3610
- import { spawnSync } from "child_process";
3611
- import { symlinkSync, mkdirSync, statSync as statSync2, readFileSync as readFileSync2, rmSync } from "fs";
3612
- import { join } from "path";
3843
+ import { spawnSync as spawnSync2 } from "child_process";
3844
+ import { symlinkSync, mkdirSync, statSync as statSync3, readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
3845
+ import { join as join2 } from "path";
3613
3846
  var VALID_SEGMENT = /^[a-zA-Z0-9._-]+$/;
3614
3847
  var MAX_SLUG_LENGTH = 64;
3615
3848
  var GIT_NO_PROMPT_ENV = { GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "" };
@@ -3628,13 +3861,13 @@ function worktreeBranchName(slug2) {
3628
3861
  return `worktree-${flattenSlug(slug2)}`;
3629
3862
  }
3630
3863
  function worktreesDir(repoRoot) {
3631
- return join(repoRoot, ".claude", "worktrees");
3864
+ return join2(repoRoot, ".claude", "worktrees");
3632
3865
  }
3633
3866
  function worktreePathFor(repoRoot, slug2) {
3634
- return join(worktreesDir(repoRoot), flattenSlug(slug2));
3867
+ return join2(worktreesDir(repoRoot), flattenSlug(slug2));
3635
3868
  }
3636
3869
  function git(args, cwd) {
3637
- const r = spawnSync("git", args, {
3870
+ const r = spawnSync2("git", args, {
3638
3871
  cwd,
3639
3872
  stdio: ["ignore", "pipe", "pipe"],
3640
3873
  env: { ...process.env, ...GIT_NO_PROMPT_ENV }
@@ -3647,16 +3880,16 @@ function findGitRoot(from) {
3647
3880
  }
3648
3881
  function readWorktreeHead(worktreePath) {
3649
3882
  try {
3650
- const gitFile = readFileSync2(join(worktreePath, ".git"), "utf-8").trim();
3883
+ const gitFile = readFileSync3(join2(worktreePath, ".git"), "utf-8").trim();
3651
3884
  const m = gitFile.match(/^gitdir:\s*(.+)$/);
3652
3885
  if (!m) return null;
3653
- const headPath = join(m[1], "HEAD");
3654
- const head = readFileSync2(headPath, "utf-8").trim();
3886
+ const headPath = join2(m[1], "HEAD");
3887
+ const head = readFileSync3(headPath, "utf-8").trim();
3655
3888
  const refMatch = head.match(/^ref:\s*(.+)$/);
3656
3889
  if (!refMatch) return head.length === 40 ? head : null;
3657
- const refPath = join(m[1], "..", refMatch[1]);
3890
+ const refPath = join2(m[1], "..", refMatch[1]);
3658
3891
  try {
3659
- return readFileSync2(refPath, "utf-8").trim();
3892
+ return readFileSync3(refPath, "utf-8").trim();
3660
3893
  } catch {
3661
3894
  }
3662
3895
  const r = git(["rev-parse", "HEAD"], worktreePath);
@@ -3689,10 +3922,10 @@ function getOrCreateWorktree(repoRoot, slug2) {
3689
3922
  }
3690
3923
  function symlinkDirs(repoRoot, worktreePath, dirs) {
3691
3924
  for (const dir of dirs) {
3692
- const src = join(repoRoot, dir);
3693
- const dst = join(worktreePath, dir);
3925
+ const src = join2(repoRoot, dir);
3926
+ const dst = join2(worktreePath, dir);
3694
3927
  try {
3695
- statSync2(src);
3928
+ statSync3(src);
3696
3929
  } catch {
3697
3930
  continue;
3698
3931
  }
@@ -3708,7 +3941,7 @@ function cleanupWorktree(repoRoot, slug2, opts) {
3708
3941
  const worktreePath = worktreePathFor(repoRoot, slug2);
3709
3942
  const branch = worktreeBranchName(slug2);
3710
3943
  try {
3711
- statSync2(worktreePath);
3944
+ statSync3(worktreePath);
3712
3945
  } catch {
3713
3946
  return { removed: false, reason: "not found" };
3714
3947
  }
@@ -3718,7 +3951,7 @@ function cleanupWorktree(repoRoot, slug2, opts) {
3718
3951
  }
3719
3952
  for (const dir of ["node_modules", ".bun"]) {
3720
3953
  try {
3721
- rmSync(join(worktreePath, dir));
3954
+ rmSync2(join2(worktreePath, dir));
3722
3955
  } catch {
3723
3956
  }
3724
3957
  }
@@ -3758,7 +3991,7 @@ function exitWorktree() {
3758
3991
  // src/subagent.ts
3759
3992
  init_NodeDiskFilesystem();
3760
3993
  init_logging();
3761
- var log6 = forComponent("subagent");
3994
+ var log7 = forComponent("subagent");
3762
3995
  async function boundedPool(items, limit, fn) {
3763
3996
  const out = new Array(items.length);
3764
3997
  let next = 0;
@@ -3830,17 +4063,17 @@ function traceFileFor(dir, label, depth) {
3830
4063
  try {
3831
4064
  mkdirSync2(dir, { recursive: true });
3832
4065
  const id = `${Date.now().toString(36)}-${(traceSeq++).toString(36)}`;
3833
- return join3(dir, `${id}-d${depth}-${sanitizeSlug(label) || "sub"}.json`);
4066
+ return join4(dir, `${id}-d${depth}-${sanitizeSlug(label) || "sub"}.json`);
3834
4067
  } catch (e) {
3835
- log6.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
4068
+ log7.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
3836
4069
  return void 0;
3837
4070
  }
3838
4071
  }
3839
4072
  function flushTrace(file, meta, messages, status) {
3840
4073
  try {
3841
- writeFileSync(file, JSON.stringify({ ...meta, status, updated: Date.now(), messages }, null, 2));
4074
+ writeFileSync2(file, JSON.stringify({ ...meta, status, updated: Date.now(), messages }, null, 2));
3842
4075
  } catch (e) {
3843
- log6.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
4076
+ log7.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
3844
4077
  }
3845
4078
  }
3846
4079
  function installTrace(child, file, meta) {
@@ -3881,7 +4114,7 @@ async function runChildTraced(args) {
3881
4114
  if (file) flushTrace(file, meta, child.transcript, wc.timedOut() ? "deadline" : res.finishReason);
3882
4115
  let result = res.finishReason === "stop" ? res.text || `(child '${label}' finished with no summary; finishReason=${res.finishReason})` : abortHint(label, res, file);
3883
4116
  if (wc.timedOut()) {
3884
- log6.warn(`sub-task '${label}' hit its ${deadlineMs}ms wall-clock deadline (steps=${res.steps})`);
4117
+ log7.warn(`sub-task '${label}' hit its ${deadlineMs}ms wall-clock deadline (steps=${res.steps})`);
3885
4118
  result += deadlineNote(deadlineMs);
3886
4119
  }
3887
4120
  await opts.hooks?.onSubagentStop?.(result, { label, agentType });
@@ -4210,7 +4443,7 @@ function reasoningToChatFragment(model, effort) {
4210
4443
  }
4211
4444
 
4212
4445
  // src/Agent.ts
4213
- var log7 = forComponent("Agent");
4446
+ var log8 = forComponent("Agent");
4214
4447
  function envInt(key, fallback) {
4215
4448
  const n = typeof process !== "undefined" ? Number(process.env?.[key]) : NaN;
4216
4449
  return Number.isFinite(n) && n >= 0 ? n : fallback;
@@ -4291,6 +4524,9 @@ var AgentOptions = class {
4291
4524
  * `maxTokens`: that is the cumulative SPEND kill-switch, and a spend budget must never be able to
4292
4525
  * drive destructive compaction (`--max-tokens 50000` would otherwise shred the transcript). */
4293
4526
  contextWindow = 0;
4527
+ /** Does a model take image input? true/false from the host's model catalog; undefined = unknown (images are tried,
4528
+ * and turned off for the run when the adapter refuses them). Unset = always try. */
4529
+ imageInput;
4294
4530
  /** Pagination ceiling for a SINGLE tool result (bytes). A result over this is cropped to page 1 with
4295
4531
  * a marker telling the model it was cropped (refine the query, or page further). Guards against one
4296
4532
  * Grep/Read/MCP call blowing the whole context window. 0 = off. Default 60k (~15k tokens). */
@@ -4590,7 +4826,7 @@ var Agent = class _Agent {
4590
4826
  const disk = new NodeDiskFilesystem2(process.cwd());
4591
4827
  await disk.init();
4592
4828
  this.options.fs = new JailedFilesystem2(disk);
4593
- log7.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
4829
+ log8.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
4594
4830
  }
4595
4831
  this.buildCtx();
4596
4832
  }
@@ -4735,7 +4971,8 @@ var Agent = class _Agent {
4735
4971
  const cap = _Agent.contextCap(this.options);
4736
4972
  const target = Math.round(cap * _Agent.COMPACT_TARGET);
4737
4973
  const kr = this.options.keepRecentImages ?? 1;
4738
- const tokensBefore = estimateTokens(this.transcript, kr);
4974
+ const vis = this.sendsImages();
4975
+ const tokensBefore = estimateTokens(this.transcript, kr, vis);
4739
4976
  const msgsBefore = this.transcript.length;
4740
4977
  let max = Math.max(2, maxMessages);
4741
4978
  if (this.transcript.length <= max && tokensBefore <= target)
@@ -4743,18 +4980,18 @@ var Agent = class _Agent {
4743
4980
  void this.activeHooks?.onPreCompact?.(this.transcript);
4744
4981
  let out = this.transcript;
4745
4982
  if (out.length > max) out = compact(out, max, focus);
4746
- if (estimateTokens(out, kr) > target) out = elideOversizedImages(out, target, true, kr);
4747
- if (estimateTokens(out, kr) > target) out = capOversizedBodies(out, target, kr);
4748
- if (estimateTokens(out, kr) > target) out = elideOversizedImages(out, target, false, kr);
4749
- while (estimateTokens(out, kr) > target && max > 2 && out.length > 2) {
4983
+ if (estimateTokens(out, kr, vis) > target) out = elideOversizedImages(out, target, true, kr);
4984
+ if (estimateTokens(out, kr, vis) > target) out = capOversizedBodies(out, target, kr);
4985
+ if (estimateTokens(out, kr, vis) > target) out = elideOversizedImages(out, target, false, kr);
4986
+ while (estimateTokens(out, kr, vis) > target && max > 2 && out.length > 2) {
4750
4987
  max = Math.max(2, Math.floor(max / 2));
4751
4988
  out = capOversizedBodies(compact(out, max, focus), target, kr);
4752
4989
  }
4753
- if (estimateTokens(out, kr) > target) out = fitTokenBudget(out, target, kr);
4990
+ if (estimateTokens(out, kr, vis) > target) out = fitTokenBudget(out, target, kr);
4754
4991
  out = dropOrphanToolResults(out);
4755
4992
  this.transcript = out;
4756
4993
  this.releaseStaleImages();
4757
- return { tokensBefore, tokensAfter: estimateTokens(this.transcript, kr), target, cap, removed: msgsBefore - this.transcript.length };
4994
+ return { tokensBefore, tokensAfter: estimateTokens(this.transcript, kr, vis), target, cap, removed: msgsBefore - this.transcript.length };
4758
4995
  }
4759
4996
  /** Drop inline base64 images from older messages so Bun/JSC can reclaim the heap (see contentBytes). */
4760
4997
  releaseStaleImages(keepRecent = 1) {
@@ -4778,7 +5015,7 @@ var Agent = class _Agent {
4778
5015
  let closureNudged = false;
4779
5016
  let lastProgressAt = start;
4780
5017
  const kill = (finishReason) => {
4781
- log7.warn(`kill-switch: ${finishReason} (steps=${steps}, tokens=${usage.totalTokens}, budgetTokens=${Math.round(usage.totalTokens - 0.9 * usage.cacheReadTokens)}, ms=${Date.now() - start - this.parkedMs}${finishReason === "timeout" ? `, idle=${Date.now() - lastProgressAt - this.parkedMs}ms` : ""}${this.parkedMs ? ` +${this.parkedMs} parked` : ""})`);
5018
+ log8.warn(`kill-switch: ${finishReason} (steps=${steps}, tokens=${usage.totalTokens}, budgetTokens=${Math.round(usage.totalTokens - 0.9 * usage.cacheReadTokens)}, ms=${Date.now() - start - this.parkedMs}${finishReason === "timeout" ? `, idle=${Date.now() - lastProgressAt - this.parkedMs}ms` : ""}${this.parkedMs ? ` +${this.parkedMs} parked` : ""})`);
4782
5019
  this.jobs.killAll(`run ended: ${finishReason}`, true);
4783
5020
  const claims = this.survivingClaims();
4784
5021
  return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
@@ -4793,7 +5030,7 @@ var Agent = class _Agent {
4793
5030
  steps++;
4794
5031
  this.options.host?.notify?.({ kind: "turn_start", message: `step ${steps}` });
4795
5032
  let res;
4796
- const sent = this.trimContext();
5033
+ let sent = this.trimContext();
4797
5034
  const frag = reasoningToChatFragment(o.model, o.reasoning);
4798
5035
  const sentTools = o.model.startsWith("claude-code/") ? [] : wireTools;
4799
5036
  const isCursorWithTools = o.model.startsWith("cursor/") && wireTools.length > 0;
@@ -4804,9 +5041,9 @@ var Agent = class _Agent {
4804
5041
  this.nativeHoldMs = o.nativeHoldMs;
4805
5042
  if (isCursorWithTools) {
4806
5043
  if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
4807
- log7.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.`);
5044
+ 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.`);
4808
5045
  }
4809
- for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log7.warn(v);
5046
+ for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log8.warn(v);
4810
5047
  }
4811
5048
  const hostExec = async (name, args) => {
4812
5049
  const tc = { id: `host-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
@@ -4865,12 +5102,20 @@ var Agent = class _Agent {
4865
5102
  res = r;
4866
5103
  }
4867
5104
  if (steps === 1 && this.isEmptyStop(res) && !this.pendingInjections.length) {
4868
- const e = new Error("model returned an empty turn (finishReason=stop, no text, no tools)");
5105
+ const e = new Error(`model returned an empty turn (finishReason=${res.finishReason ?? "n/a"}, no text, no tools)`);
4869
5106
  e.code = "empty";
5107
+ if (res.finishReason) e.providerFinishReason = res.finishReason;
4870
5108
  throw e;
4871
5109
  }
4872
5110
  break;
4873
5111
  } catch (err) {
5112
+ if (!this.imagesOff && /does not support .*input/i.test(String(err?.message ?? "")) && sent.some((m) => Array.isArray(m.content) && m.role === "tool")) {
5113
+ this.imagesOff = true;
5114
+ log8.warn(`model refused image input (${err?.message}) \u2014 resending with image Reads as text stubs`);
5115
+ sent = this.trimContext();
5116
+ attempt--;
5117
+ continue;
5118
+ }
4874
5119
  const sc = err?.statusCode;
4875
5120
  const serverSide = sc >= 500 || sc === 408 || /Service Unavailable|overloaded|Internal server error|Bad gateway|Gateway time/i.test(String(err?.message ?? ""));
4876
5121
  const network = !sc && /ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|EPIPE|socket hang up|fetch failed|network|terminated|UND_ERR/i.test(String(err?.message ?? err?.code ?? err));
@@ -4878,11 +5123,11 @@ var Agent = class _Agent {
4878
5123
  const empty = err?.code === "empty" || err?.code === "cursor_silent";
4879
5124
  const maxAttempts = empty ? Math.max(o.emptyRetries, 0) : 2;
4880
5125
  const replaysTool = stalled && this.toolRanThisAttempt;
4881
- if (replaysTool) log7.warn("model stall after tool(s) already ran this step \u2014 not retrying (a retry would replay them)");
5126
+ if (replaysTool) log8.warn("model stall after tool(s) already ran this step \u2014 not retrying (a retry would replay them)");
4882
5127
  const transient = !o.signal?.aborted && !isAbortError(err) && !replaysTool && attempt < maxAttempts && (network || serverSide || stalled || empty);
4883
5128
  if (!transient) throw err;
4884
5129
  const waitMs = (empty ? o.emptyRetryBaseMs : 1e3) * (attempt + 1);
4885
- log7.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err?.message ?? err}) \u2014 retrying in ${waitMs}ms`);
5130
+ log8.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err?.message ?? err}) \u2014 retrying in ${waitMs}ms`);
4886
5131
  o.host?.notify?.({ kind: "retry", message: `${empty ? "model returned nothing" : stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
4887
5132
  await new Promise((r) => setTimeout(r, waitMs));
4888
5133
  }
@@ -4890,13 +5135,14 @@ var Agent = class _Agent {
4890
5135
  } catch (err) {
4891
5136
  if (err?.code === "budget") return kill("budget");
4892
5137
  if (err?.code === "stall") {
4893
- log7.warn(`model stalled out after retries: ${err?.message ?? err}`);
4894
- return { ...kill("timeout"), error: err };
5138
+ log8.warn(`model stalled out after retries: ${err?.message ?? err}`);
5139
+ return { ...kill("timeout"), providerFinishReason: "none (stalled)", error: err };
4895
5140
  }
4896
5141
  if (o.signal?.aborted || isAbortError(err)) return kill("aborted");
4897
5142
  if (err?.code === "empty" || err?.code === "cursor_silent") {
4898
- log7.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
4899
- return { text: "", steps, finishReason: "empty", messages: this.transcript, usage, usageEstimated, error: err };
5143
+ log8.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
5144
+ const pfr = err?.providerFinishReason;
5145
+ return { text: "", steps, finishReason: "empty", ...pfr ? { providerFinishReason: pfr } : {}, messages: this.transcript, usage, usageEstimated, error: err };
4900
5146
  }
4901
5147
  const body = err?.body ?? err?.response?.data ?? err?.error;
4902
5148
  let bodyStr;
@@ -4907,13 +5153,13 @@ var Agent = class _Agent {
4907
5153
  }
4908
5154
  if (bodyStr && err instanceof Error && !err.message.includes(bodyStr)) err.detail = bodyStr;
4909
5155
  const errLog = err instanceof Error ? { message: err.message, ...err.detail ? { detail: err.detail } : {}, stack: err.stack } : err;
4910
- log7.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
5156
+ log8.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
4911
5157
  return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err };
4912
5158
  }
4913
5159
  if (o.signal?.aborted) return kill("aborted");
4914
5160
  if (res.finishReason === "error" && !res.toolCalls?.length) {
4915
5161
  const err = new Error(contentText(res.content ?? "").trim() || "provider error");
4916
- log7.error(`chat() failed: ${err.message}`);
5162
+ log8.error(`chat() failed: ${err.message}`);
4917
5163
  return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err };
4918
5164
  }
4919
5165
  if (!res.usage) {
@@ -4971,21 +5217,21 @@ var Agent = class _Agent {
4971
5217
  if (emptyTurn && o.failEmptyTurns) {
4972
5218
  if (!emptyNudged && canNudge()) {
4973
5219
  emptyNudged = true;
4974
- log7.warn(`empty model turn after tool work (step ${steps}, provider finishReason=${res.finishReason ?? "n/a"}) \u2014 nudging once`);
5220
+ log8.warn(`empty model turn after tool work (step ${steps}, provider finishReason=${res.finishReason ?? "n/a"}) \u2014 nudging once`);
4975
5221
  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." });
4976
5222
  continue;
4977
5223
  }
4978
- log7.warn(`model returned empty turns after tool work \u2014 surfacing as failed`);
5224
+ log8.warn(`model returned empty turns after tool work \u2014 surfacing as failed`);
4979
5225
  const e = new Error("empty_response: model returned an empty turn (no text, no tools) after a re-prompt");
4980
5226
  e.code = "empty";
4981
5227
  return { text: "", steps, finishReason: "empty", ...res.finishReason ? { providerFinishReason: res.finishReason } : {}, messages: this.transcript, usage, usageEstimated, error: e };
4982
5228
  }
4983
5229
  const fabricated = o.catchFabricatedToolUse && !closureNudged ? this.fabricatedToolNames(contentText(res.content ?? "")) : [];
4984
5230
  for (const n of fabricated) this.fabricatedClaims.add(n);
4985
- if (fabricated.length && !canNudge()) log7.warn(`fabricated tool use: text names ${fabricated.join(", ")} but no step/budget left to demand the call \u2014 finishing with the existing answer`);
5231
+ 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`);
4986
5232
  if (fabricated.length && canNudge()) {
4987
5233
  closureNudged = true;
4988
- log7.warn(`fabricated tool use: text names ${fabricated.join(", ")} but the run invoked ${this.invokedTools.size ? [...this.invokedTools].join(", ") : "nothing"} \u2014 demanding the real call`);
5234
+ 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`);
4989
5235
  this.transcript.push({
4990
5236
  role: "user",
4991
5237
  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.`
@@ -4994,14 +5240,14 @@ var Agent = class _Agent {
4994
5240
  }
4995
5241
  if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && canNudge()) {
4996
5242
  closureNudged = true;
4997
- log7.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
5243
+ log8.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
4998
5244
  this.transcript.push({
4999
5245
  role: "user",
5000
5246
  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."
5001
5247
  });
5002
5248
  continue;
5003
5249
  }
5004
- log7.verbose(`completed in ${steps} step(s)`);
5250
+ log8.verbose(`completed in ${steps} step(s)`);
5005
5251
  const drained = await this.jobs.drain(o.signal);
5006
5252
  if (o.signal?.aborted) return kill("aborted");
5007
5253
  if (drained.length) lastProgressAt = Date.now();
@@ -5035,7 +5281,7 @@ var Agent = class _Agent {
5035
5281
  if (this.ctx.needsInput) {
5036
5282
  const question = this.ctx.needsInput;
5037
5283
  this.ctx.needsInput = void 0;
5038
- log7.warn(`needs_input: ${question.question} (steps=${steps})`);
5284
+ log8.warn(`needs_input: ${question.question} (steps=${steps})`);
5039
5285
  await this.jobs.drain();
5040
5286
  const text = [
5041
5287
  `OPEN QUESTION (run paused \u2014 no human was reachable): ${question.question}`,
@@ -5077,12 +5323,12 @@ var Agent = class _Agent {
5077
5323
  if (this.toolHoldActive()) {
5078
5324
  const now3 = Date.now();
5079
5325
  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(", ");
5080
- log7.warn(`stall watchdog: no stream output for ${stallMs}ms \u2014 held by ${who}`);
5326
+ log8.warn(`stall watchdog: no stream output for ${stallMs}ms \u2014 held by ${who}`);
5081
5327
  this.options.host?.notify?.({ kind: "tool_hold", message: `waiting on ${who}` });
5082
5328
  return poke();
5083
5329
  }
5084
5330
  const inFlight = this.jobs.list().filter((j) => j.status === "running");
5085
- log7.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(", ")})` : ""));
5331
+ 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(", ")})` : ""));
5086
5332
  fired = true;
5087
5333
  ac.abort(stallError());
5088
5334
  }, stallMs);
@@ -5252,13 +5498,13 @@ var Agent = class _Agent {
5252
5498
  const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
5253
5499
  if (decision?.block) {
5254
5500
  const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
5255
- log7.debug(`${tc.function.name} -> ${blocked}`);
5501
+ log8.debug(`${tc.function.name} -> ${blocked}`);
5256
5502
  await hooks?.postToolUse?.(call, blocked, meta);
5257
5503
  return blocked;
5258
5504
  }
5259
5505
  this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
5260
5506
  if (earlyError) {
5261
- log7.debug(`${tc.function.name} -> ${earlyError}`);
5507
+ log8.debug(`${tc.function.name} -> ${earlyError}`);
5262
5508
  await hooks?.postToolUse?.(call, earlyError, meta);
5263
5509
  this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
5264
5510
  return earlyError;
@@ -5267,12 +5513,12 @@ var Agent = class _Agent {
5267
5513
  let images;
5268
5514
  let threw = false;
5269
5515
  try {
5270
- log7.debug(`${tc.function.name}(${tc.function.arguments})`);
5516
+ log8.debug(`${tc.function.name}(${tc.function.arguments})`);
5271
5517
  this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
5272
5518
  try {
5273
5519
  hooks.onToolOutput(call, chunk, meta);
5274
5520
  } catch (e) {
5275
- log7.debug(`onToolOutput hook error: ${e}`);
5521
+ log8.debug(`onToolOutput hook error: ${e}`);
5276
5522
  }
5277
5523
  } : void 0;
5278
5524
  const raw = await tool.run(args, this.ctx);
@@ -5284,7 +5530,7 @@ var Agent = class _Agent {
5284
5530
  }
5285
5531
  } catch (e) {
5286
5532
  const msg = e instanceof Error ? e.message : String(e);
5287
- log7.debug(`${tc.function.name} -> error: ${msg}`);
5533
+ log8.debug(`${tc.function.name} -> error: ${msg}`);
5288
5534
  result = `Error: ${msg}`;
5289
5535
  threw = true;
5290
5536
  } finally {
@@ -5344,6 +5590,13 @@ ${out}`;
5344
5590
  * 3. token-aware backstop (`maxContextTokens`) — drop oldest messages until under an estimated token ceiling.
5345
5591
  * With every knob off, returns the transcript unchanged (same reference).
5346
5592
  */
5593
+ /** Set when the model refused image parts this run: tool image Reads go as text stubs from then on. */
5594
+ imagesOff = false;
5595
+ /** Send tool image Reads as images? Known models by their catalog flag; unknown ones try (and latch off on refusal). */
5596
+ sendsImages() {
5597
+ if (this.imagesOff) return false;
5598
+ return this.options.imageInput?.(this.options.model) ?? true;
5599
+ }
5347
5600
  trimContext() {
5348
5601
  const o = this.options;
5349
5602
  const m = this.transcript;
@@ -5353,17 +5606,19 @@ ${out}`;
5353
5606
  const browserKeep = this.browserActive ? o.browserTrim?.keepOutputs : void 0;
5354
5607
  const keep = browserKeep != null ? o.keepToolOutputs ? Math.min(o.keepToolOutputs, browserKeep) : browserKeep : o.keepToolOutputs;
5355
5608
  if (keep) out = stubOldToolResults(out ?? m, keep);
5356
- out = expandImagesForSend(dropOrphanToolResults(out ?? m), o.keepRecentImages ?? 1);
5609
+ out = expandImagesForSend(dropOrphanToolResults(out ?? m), o.keepRecentImages ?? 1, void 0, this.sendsImages());
5357
5610
  if (o.maxContextTokens) {
5358
5611
  const pre = out.length;
5359
- out = fitTokenBudget(out, o.maxContextTokens, o.keepRecentImages ?? 1);
5612
+ out = fitTokenBudget(out, o.maxContextTokens, o.keepRecentImages ?? 1, this.sendsImages());
5360
5613
  const dropped = pre - out.length;
5361
5614
  if (dropped > 0 && dropped !== this.lastTrimNotified) {
5362
5615
  this.lastTrimNotified = dropped;
5363
5616
  o.host?.notify?.({ kind: "compaction", message: `context full \u2014 auto-trimmed ${dropped} oldest message(s) to fit ~${Math.round(o.maxContextTokens / 1e3)}k tokens` });
5364
5617
  }
5365
5618
  }
5366
- return dropOrphanToolResults(out);
5619
+ out = dropOrphanToolResults(out);
5620
+ if (/^google\//.test(o.model ?? "")) out = hoistToolImages(out);
5621
+ return scrubBase64(out);
5367
5622
  }
5368
5623
  };
5369
5624
  function dropOldest(m, max) {
@@ -5372,8 +5627,8 @@ function dropOldest(m, max) {
5372
5627
  }
5373
5628
  var ANTHROPIC_WINDOW_WITHOUT_1M_BETA = 2e5;
5374
5629
  var isAnthropicModel = (m) => !!m && (m.startsWith("anthropic/") || /(^|\/)claude-/.test(m));
5375
- function estimateTokens(m, keepRecentImages = 1) {
5376
- return Math.ceil(sendBytes(m, keepRecentImages) / 4);
5630
+ function estimateTokens(m, keepRecentImages = 1, vision = true) {
5631
+ return Math.ceil(sendBytes(m, keepRecentImages, vision) / 4);
5377
5632
  }
5378
5633
  var TRANSPORT_TOOL_TIMEOUT_FLOOR_MS = 18e5;
5379
5634
  var isMcpTool = (t) => t.name.startsWith("mcp__") || t.name === "McpCall";
@@ -5542,8 +5797,8 @@ function dropOrphanToolResults(messages) {
5542
5797
  const ok = (m) => m.role !== "tool" || ids.has(m.tool_call_id ?? "");
5543
5798
  return messages.every(ok) ? messages : messages.filter(ok);
5544
5799
  }
5545
- function fitTokenBudget(messages, maxTokens, keepRecentImages = 1) {
5546
- const per = sendBytesPer(messages, keepRecentImages).map((b) => Math.ceil(b / 4));
5800
+ function fitTokenBudget(messages, maxTokens, keepRecentImages = 1, vision = true) {
5801
+ const per = sendBytesPer(messages, keepRecentImages, vision).map((b) => Math.ceil(b / 4));
5547
5802
  let total = per.reduce((a, b) => a + b, 0);
5548
5803
  if (total <= maxTokens) return messages;
5549
5804
  const head = messages[0]?.role === "system" ? [messages[0]] : [];
@@ -5552,7 +5807,7 @@ function fitTokenBudget(messages, maxTokens, keepRecentImages = 1) {
5552
5807
  const ids = callIdSet(messages.slice(from));
5553
5808
  while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
5554
5809
  if (total > maxTokens)
5555
- log7.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
5810
+ log8.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
5556
5811
  return [...head, ...messages.slice(from)];
5557
5812
  }
5558
5813
  function compact(m, max, focus) {
@@ -6235,7 +6490,7 @@ init_tools_web();
6235
6490
  init_tools();
6236
6491
  init_tools_structured();
6237
6492
  init_logging();
6238
- var log9 = forComponent("scratch");
6493
+ var log10 = forComponent("scratch");
6239
6494
  var SCRATCH_DIR = "/scratch";
6240
6495
  function shortArgs(args) {
6241
6496
  try {
@@ -6275,7 +6530,7 @@ var Scratch = class {
6275
6530
  await (this.dirReady ??= mkdirp(this.fs, dir));
6276
6531
  await this.fs.writeFile(path, header + raw);
6277
6532
  } catch (e) {
6278
- log9.debug("scratch write failed; returning raw", e);
6533
+ log10.debug("scratch write failed; returning raw", e);
6279
6534
  return raw;
6280
6535
  }
6281
6536
  const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
@@ -6305,7 +6560,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
6305
6560
  await (this.dirReady ??= mkdirp(this.fs, dir));
6306
6561
  await this.fs.writeFile(path, header + full);
6307
6562
  } catch (e) {
6308
- log9.debug("scratch spill failed; cropping lossy", e);
6563
+ log10.debug("scratch spill failed; cropping lossy", e);
6309
6564
  return full.slice(0, pageBytes) + `
6310
6565
 
6311
6566
  [output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
@@ -6351,7 +6606,7 @@ Question: ${q}`);
6351
6606
  const answer = (res.text ?? "").trim();
6352
6607
  return answer || "(no answer found in scratch)";
6353
6608
  } catch (e) {
6354
- log9.debug("Ask peek failed", e);
6609
+ log10.debug("Ask peek failed", e);
6355
6610
  return `Error querying scratch: ${e?.message ?? e}`;
6356
6611
  }
6357
6612
  }
@@ -6360,7 +6615,7 @@ Question: ${q}`);
6360
6615
 
6361
6616
  // src/lessons.ts
6362
6617
  init_logging();
6363
- var log10 = forComponent("Lessons");
6618
+ var log11 = forComponent("Lessons");
6364
6619
  var LessonOptionsDefaults = class {
6365
6620
  minRepeats = 2;
6366
6621
  };
@@ -6385,8 +6640,8 @@ function lessonCapture(options) {
6385
6640
  counts.set(lesson.slug, n);
6386
6641
  if (n < o.minRepeats) return;
6387
6642
  written.add(lesson.slug);
6388
- await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log10.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
6389
- log10.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
6643
+ await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log11.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
6644
+ log11.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
6390
6645
  }
6391
6646
  };
6392
6647
  }
@@ -6394,7 +6649,7 @@ function lessonCapture(options) {
6394
6649
  // src/reflect.ts
6395
6650
  init_llm();
6396
6651
  init_logging();
6397
- var log11 = forComponent("Reflect");
6652
+ var log12 = forComponent("Reflect");
6398
6653
  async function reflectOnRun(o) {
6399
6654
  const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
6400
6655
  if (!digest.trim()) return null;
@@ -6410,7 +6665,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
6410
6665
  const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
6411
6666
  text = r?.content ?? "";
6412
6667
  } catch (e) {
6413
- log11.warn(`reflection call failed: ${e?.message ?? e}`);
6668
+ log12.warn(`reflection call failed: ${e?.message ?? e}`);
6414
6669
  return null;
6415
6670
  }
6416
6671
  const m = text.match(/LESSON:\s*(.+)/i);
@@ -6420,10 +6675,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
6420
6675
  try {
6421
6676
  await writeFact(o.fs, o.dir, slug2, lesson);
6422
6677
  } catch (e) {
6423
- log11.warn(`could not persist lesson: ${e?.message ?? e}`);
6678
+ log12.warn(`could not persist lesson: ${e?.message ?? e}`);
6424
6679
  return null;
6425
6680
  }
6426
- log11.debug(`reflection persisted ${slug2}`);
6681
+ log12.debug(`reflection persisted ${slug2}`);
6427
6682
  return slug2;
6428
6683
  }
6429
6684
  function digestRun(messages, maxChars) {
@@ -6472,7 +6727,7 @@ function forComponent2(name) {
6472
6727
  }
6473
6728
 
6474
6729
  // node_modules/@bod.ee/voice/src/core/emotion.ts
6475
- var log12 = forComponent2("Emotion");
6730
+ var log13 = forComponent2("Emotion");
6476
6731
  var EMOTIONS = [
6477
6732
  // primary (best results)
6478
6733
  "neutral",
@@ -6626,7 +6881,7 @@ var EmotionStream = class {
6626
6881
  if (tag) {
6627
6882
  this.pending = tag;
6628
6883
  if (this.show) display += m[0];
6629
- } else log12.debug(`dropping unknown emotion tag ${m[0]}`);
6884
+ } else log13.debug(`dropping unknown emotion tag ${m[0]}`);
6630
6885
  last = m.index + m[0].length;
6631
6886
  }
6632
6887
  this.emit(body.slice(last), (s, d, p) => {
@@ -6773,7 +7028,7 @@ function computeHoldMs(ctx) {
6773
7028
  }
6774
7029
 
6775
7030
  // node_modules/@bod.ee/voice/src/core/engine.ts
6776
- var log13 = forComponent2("VoiceEngine");
7031
+ var log14 = forComponent2("VoiceEngine");
6777
7032
  var realClock = {
6778
7033
  now: () => performance.now(),
6779
7034
  setTimeout: (fn, ms) => setTimeout(fn, ms),
@@ -7161,8 +7416,8 @@ var VoiceEngine = class _VoiceEngine {
7161
7416
  this.tts.warmup?.();
7162
7417
  this.setState("listening");
7163
7418
  if (this.options.bargeInMode === "aec" && !this.stt.usingAec)
7164
- log13.warn("bargeInMode=aec but mic does not report echoCancellation applied \u2014 STT may transcribe the assistant");
7165
- log13.debug(`voice I/O up (${this.usingAec ? "AEC" : "heuristic echo"} capture, mode=${this.options.bargeInMode}, mic reports aec=${this.stt.usingAec})`);
7419
+ log14.warn("bargeInMode=aec but mic does not report echoCancellation applied \u2014 STT may transcribe the assistant");
7420
+ log14.debug(`voice I/O up (${this.usingAec ? "AEC" : "heuristic echo"} capture, mode=${this.options.bargeInMode}, mic reports aec=${this.stt.usingAec})`);
7166
7421
  }
7167
7422
  /** The RESOLVED barge-in tier (the single seam every tier branch reads). `bargeInMode` overrides
7168
7423
  * the mic's self-report — see the option doc for why the report is not trustworthy on mobile. */
@@ -7187,7 +7442,7 @@ var VoiceEngine = class _VoiceEngine {
7187
7442
  this.options.onDiag({ t: this.clock.now(), kind, ...fields });
7188
7443
  } catch (e) {
7189
7444
  this.diagOn = false;
7190
- log13.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
7445
+ log14.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
7191
7446
  }
7192
7447
  }
7193
7448
  idleWaiters = [];
@@ -7326,7 +7581,7 @@ var VoiceEngine = class _VoiceEngine {
7326
7581
  }
7327
7582
  for (const w of this.words(this.reply)) this.echoWords.add(w);
7328
7583
  this.tts.speak(forSpeech(speech), true);
7329
- if (!this.spokeDeltas && this.turnStartAt) log13.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
7584
+ if (!this.spokeDeltas && this.turnStartAt) log14.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
7330
7585
  this.spokeDeltas = true;
7331
7586
  this.setState("speaking");
7332
7587
  if (this.options.revealMode !== "off" && display) {
@@ -7439,13 +7694,13 @@ var VoiceEngine = class _VoiceEngine {
7439
7694
  this.drainTimer = null;
7440
7695
  this.speaking = false;
7441
7696
  this.stopWordReveal(true);
7442
- if (this.turnStartAt) log13.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
7697
+ if (this.turnStartAt) log14.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
7443
7698
  this.echoUntil = this.clock.now() + 2500;
7444
7699
  if (!this.usingAec) this.stt.reset();
7445
7700
  try {
7446
7701
  this.options.onSpeechEnd();
7447
7702
  } catch (e) {
7448
- log13.warn(`onSpeechEnd threw: ${e instanceof Error ? e.message : e}`);
7703
+ log14.warn(`onSpeechEnd threw: ${e instanceof Error ? e.message : e}`);
7449
7704
  }
7450
7705
  this.setState("listening");
7451
7706
  if (this.uttQueue.length) this.pumpQueue();
@@ -7519,7 +7774,7 @@ var VoiceEngine = class _VoiceEngine {
7519
7774
  if (!host) return pick;
7520
7775
  const ctx = { text: this.lastDispatchText, question: this.lastDispatchWasQuestion, barged: this.bargedBeforeDispatch };
7521
7776
  const fallback = (e) => {
7522
- log13.warn(`pickAck failed \u2014 using the default pools: ${e instanceof Error ? e.message : e}`);
7777
+ log14.warn(`pickAck failed \u2014 using the default pools: ${e instanceof Error ? e.message : e}`);
7523
7778
  pick.picked = false;
7524
7779
  return void 0;
7525
7780
  };
@@ -7609,7 +7864,7 @@ var VoiceEngine = class _VoiceEngine {
7609
7864
  this.ackProbe = null;
7610
7865
  const queued = this.uttQueue.splice(0);
7611
7866
  const droppedQueued = queued.length;
7612
- if (droppedQueued) log13.info(`barge-in cut ${droppedQueued} queued worker utterance(s) \u2014 handed to the host`);
7867
+ if (droppedQueued) log14.info(`barge-in cut ${droppedQueued} queued worker utterance(s) \u2014 handed to the host`);
7613
7868
  this.overlapKeptUntil = 0;
7614
7869
  if (!this.speaking && !this.drainTimer) {
7615
7870
  if (droppedQueued) this.lastInterrupted = { full: "", heard: "", queued, queuedTurn: false };
@@ -7838,7 +8093,7 @@ var VoiceEngine = class _VoiceEngine {
7838
8093
  if (flat && this.lastDispatchFlat && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS && (this.lastDispatchFlat.startsWith(flat) || flat.startsWith(this.lastDispatchFlat))) return;
7839
8094
  this.specSpent = true;
7840
8095
  this.specText = this.specPartial;
7841
- log13.debug(`speculate: "${this.specText.slice(0, 60)}"`);
8096
+ log14.debug(`speculate: "${this.specText.slice(0, 60)}"`);
7842
8097
  this.diag("speculate", { text: this.specText });
7843
8098
  this.options.onSpeculate(this.specText);
7844
8099
  }, this.options.speculativeMs);
@@ -7907,7 +8162,7 @@ var VoiceEngine = class _VoiceEngine {
7907
8162
  };
7908
8163
  this.tts.newContext();
7909
8164
  this.tts.speak(phrase, false);
7910
- log13.debug(`backchannel: "${phrase}"`);
8165
+ log14.debug(`backchannel: "${phrase}"`);
7911
8166
  this.diag("backchannel", { phrase });
7912
8167
  this.options.onBackchannel(phrase);
7913
8168
  }
@@ -7940,7 +8195,7 @@ var VoiceEngine = class _VoiceEngine {
7940
8195
  });
7941
8196
  if (idx < 0) return text;
7942
8197
  toks.splice(idx, 1);
7943
- log13.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
8198
+ log14.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
7944
8199
  return toks.join(" ");
7945
8200
  }
7946
8201
  /** Merge a resumed utterance into the pending one, deduping any word-overlap. Soniox re-finalizes
@@ -8126,7 +8381,7 @@ var VoiceEngine = class _VoiceEngine {
8126
8381
  }
8127
8382
  const bargeHold = !this.looksIncomplete(this.pendingUtt) && this.unfinishedBargeLine(this.pendingUtt);
8128
8383
  if (this.options.incompleteMergeMs && (bargeHold || this.looksIncomplete(this.pendingUtt))) {
8129
- log13.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
8384
+ log14.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
8130
8385
  this.diag("hold", { reason: bargeHold ? "barge_unfinished" : "incomplete", tail: this.pendingUtt.slice(-40) });
8131
8386
  this.options.onHold();
8132
8387
  if (this.options.holdFiller && !this.speaking) this.speakFiller(this.options.holdFiller);
@@ -8165,13 +8420,13 @@ var VoiceEngine = class _VoiceEngine {
8165
8420
  const answered = this.answeredSinceDispatch();
8166
8421
  const part = flat !== this.lastDispatchFlat && !answered && this.echoesDispatch(text);
8167
8422
  if (part || flat && flat === this.lastDispatchFlat && !answered && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS) {
8168
- log13.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
8423
+ log14.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
8169
8424
  this.diag("utterance_drop", { reason: "dup_final", text: text.slice(0, 60) });
8170
8425
  return;
8171
8426
  }
8172
8427
  const refinal = this.refinalizes(text);
8173
8428
  if (refinal && this.repliedSinceDispatch) {
8174
- log13.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
8429
+ log14.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
8175
8430
  this.diag("utterance_drop", { reason: "refinalized", text: text.slice(0, 60) });
8176
8431
  this.lastDispatchFlat = flat;
8177
8432
  this.lastDispatchWords = text.trim().split(/\s+/);
@@ -8450,7 +8705,7 @@ var SpokenSplitter = class {
8450
8705
  };
8451
8706
 
8452
8707
  // node_modules/@bod.ee/voice/src/adapters/soniox.ts
8453
- var log14 = forComponent2("SonioxSTT");
8708
+ var log15 = forComponent2("SonioxSTT");
8454
8709
  var now = () => performance.now();
8455
8710
  var SonioxSTTOptions = class {
8456
8711
  auth = "";
@@ -8495,7 +8750,7 @@ var SonioxSTT = class {
8495
8750
  this.onDiag({ t: now(), kind, ...fields });
8496
8751
  } catch (e) {
8497
8752
  this.diagOn = false;
8498
- log14.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
8753
+ log15.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
8499
8754
  }
8500
8755
  }
8501
8756
  lastChunkAt = 0;
@@ -8537,11 +8792,11 @@ var SonioxSTT = class {
8537
8792
  this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
8538
8793
  this.ws.onclose = (ev) => {
8539
8794
  if (this.stopped) return;
8540
- log14.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
8795
+ log15.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
8541
8796
  this.diag("stt_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: true });
8542
8797
  this.reset();
8543
8798
  this.connectWs().catch((e) => {
8544
- log14.error(`soniox reconnect failed: ${e.message}`);
8799
+ log15.error(`soniox reconnect failed: ${e.message}`);
8545
8800
  this.diag("stt_reconnect_failed", { message: e.message });
8546
8801
  });
8547
8802
  };
@@ -8553,7 +8808,7 @@ var SonioxSTT = class {
8553
8808
  this.endpointTimer = setInterval(() => {
8554
8809
  const combined = (this.finalText + this.partialText).trim();
8555
8810
  if (!combined || now() - this.lastChangeAt < this.options.silenceEndpointMs) return;
8556
- if (this.firstTokenAt) log14.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
8811
+ if (this.firstTokenAt) log15.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
8557
8812
  this.reset();
8558
8813
  this.onUtterance(combined, now());
8559
8814
  }, 120);
@@ -8565,7 +8820,7 @@ var SonioxSTT = class {
8565
8820
  if (this.stopped) return;
8566
8821
  const ref = this.lastChunkAt || this.startedChunksAt;
8567
8822
  if (now() - ref > noAudioMs) {
8568
- log14.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
8823
+ log15.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
8569
8824
  this.diag("stt_watchdog_fatal", { noAudioMs });
8570
8825
  this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
8571
8826
  this.stop();
@@ -8588,7 +8843,7 @@ var SonioxSTT = class {
8588
8843
  handle(m) {
8589
8844
  if (m.error_message) {
8590
8845
  this.diag("stt_error", { message: String(m.error_message), code: m.error_code });
8591
- return log14.error(`soniox: ${m.error_message}`);
8846
+ return log15.error(`soniox: ${m.error_message}`);
8592
8847
  }
8593
8848
  let endpoint = false;
8594
8849
  for (const t of m.tokens ?? []) {
@@ -8605,7 +8860,7 @@ var SonioxSTT = class {
8605
8860
  this.onPartial(combined);
8606
8861
  if (endpoint && this.finalText.trim()) {
8607
8862
  const utterance = this.finalText.trim();
8608
- if (this.firstTokenAt) log14.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
8863
+ if (this.firstTokenAt) log15.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
8609
8864
  this.reset();
8610
8865
  this.onUtterance(utterance, now());
8611
8866
  }
@@ -8627,7 +8882,7 @@ var SonioxSTT = class {
8627
8882
  };
8628
8883
 
8629
8884
  // node_modules/@bod.ee/voice/src/adapters/cartesia.ts
8630
- var log15 = forComponent2("CartesiaTTS");
8885
+ var log16 = forComponent2("CartesiaTTS");
8631
8886
  var now2 = () => performance.now();
8632
8887
  var CartesiaTTSOptions = class {
8633
8888
  auth = "";
@@ -8663,7 +8918,7 @@ var CartesiaTTS = class _CartesiaTTS {
8663
8918
  this.onDiag({ t: now2(), kind, ...fields });
8664
8919
  } catch (e) {
8665
8920
  this.diagOn = false;
8666
- log15.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
8921
+ log16.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
8667
8922
  }
8668
8923
  }
8669
8924
  firstAudioAt = 0;
@@ -8698,11 +8953,11 @@ var CartesiaTTS = class _CartesiaTTS {
8698
8953
  this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
8699
8954
  });
8700
8955
  this.ws.onclose = (ev) => {
8701
- log15.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
8956
+ log16.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
8702
8957
  this.diag("tts_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: !this.closed });
8703
8958
  if (!this.closed) {
8704
8959
  this.connecting = this.doConnect().catch((e) => {
8705
- log15.error(`cartesia reconnect failed: ${e.message}`);
8960
+ log16.error(`cartesia reconnect failed: ${e.message}`);
8706
8961
  this.diag("tts_reconnect_failed", { message: e.message });
8707
8962
  });
8708
8963
  }
@@ -8730,12 +8985,12 @@ var CartesiaTTS = class _CartesiaTTS {
8730
8985
  this.down = true;
8731
8986
  this.downAt = now2();
8732
8987
  this.consecutiveOk = 0;
8733
- log15.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
8988
+ log16.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
8734
8989
  this.diag("tts_breaker_open", { errors: this.consecutiveErrors });
8735
8990
  this.onDone();
8736
8991
  this.startProbe();
8737
8992
  } else if (!this.down) {
8738
- (/No valid transcripts/i.test(m.message || "") ? log15.debug : log15.warn)(`cartesia: ${JSON.stringify(m)}`);
8993
+ (/No valid transcripts/i.test(m.message || "") ? log16.debug : log16.warn)(`cartesia: ${JSON.stringify(m)}`);
8739
8994
  }
8740
8995
  }
8741
8996
  };
@@ -8750,7 +9005,7 @@ var CartesiaTTS = class _CartesiaTTS {
8750
9005
  this.stopProbe();
8751
9006
  const downMs = this.downAt ? now2() - this.downAt : 0;
8752
9007
  this.diag("tts_breaker_close", { downMs: Math.round(downMs) });
8753
- (downMs < 2e3 ? log15.debug : log15.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
9008
+ (downMs < 2e3 ? log16.debug : log16.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
8754
9009
  }
8755
9010
  /** Ensure the WS is open before sending — reconnects if idle-closed. */
8756
9011
  async ensureConnected() {
@@ -8833,13 +9088,13 @@ function base64ToBytes(b64) {
8833
9088
  }
8834
9089
 
8835
9090
  // node_modules/@bod.ee/voice/src/adapters/fish.ts
8836
- var log16 = forComponent2("FishTTS");
9091
+ var log17 = forComponent2("FishTTS");
8837
9092
 
8838
9093
  // node_modules/@bod.ee/voice/src/adapters/grok.ts
8839
- var log17 = forComponent2("GrokTTS");
9094
+ var log18 = forComponent2("GrokTTS");
8840
9095
 
8841
9096
  // node_modules/@bod.ee/voice/src/adapters/grok-stt.ts
8842
- var log18 = forComponent2("GrokSTT");
9097
+ var log19 = forComponent2("GrokSTT");
8843
9098
 
8844
9099
  // src/voice/engine.ts
8845
9100
  init_logging();
@@ -8851,7 +9106,7 @@ configureLogging((name) => {
8851
9106
 
8852
9107
  // src/voice/instinct.ts
8853
9108
  init_logging();
8854
- var log19 = forComponent("Instinct");
9109
+ var log20 = forComponent("Instinct");
8855
9110
  var ACK_SITUATIONS = {
8856
9111
  hard_question: `a question that takes real thought, judgment or looking something up ("what's the biggest takeaway?", "how should I structure this?")`,
8857
9112
  simple_question: 'a quick question with an instant, obvious answer ("how are you?", "what time is it?", "can you hear me?")',
@@ -8890,7 +9145,7 @@ function interruptionNote(cut, intent) {
8890
9145
 
8891
9146
  // src/voice/unsaid.ts
8892
9147
  init_logging();
8893
- var log20 = forComponent("Unsaid");
9148
+ var log21 = forComponent("Unsaid");
8894
9149
  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(" "));
8895
9150
  var contentStems = (s) => (s.toLowerCase().match(/[a-z0-9']+/g) ?? []).filter((w) => w.length >= 4 && !STOP2.has(w)).map((w) => w.slice(0, 4));
8896
9151
  var SAID_OVERLAP = 0.6;
@@ -8930,7 +9185,7 @@ async function unsaidClauses(pieces, spoken, decider, timeoutMs = 5e3) {
8930
9185
  return p < CONVEYED_MIN;
8931
9186
  }).map((x) => x.c);
8932
9187
  } catch (e) {
8933
- log20.warn(`unsaid judge failed \u2014 only near-zero-overlap clauses count as unsaid: ${e.message}`);
9188
+ log21.warn(`unsaid judge failed \u2014 only near-zero-overlap clauses count as unsaid: ${e.message}`);
8934
9189
  return fallback();
8935
9190
  } finally {
8936
9191
  clearTimeout(timer);
@@ -8970,15 +9225,16 @@ var STRICT = [
8970
9225
  ];
8971
9226
  var SENTENCE_END = new RegExp(`[.!?\u2026]+["')\\]]*(?:\\s|(?=\\p{Lu}|\\[))`, "gu");
8972
9227
  var SENTENCE_SPLIT = new RegExp(`(?<=[.!?\u2026]+["')\\]]*)(?:\\s+|(?=\\p{Lu}|\\[))`, "u");
9228
+ var BRACKET_NOTE = /^\s*\[\s*(?:[^\]\s]+\s+){3,}[^\]]*(?:\]\s*)?$/;
8973
9229
  var clean2 = (s) => s.replace(/\[[a-z_ ]+\]\s*/gi, "").trim();
8974
9230
  function isLeakSuspect(sentence) {
8975
9231
  const s = clean2(sentence);
8976
- return SUSPECT.some((re) => re.test(s));
9232
+ return BRACKET_NOTE.test(sentence) || SUSPECT.some((re) => re.test(s));
8977
9233
  }
8978
9234
  function isStrictLeak(sentence) {
8979
9235
  return sentence.split(SENTENCE_SPLIT).some((x) => {
8980
9236
  const s = clean2(x);
8981
- return STRICT.some((re) => re.test(s));
9237
+ return BRACKET_NOTE.test(x) || STRICT.some((re) => re.test(s));
8982
9238
  });
8983
9239
  }
8984
9240
  var JUDGE_INSTRUCTIONS = `This sentence was produced by a voice assistant's reply stream and is about to be read aloud to the user. Is it the assistant's private scratchpad reasoning that leaked out \u2014 talking ABOUT the conversation instead of TO the listener? Leaked reasoning: refers to the listener in the third person ("the user wants\u2026", "user said\u2026"), plans its own reply ("we need to respond\u2026", "we should output\u2026", "I should answer briefly"), cites its own guidelines/instructions, or narrates its internal tool calls and task events (QuickLook, Act, Think, Hold, TaskStatus, "t2 completed") as steps it is taking. Speech: anything said TO the listener \u2014 answers, questions, offers, news, advice, explaining what it can do when asked, even if it mentions a user, an assistant, a policy, instructions, replying, or a tool.`;
@@ -9000,7 +9256,7 @@ async function judgeLeak(decider, sentence, userSaid) {
9000
9256
  }
9001
9257
 
9002
9258
  // src/duplex.ts
9003
- var log21 = forComponent("DuplexAgent");
9259
+ var log22 = forComponent("DuplexAgent");
9004
9260
  function describeCall(call) {
9005
9261
  const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
9006
9262
  const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
@@ -9260,7 +9516,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9260
9516
  const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
9261
9517
  if (m) {
9262
9518
  this.fabricationCut = true;
9263
- log21.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
9519
+ log22.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
9264
9520
  this.reflexBuf = this.reflexBuf.slice(0, Math.max(m.index, this.reflexForwarded));
9265
9521
  this.pumpReflex(true);
9266
9522
  return;
@@ -9352,7 +9608,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9352
9608
  const at = from + ok;
9353
9609
  this.leakHit = true;
9354
9610
  this.leakCuts.push(leak.trim().slice(0, 40));
9355
- log21.warn(`reflex spoke its own analysis ("${leak.trim().slice(0, 40)}\u2026") \u2014 cut before TTS`);
9611
+ log22.warn(`reflex spoke its own analysis ("${leak.trim().slice(0, 40)}\u2026") \u2014 cut before TTS`);
9356
9612
  this.notify("diag", "analysis_leak", { text: leak.trim().slice(0, 80), at: this.turnSpoken.trim() ? "mid_reply" : "before_speech" });
9357
9613
  const next = this.stepMarks.find((k) => k > at);
9358
9614
  if (next === void 0) {
@@ -9392,7 +9648,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9392
9648
  this.notify("diag", "leak_judge", { text: s.slice(0, 80), p: Math.round(p * 100) / 100, ms: Math.round(performance.now() - t0) });
9393
9649
  return p >= LEAK_JUDGE_THRESHOLD;
9394
9650
  }, (e) => {
9395
- log21.warn(`leak judge failed \u2014 speaking the sentence: ${e.message}`);
9651
+ log22.warn(`leak judge failed \u2014 speaking the sentence: ${e.message}`);
9396
9652
  return false;
9397
9653
  }).then((leak) => {
9398
9654
  clearTimeout(timer);
@@ -9426,7 +9682,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9426
9682
  let i = t.length - 1;
9427
9683
  while (i >= this.turnBase && !(t[i].role === "assistant" && typeof t[i].content === "string" && t[i].content.includes(key))) i--;
9428
9684
  if (i < this.turnBase) {
9429
- log21.warn("analysis leak cut, but its text was not found in the transcript to scrub");
9685
+ log22.warn("analysis leak cut, but its text was not found in the transcript to scrub");
9430
9686
  continue;
9431
9687
  }
9432
9688
  const m = t[i];
@@ -9506,7 +9762,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9506
9762
  return {
9507
9763
  preToolUse: (call) => {
9508
9764
  if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
9509
- log21.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
9765
+ log22.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
9510
9766
  this.abortSpeculation();
9511
9767
  return { block: true, reason: "Speculative turn aborted." };
9512
9768
  }
@@ -9551,7 +9807,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9551
9807
  try {
9552
9808
  await this.voice.send(reminder);
9553
9809
  } catch (e) {
9554
- log21.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
9810
+ log22.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
9555
9811
  } finally {
9556
9812
  this.nudging = false;
9557
9813
  const i = this.voice.transcript.findIndex((m, k) => k >= base && m.role === "user" && m.content === reminder);
@@ -9581,12 +9837,17 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9581
9837
  * `opts.id` names the turn; `opts.replaces` names an earlier one this content REPLACES — the voice engine's
9582
9838
  * fuller re-finalization of an utterance whose reply had not started ("…previous" → "…previous
9583
9839
  * conversation."). The replaced turn is skipped if it has not run yet, else aborted and rolled back out of
9584
- * the transcript (kept only if it already dispatched work) — one reply, to the full text. */
9840
+ * the transcript (kept only if it already dispatched work) — one reply, to the full text. `opts.followUp` = the new words
9841
+ * beyond the replaced turn (a string), or beyond each earlier turn of a re-final chain ({turnId: words}): when the nearest
9842
+ * turn still in the transcript is KEPT, its words run instead of the merged text (which would repeat its request — a
9843
+ * second Act). The host gets `turn_replace` {id, replaces, rolledBack, followed?}: only a rollback drops the replaced
9844
+ * line; `followed` = the kept turn whose follow-up ran (absent: the merged text ran). */
9585
9845
  send(content, opts) {
9586
9846
  if (opts?.replaces) this.replaceTurn(opts.replaces);
9587
9847
  const cur = this.voice.options.signal;
9588
9848
  const ext = cur && cur === this.turnSignal?.own ? this.turnSignal.ext : cur;
9589
9849
  const rec = opts?.id ? this.trackTurn(opts.id) : void 0;
9850
+ if (rec && opts?.replaces && typeof content === "string") rec.replaces = { id: opts.id, of: opts.replaces, content, followUp: typeof opts.followUp === "string" ? { [opts.replaces]: opts.followUp } : opts.followUp ?? {} };
9590
9851
  if (rec?.superseded) {
9591
9852
  this.notify("diag", "turn_replaced", { id: opts.id, ran: false });
9592
9853
  return Promise.resolve(_DuplexAgent.replacedResult(this.voice.transcript));
@@ -9598,7 +9859,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9598
9859
  const accepted = this.offerOpen && this.parkedRedeliver.length > 0 && this.parkedRedeliver.every((p) => p.task === this.offer.task) && (isOfferAcceptance(content) || isTrivialBarge(content));
9599
9860
  const judge = accepted ? void 0 : this.options.bargeIntent;
9600
9861
  return (accepted ? Promise.resolve("resume") : judge ? judge(content).catch((e) => {
9601
- log21.warn(`bargeIntent judge failed \u2014 using the allowlist: ${e.message}`);
9862
+ log22.warn(`bargeIntent judge failed \u2014 using the allowlist: ${e.message}`);
9602
9863
  return fallback();
9603
9864
  }) : Promise.resolve(fallback())).then((intent) => {
9604
9865
  this.notify("diag", "barge_intent", { intent, text: content.slice(0, 80) });
@@ -9644,6 +9905,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9644
9905
  if (rec.done) {
9645
9906
  const drop = rec.dropped;
9646
9907
  rec.dropped = void 0;
9908
+ rec.kept = !drop?.length;
9647
9909
  if (drop?.length) void this.enqueue(async () => {
9648
9910
  const t = this.voice.transcript;
9649
9911
  for (const m of drop) {
@@ -9665,12 +9927,13 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9665
9927
  * carries the user's full words) — unless it already dispatched work or SPOKE: the task is real, and the user
9666
9928
  * heard those words, so both stay visible to the replacing turn. An aborted stream never reaches the transcript,
9667
9929
  * so what was spoken is recorded as the replaced turn's reply. */
9668
- dropReplacedTurn() {
9930
+ dropReplacedTurn(rec) {
9669
9931
  this.fabricationCut = true;
9670
9932
  this.reflexBuf = this.reflexBuf.slice(0, this.reflexForwarded);
9671
9933
  this.dropJudge();
9672
9934
  const spoke = this.turnSpoken.trim();
9673
9935
  const rolledBack = !this.turnDispatched && !spoke;
9936
+ rec.kept = !rolledBack;
9674
9937
  if (rolledBack) this.voice.transcript.length = this.turnBase;
9675
9938
  else if (spoke) {
9676
9939
  const t = this.voice.transcript;
@@ -9714,6 +9977,22 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9714
9977
  this.parkedRedeliver = [];
9715
9978
  for (const t of this.tasks.values()) if (t.deliveryParked === "barge") t.deliveryParked = "moved_on";
9716
9979
  }
9980
+ if (rec?.replaces) {
9981
+ const { id, of, content: said, followUp } = rec.replaces;
9982
+ let base;
9983
+ for (let t = of, n = 0; t && n < 32; n++) {
9984
+ const r = this.userTurns.get(t);
9985
+ if (r?.kept) {
9986
+ base = t;
9987
+ break;
9988
+ }
9989
+ t = r?.replaces?.of;
9990
+ }
9991
+ const fu = base !== void 0 ? followUp[base] : void 0;
9992
+ if (fu !== void 0 && typeof content === "string" && content.startsWith(said)) content = fu + content.slice(said.length);
9993
+ else base = void 0;
9994
+ this.notify("turn_replace", of, { id, replaces: of, rolledBack: !this.userTurns.get(of)?.kept, ...base !== void 0 ? { followed: base } : {} });
9995
+ }
9717
9996
  if (rec?.superseded) {
9718
9997
  this.notify("diag", "turn_replaced", { ran: false });
9719
9998
  return _DuplexAgent.replacedResult(this.voice.transcript);
@@ -9733,7 +10012,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9733
10012
  }
9734
10013
  if (rec) rec.done = true;
9735
10014
  }
9736
- if (rec?.superseded) return this.dropReplacedTurn();
10015
+ if (rec?.superseded) return this.dropReplacedTurn(rec);
9737
10016
  if (rec && res.finishReason === "aborted" && !this.turnDispatched && !this.turnSpoken.trim()) rec.dropped = this.voice.transcript.slice(this.turnBase);
9738
10017
  await this.endTurn(res);
9739
10018
  return res;
@@ -9776,7 +10055,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9776
10055
  try {
9777
10056
  res = await this.voice.send(spec.text);
9778
10057
  } catch (e) {
9779
- log21.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
10058
+ log22.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
9780
10059
  } finally {
9781
10060
  this.voice.options.signal = prevSignal;
9782
10061
  }
@@ -9790,7 +10069,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9790
10069
  if (d === "abort") {
9791
10070
  if (this.voice.transcript.length > base) this.voice.transcript.length = base;
9792
10071
  this.speculativeAbortedCalls++;
9793
- log21.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
10072
+ log22.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
9794
10073
  if (this.spec === spec) this.spec = void 0;
9795
10074
  return res ?? empty;
9796
10075
  }
@@ -9803,7 +10082,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
9803
10082
  }
9804
10083
  this.spec = void 0;
9805
10084
  if (spec.rec) spec.rec.done = true;
9806
- if (spec.rec?.superseded) return this.dropReplacedTurn();
10085
+ if (spec.rec?.superseded) return this.dropReplacedTurn(spec.rec);
9807
10086
  await this.endTurn(res);
9808
10087
  return res ?? empty;
9809
10088
  });
@@ -10140,7 +10419,7 @@ Another agent just implemented the above. Independently check the CURRENT state
10140
10419
  this.notify("task_verify", `task ${id}: verifying`, { id });
10141
10420
  const cres = await new Agent(checkerOpts).run(checkBrief);
10142
10421
  if (cres.finishReason !== "stop") {
10143
- log21.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
10422
+ log22.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
10144
10423
  this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
10145
10424
  }
10146
10425
  const sum = (a = 0, b = 0) => a + b;
@@ -10276,7 +10555,7 @@ Another agent just implemented the above. Independently check the CURRENT state
10276
10555
  rec.status = "done";
10277
10556
  rec.result = res.text;
10278
10557
  const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
10279
- log21.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
10558
+ log22.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
10280
10559
  this.notify("task_done", `task ${id} (${rec.label}) completed`, {
10281
10560
  id,
10282
10561
  text: res.text,
@@ -10310,14 +10589,14 @@ Another agent just implemented the above. Independently check the CURRENT state
10310
10589
  foldIfSuperseded(rec, note) {
10311
10590
  if (rec.deliveryParked !== "moved_on") return false;
10312
10591
  if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
10313
- log21.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
10592
+ log22.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
10314
10593
  return true;
10315
10594
  }
10316
10595
  failTask(rec, msg) {
10317
10596
  this.dropAsk(rec.id);
10318
10597
  rec.status = "error";
10319
10598
  rec.result = msg;
10320
- log21.warn(`task ${rec.id} failed: ${msg}`);
10599
+ log22.warn(`task ${rec.id} failed: ${msg}`);
10321
10600
  this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`, { id: rec.id });
10322
10601
  if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
10323
10602
  this.dropParked(rec);
@@ -10556,7 +10835,7 @@ Another agent just implemented the above. Independently check the CURRENT state
10556
10835
  // src/modelRouter.ts
10557
10836
  init_llm();
10558
10837
  init_logging();
10559
- var log22 = forComponent("ModelRouter");
10838
+ var log23 = forComponent("ModelRouter");
10560
10839
  var ModelRouterOptions = class {
10561
10840
  classifier = jevClassifier();
10562
10841
  /** Capability tiers → model id (any provider). basic/standard run the main session; premium is only ever an offload. */
@@ -10640,10 +10919,10 @@ var ModelRouter = class {
10640
10919
  if (tier === "premium") d.offload = this.state.offload && (c.sameThread ?? 0) >= o.sameThreadAt ? "continue" : "fresh";
10641
10920
  } catch (e) {
10642
10921
  const msg = e instanceof Error ? e.message : String(e);
10643
- log22.warn(`classifier failed, falling back to standard: ${msg}`);
10922
+ log23.warn(`classifier failed, falling back to standard: ${msg}`);
10644
10923
  d = { tier: "standard", model: o.models.standard, probs: {}, fallback: msg, classifyMs: Math.round(performance.now() - t0) };
10645
10924
  }
10646
- log22.info(`route ${d.tier}${d.offload ? `(${d.offload})` : ""} \xB7 argmax=${d.choice ?? "-"} \xB7 p=${JSON.stringify(d.probs)}${d.sameThread != null ? ` \xB7 same=${d.sameThread.toFixed(2)}` : ""} \xB7 ${d.classifyMs}ms`);
10925
+ log23.info(`route ${d.tier}${d.offload ? `(${d.offload})` : ""} \xB7 argmax=${d.choice ?? "-"} \xB7 p=${JSON.stringify(d.probs)}${d.sameThread != null ? ` \xB7 same=${d.sameThread.toFixed(2)}` : ""} \xB7 ${d.classifyMs}ms`);
10647
10926
  return d;
10648
10927
  }
10649
10928
  /** Run one user turn on `agent` with routing. Returns a RunResult carrying `costUsd` + `routing`. */
@@ -10695,7 +10974,7 @@ ${text}`;
10695
10974
  const answer = res.text || `(premium sub-agent finished with no reply; finishReason=${res.finishReason})`;
10696
10975
  agent.transcript.push({ role: "user", content }, { role: "assistant", content: answer });
10697
10976
  this.state.offload = { messages: child.transcript, mainLen: agent.transcript.length, question: text };
10698
- log22.info(`offload ${cont ? "continue" : "fresh"} done \xB7 ${res.finishReason} \xB7 ${res.steps} steps`);
10977
+ log23.info(`offload ${cont ? "continue" : "fresh"} done \xB7 ${res.finishReason} \xB7 ${res.steps} steps`);
10699
10978
  const costUsd = o.costOf(main, briefUsage) + o.costOf(o.models.premium, res.usage);
10700
10979
  return { ...res, text: answer, messages: agent.transcript, usage: addUsage(briefUsage, res.usage), costUsd, routing: d };
10701
10980
  }
@@ -10711,7 +10990,7 @@ The request:
10711
10990
  ${text}`);
10712
10991
  return { text: r.text.trim() || text, usage: r.usage };
10713
10992
  } catch (e) {
10714
- log22.warn(`brief failed, sending the raw request: ${e instanceof Error ? e.message : String(e)}`);
10993
+ log23.warn(`brief failed, sending the raw request: ${e instanceof Error ? e.message : String(e)}`);
10715
10994
  return { text };
10716
10995
  } finally {
10717
10996
  agent.transcript = snap;
@@ -10743,8 +11022,8 @@ async function loadJev(opts) {
10743
11022
  const env = { ...process.env };
10744
11023
  const file = opts.envFile ?? process.env.AGENTX_JEV_ENV;
10745
11024
  if (file) {
10746
- const { readFileSync: readFileSync3 } = await import("fs");
10747
- for (const l of readFileSync3(file, "utf8").split("\n")) {
11025
+ const { readFileSync: readFileSync4 } = await import("fs");
11026
+ for (const l of readFileSync4(file, "utf8").split("\n")) {
10748
11027
  const m = l.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*?)\s*$/);
10749
11028
  if (m && !env[m[1]]) env[m[1]] = m[2].replace(/^['"]|['"]$/g, "");
10750
11029
  }
@@ -10756,7 +11035,7 @@ async function jevAvailable(opts = {}) {
10756
11035
  const { env } = await loadJev(opts);
10757
11036
  return JEV_KEYS.some((k) => env[k]?.trim());
10758
11037
  } catch (e) {
10759
- log22.debug(`jev unavailable: ${e instanceof Error ? e.message : String(e)}`);
11038
+ log23.debug(`jev unavailable: ${e instanceof Error ? e.message : String(e)}`);
10760
11039
  return false;
10761
11040
  }
10762
11041
  }