@livx.cc/agentx 0.99.26 → 0.99.29

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
@@ -8,6 +8,274 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
+ // src/logging.ts
12
+ import { log } from "libx.js/src/modules/log";
13
+ var forComponent;
14
+ var init_logging = __esm({
15
+ "src/logging.ts"() {
16
+ "use strict";
17
+ forComponent = (name) => log.forComponent(name);
18
+ }
19
+ });
20
+
21
+ // src/llm.ts
22
+ import { readFileSync, statSync, realpathSync } from "fs";
23
+ import { resolve, sep } from "path";
24
+ import { randomBytes } from "crypto";
25
+ function contentText(content) {
26
+ if (content == null) return "";
27
+ if (typeof content === "string") return content;
28
+ return content.map((p) => p.type === "text" ? p.text ?? "" : "[image]").join(p_sep(content));
29
+ }
30
+ function imagePart(url) {
31
+ return { type: "image_url", image_url: { url } };
32
+ }
33
+ function messageHasInlineImage(content) {
34
+ if (content == null || typeof content === "string") return false;
35
+ return content.some((p) => p.type === "image_url" && (p.image_url?.url ?? "").startsWith("data:"));
36
+ }
37
+ function contentBytes(content) {
38
+ if (content == null) return 0;
39
+ if (typeof content === "string") return content.length;
40
+ let n = 0;
41
+ for (const p of content) {
42
+ if (p.type === "text") {
43
+ n += (p.text ?? "").length;
44
+ continue;
45
+ }
46
+ const url = p.image_url?.url ?? "";
47
+ const ref = parseImageRef(url);
48
+ n += (ref ? ref.bytes : url.length) || 7;
49
+ }
50
+ return n;
51
+ }
52
+ function elideInlineImages(content, stub = IMAGE_ELIDE_STUB) {
53
+ return elideImageParts(content, (url) => url.startsWith("data:"), stub);
54
+ }
55
+ function elideImages(content, stub = IMAGE_ELIDE_STUB) {
56
+ return elideImageParts(content, (url) => url.startsWith("data:") || isImageRefUrl(url), stub);
57
+ }
58
+ function elideImageParts(content, hit, stub) {
59
+ if (typeof content === "string") return content;
60
+ let changed = false;
61
+ const out = [];
62
+ for (const p of content) {
63
+ if (p.type !== "image_url" || !hit(p.image_url?.url ?? "")) {
64
+ out.push(p);
65
+ continue;
66
+ }
67
+ changed = true;
68
+ out.push({ type: "text", text: stub });
69
+ }
70
+ return changed ? out : content;
71
+ }
72
+ function mintImageRef(path, mime) {
73
+ const token = randomBytes(16).toString("hex");
74
+ let bytes = 0;
75
+ try {
76
+ bytes = Math.ceil(statSync(path).size * 4 / 3);
77
+ } catch (e) {
78
+ log2.warn(`image ref stat failed, weighing as 0: ${path}`, e);
79
+ }
80
+ refRegistry.set(token, { mime, path, bytes });
81
+ while (refRegistry.size > REF_REGISTRY_MAX) refRegistry.delete(refRegistry.keys().next().value);
82
+ return `${IMAGE_REF_SCHEME}${mime};${token};${path}`;
83
+ }
84
+ function clearImageRefs() {
85
+ refRegistry.clear();
86
+ }
87
+ function imageRefPart(path, mime) {
88
+ return { type: "image_url", image_url: { url: mintImageRef(path, mime) } };
89
+ }
90
+ function parseImageRef(url) {
91
+ const parts = refUrlParts(url);
92
+ const ref = parts && refRegistry.get(parts.token);
93
+ if (!ref) {
94
+ log2.debug("image ref not in registry \u2014 treating as non-image text");
95
+ return null;
96
+ }
97
+ return ref;
98
+ }
99
+ function refUrlParts(url) {
100
+ if (!isImageRefUrl(url)) return null;
101
+ const rest = url.slice(IMAGE_REF_SCHEME.length);
102
+ const a = rest.indexOf(";");
103
+ if (a < 0) return null;
104
+ const b = rest.indexOf(";", a + 1);
105
+ return { mime: rest.slice(0, a), token: b < 0 ? rest.slice(a + 1) : rest.slice(a + 1, b), path: b < 0 ? "" : rest.slice(b + 1) };
106
+ }
107
+ function remintImageRefs(messages, roots) {
108
+ const allowed = [];
109
+ for (const r of roots) {
110
+ try {
111
+ allowed.push(realpathSync(resolve(r)));
112
+ } catch (e) {
113
+ log2.debug(`image-ref root unavailable: ${r}`, e);
114
+ }
115
+ }
116
+ if (allowed.length === 0) return messages;
117
+ const inside = (p) => allowed.some((root) => p === root || p.startsWith(root + sep));
118
+ let changed = false;
119
+ const out = messages.map((m) => {
120
+ if (m.role !== "user" && m.role !== "assistant" || typeof m.content === "string" || m.content == null) return m;
121
+ let touched = false;
122
+ const parts = m.content.map((p) => {
123
+ if (p.type !== "image_url") return p;
124
+ const f = refUrlParts(p.image_url?.url);
125
+ if (!f || refRegistry.has(f.token)) return p;
126
+ let real;
127
+ try {
128
+ real = realpathSync(f.path);
129
+ } catch (e) {
130
+ log2.warn(`resumed image ref unreadable, will stub: ${f.path}`, e);
131
+ return p;
132
+ }
133
+ if (!inside(real)) {
134
+ log2.warn(`resumed image ref outside the allowed roots, will stub: ${f.path} -> ${real}`);
135
+ return p;
136
+ }
137
+ touched = true;
138
+ return { type: "image_url", image_url: { url: mintImageRef(real, f.mime) } };
139
+ });
140
+ if (!touched) return m;
141
+ changed = true;
142
+ return { ...m, content: parts };
143
+ });
144
+ return changed ? out : messages;
145
+ }
146
+ function messageHasImageRef(content) {
147
+ if (content == null || typeof content === "string") return false;
148
+ return content.some((p) => p.type === "image_url" && isImageRefUrl(p.image_url?.url));
149
+ }
150
+ function messageHasImage(content) {
151
+ return messageHasInlineImage(content) || messageHasImageRef(content);
152
+ }
153
+ function imageRefResult(path, mime) {
154
+ return JSON.stringify({ imageRef: mintImageRef(path, mime), image: path });
155
+ }
156
+ function toolImageRef(content) {
157
+ if (typeof content !== "string" || !content.startsWith('{"imageRef":')) return null;
158
+ try {
159
+ return parseImageRef(JSON.parse(content).imageRef);
160
+ } catch (e) {
161
+ log2.debug("bad imageRef tool result", e);
162
+ return null;
163
+ }
164
+ }
165
+ function elideStaleImages(messages, keepRecent = 1) {
166
+ const idx = messages.map((m, i) => messageHasImage(m.content) ? i : -1).filter((i) => i >= 0);
167
+ const drop = new Set(idx.slice(0, Math.max(0, idx.length - keepRecent)));
168
+ if (drop.size === 0) return messages;
169
+ let changed = false;
170
+ const out = messages.map((m, i) => {
171
+ if (!drop.has(i)) return m;
172
+ const c = elideInlineImages(m.content);
173
+ if (c === m.content) return m;
174
+ changed = true;
175
+ return { ...m, content: c };
176
+ });
177
+ return changed ? out : messages;
178
+ }
179
+ function imageWindows(messages, keepRecent) {
180
+ const attach = [];
181
+ const tool = [];
182
+ messages.forEach((m, i) => {
183
+ if (toolImageRef(m.content)) tool.push(i);
184
+ else if (messageHasImage(m.content)) attach.push(i);
185
+ });
186
+ const k = Math.max(0, keepRecent);
187
+ return /* @__PURE__ */ new Set([...attach.slice(Math.max(0, attach.length - k)), ...tool.slice(Math.max(0, tool.length - k))]);
188
+ }
189
+ function expandImagesForSend(messages, keepRecent = 1, stub = IMAGE_ELIDE_STUB) {
190
+ if (!messages.some(bearsImage)) return messages;
191
+ const keep = imageWindows(messages, keepRecent);
192
+ let changed = false;
193
+ const out = messages.map((m, i) => {
194
+ const content = keep.has(i) ? expandContent(m.content, stub) : stripContent(m.content, stub);
195
+ if (content === m.content) return m;
196
+ changed = true;
197
+ return { ...m, content };
198
+ });
199
+ return changed ? out : messages;
200
+ }
201
+ function sendBytes(messages, keepRecent = 1, stub = IMAGE_ELIDE_STUB) {
202
+ let n = 0;
203
+ for (const b of sendBytesPer(messages, keepRecent, stub)) n += b;
204
+ return n;
205
+ }
206
+ function sendBytesPer(messages, keepRecent = 1, stub = IMAGE_ELIDE_STUB) {
207
+ const keep = messages.some(bearsImage) ? imageWindows(messages, keepRecent) : null;
208
+ return messages.map((m, i) => wireBytes(m.content, keep == null || keep.has(i), stub) + (m.tool_calls ? JSON.stringify(m.tool_calls).length : 0));
209
+ }
210
+ function wireBytes(content, kept, stub) {
211
+ const tool = toolImageRef(content);
212
+ if (tool) return kept ? tool.bytes : stub.length;
213
+ if (content == null || typeof content === "string") return contentBytes(content);
214
+ if (kept) return contentBytes(content);
215
+ let n = 0;
216
+ for (const p of content) {
217
+ if (p.type === "text") {
218
+ n += (p.text ?? "").length;
219
+ continue;
220
+ }
221
+ const url = p.image_url?.url ?? "";
222
+ n += url.startsWith("data:") || isImageRefUrl(url) ? stub.length : url.length || 7;
223
+ }
224
+ return n;
225
+ }
226
+ function readRefDataUrl(ref) {
227
+ try {
228
+ return `data:${ref.mime};base64,${readFileSync(ref.path).toString("base64")}`;
229
+ } catch (e) {
230
+ log2.warn(`image ref unreadable, sending stub instead: ${ref.path}`, e);
231
+ return null;
232
+ }
233
+ }
234
+ function expandContent(content, stub) {
235
+ const tool = toolImageRef(content);
236
+ if (tool) {
237
+ const url = readRefDataUrl(tool);
238
+ return url ? JSON.stringify({ dataUrl: url, image: tool.path }) : stub;
239
+ }
240
+ if (typeof content === "string") return content;
241
+ let changed = false;
242
+ const out = content.map((p) => {
243
+ if (p.type !== "image_url" || !isImageRefUrl(p.image_url?.url)) return p;
244
+ changed = true;
245
+ const ref = parseImageRef(p.image_url?.url);
246
+ const url = ref && readRefDataUrl(ref);
247
+ return url ? imagePart(url) : { type: "text", text: stub };
248
+ });
249
+ return changed ? out : content;
250
+ }
251
+ function stripContent(content, stub) {
252
+ if (toolImageRef(content)) return stub;
253
+ if (typeof content === "string") return content;
254
+ let changed = false;
255
+ const out = content.map((p) => {
256
+ const url = p.type === "image_url" ? p.image_url?.url ?? "" : "";
257
+ if (!url.startsWith("data:") && !isImageRefUrl(url)) return p;
258
+ changed = true;
259
+ return { type: "text", text: stub };
260
+ });
261
+ return changed ? out : content;
262
+ }
263
+ var log2, p_sep, IMAGE_ELIDE_STUB, IMAGE_REF_SCHEME, REF_REGISTRY_MAX, refRegistry, isImageRefUrl, bearsImage;
264
+ var init_llm = __esm({
265
+ "src/llm.ts"() {
266
+ "use strict";
267
+ init_logging();
268
+ log2 = forComponent("llm");
269
+ p_sep = (parts) => parts.length > 1 ? "\n" : "";
270
+ IMAGE_ELIDE_STUB = "[image elided \u2014 re-attach @path or /paste to view again]";
271
+ IMAGE_REF_SCHEME = "agentx-image:";
272
+ REF_REGISTRY_MAX = 256;
273
+ refRegistry = /* @__PURE__ */ new Map();
274
+ isImageRefUrl = (url) => !!url && url.startsWith(IMAGE_REF_SCHEME);
275
+ bearsImage = (m) => messageHasImage(m.content) || toolImageRef(m.content) != null;
276
+ }
277
+ });
278
+
11
279
  // src/redact.ts
12
280
  function redactSecrets(text) {
13
281
  if (!text) return text;
@@ -474,16 +742,6 @@ var init_todo = __esm({
474
742
  }
475
743
  });
476
744
 
477
- // src/logging.ts
478
- import { log } from "libx.js/src/modules/log";
479
- var forComponent;
480
- var init_logging = __esm({
481
- "src/logging.ts"() {
482
- "use strict";
483
- forComponent = (name) => log.forComponent(name);
484
- }
485
- });
486
-
487
745
  // src/tools.web.ts
488
746
  function htmlToText(html) {
489
747
  let s = html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<title[\s\S]*?<\/title>/gi, " ").replace(/<noscript[\s\S]*?<\/noscript>/gi, " ").replace(/<textarea[\s\S]*?<\/textarea>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<\/(p|div|li|h[1-6]|tr|section|article|header|footer|nav)>/gi, "\n").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, " ");
@@ -605,7 +863,7 @@ function makeWebFetchTool(options = {}) {
605
863
 
606
864
  ${capped}`;
607
865
  } catch (e) {
608
- log2.debug(`WebFetch ${u} failed`, e);
866
+ log3.debug(`WebFetch ${u} failed`, e);
609
867
  return `Error fetching ${u}: ${e?.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e?.message ?? e}`;
610
868
  } finally {
611
869
  clearTimeout(timer);
@@ -729,7 +987,7 @@ function makeWebSearchTool(options = {}) {
729
987
  if (!res.ok) return `Error: search returned ${res.status} ${res.statusText}`;
730
988
  return formatHits(parseDdgHtml(await res.text(), maxResults));
731
989
  } catch (e) {
732
- log2.debug("WebSearch failed", e);
990
+ log3.debug("WebSearch failed", e);
733
991
  return `Error searching: ${e?.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e?.message ?? e}`;
734
992
  } finally {
735
993
  clearTimeout(timer);
@@ -740,12 +998,12 @@ function makeWebSearchTool(options = {}) {
740
998
  function makeWebSearchAnthropicTool(opts = {}) {
741
999
  return makeWebSearchTool({ provider: "anthropic", name: "WebSearchAnthropic", description: ANTHROPIC_SEARCH_DESC, anthropicApiKey: opts.anthropicApiKey, model: opts.model });
742
1000
  }
743
- var log2, _dnsLookup, webFetchTool, webSearchTool, ANTHROPIC_SEARCH_DESC, webSearchAnthropicTool;
1001
+ var log3, _dnsLookup, webFetchTool, webSearchTool, ANTHROPIC_SEARCH_DESC, webSearchAnthropicTool;
744
1002
  var init_tools_web = __esm({
745
1003
  "src/tools.web.ts"() {
746
1004
  "use strict";
747
1005
  init_logging();
748
- log2 = forComponent("web");
1006
+ log3 = forComponent("web");
749
1007
  webFetchTool = makeWebFetchTool();
750
1008
  webSearchTool = makeWebSearchTool();
751
1009
  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).";
@@ -960,6 +1218,36 @@ ${out}`.trim() : out || "(command succeeded, no output)";
960
1218
  );
961
1219
  return `Started background job ${id} \u2014 poll with JobOutput({id:"${id}"}) / JobStatus, stop with JobKill.`;
962
1220
  }
1221
+ function realPathOf(fs, path) {
1222
+ if (typeof fs.realPath !== "function") return null;
1223
+ try {
1224
+ return fs.realPath(path);
1225
+ } catch (e) {
1226
+ log4.debug(`realPath unavailable for ${path} \u2014 inlining image bytes instead`, e);
1227
+ return null;
1228
+ }
1229
+ }
1230
+ function isBinary(raw) {
1231
+ if (raw === "") return false;
1232
+ const head = raw.length > 8192 ? raw.slice(0, 8192) : raw;
1233
+ let repl = 0;
1234
+ for (let i = 0; i < head.length; i++) {
1235
+ const c = head.charCodeAt(i);
1236
+ if (c === 0) return true;
1237
+ if (c === 65533) repl++;
1238
+ }
1239
+ return repl / head.length > 0.1;
1240
+ }
1241
+ async function byteSize(fs, path, raw) {
1242
+ const bfs = fs;
1243
+ if (typeof bfs.readFileBytes === "function") {
1244
+ try {
1245
+ return (await bfs.readFileBytes(path)).length;
1246
+ } catch {
1247
+ }
1248
+ }
1249
+ return Buffer.byteLength(raw, "utf8");
1250
+ }
963
1251
  function exitSessionTool(onExit) {
964
1252
  return {
965
1253
  name: "ExitSession",
@@ -986,15 +1274,18 @@ function toolsByName(names) {
986
1274
  return t;
987
1275
  });
988
1276
  }
989
- var numberLines, bashTool, IMG_MIME, readTool, editTool;
1277
+ var log4, numberLines, bashTool, IMG_MIME, readTool, editTool;
990
1278
  var init_tools = __esm({
991
1279
  "src/tools.ts"() {
992
1280
  "use strict";
1281
+ init_llm();
1282
+ init_logging();
993
1283
  init_tools_structured();
994
1284
  init_todo();
995
1285
  init_tools_web();
996
1286
  init_OverlayFilesystem();
997
1287
  init_redact();
1288
+ log4 = forComponent("tools");
998
1289
  numberLines = (content, offset = 0, limit) => {
999
1290
  const lines = content.split("\n");
1000
1291
  const start = Math.max(0, offset);
@@ -1049,12 +1340,22 @@ var init_tools = __esm({
1049
1340
  if (typeof fs.readFileBytes !== "function") {
1050
1341
  return `[${path} is an image, but this filesystem can't read binary \u2014 attach it as @${path} instead, or run on disk.]`;
1051
1342
  }
1343
+ const real = realPathOf(fs, path);
1344
+ if (real) {
1345
+ if (!await ctx.fs.exists(path)) return `Error: File not found: ${path}`;
1346
+ return imageRefResult(real, IMG_MIME[ext]);
1347
+ }
1052
1348
  const bytes = await fs.readFileBytes(path);
1053
1349
  const b64 = Buffer.from(bytes).toString("base64");
1054
1350
  return JSON.stringify({ dataUrl: `data:${IMG_MIME[ext]};base64,${b64}`, image: path });
1055
1351
  }
1056
1352
  const raw = await ctx.fs.readFile(path);
1353
+ if (isBinary(raw)) {
1354
+ const bytes = await byteSize(ctx.fs, path, raw);
1355
+ return `[binary file: ${path} \u2014 ${bytes} bytes, not shown as text. Read renders png/jpg/jpeg/gif/webp images directly; otherwise attach it as @${path}.]`;
1356
+ }
1057
1357
  ctx.readState.set(ctx.fs.resolvePath(path), raw);
1358
+ if (raw === "") return `[empty file: ${path} \u2014 0 bytes]`;
1058
1359
  const content = CONFIG_FILE_RE.test(ctx.fs.resolvePath(path)) ? redactSecrets(raw) : raw;
1059
1360
  const total = content === "" ? 0 : content.split("\n").length;
1060
1361
  const start = Math.max(0, offset ?? 0);
@@ -1188,6 +1489,11 @@ var init_NodeDiskFilesystem = __esm({
1188
1489
  throw new Error(`File not found: ${path}`);
1189
1490
  }
1190
1491
  }
1492
+ /** The REAL on-disk path behind a VFS path — an optional capability (not on IFilesystem) the Read
1493
+ * tool duck-types so it can persist an image REFERENCE instead of megabytes of base64. */
1494
+ realPath(path) {
1495
+ return this.real(this.resolvePath(path));
1496
+ }
1191
1497
  /** Read raw bytes (for binary files like images). Not on the base IFilesystem (which is utf8-only) — an
1192
1498
  * optional capability the Read tool duck-types to return image blocks. Same symlink/jail guards as readFile. */
1193
1499
  async readFileBytes(path) {
@@ -1370,6 +1676,12 @@ var init_JailedFilesystem = __esm({
1370
1676
  if (typeof inner.readFileBytes !== "function") throw new Error("binary read not supported by this filesystem");
1371
1677
  return inner.readFileBytes(this.guardRead("readFileBytes", path));
1372
1678
  }
1679
+ /** Forward the optional real-path capability (image REFS), jail-guarded like readFile. */
1680
+ realPath(path) {
1681
+ const inner = this.inner;
1682
+ if (typeof inner.realPath !== "function") throw new Error("realPath not supported by this filesystem");
1683
+ return inner.realPath(this.guardRead("realPath", path));
1684
+ }
1373
1685
  async writeFile(path, content) {
1374
1686
  return this.inner.writeFile(this.guardWrite("writeFile", path), content);
1375
1687
  }
@@ -1610,13 +1922,13 @@ function makeRealShellTool(options) {
1610
1922
  }
1611
1923
  };
1612
1924
  try {
1613
- return await new Promise((resolve) => {
1925
+ return await new Promise((resolve2) => {
1614
1926
  let out = "";
1615
1927
  let settled = false;
1616
1928
  const finish = (s) => {
1617
1929
  if (settled) return;
1618
1930
  settled = true;
1619
- resolve(s);
1931
+ resolve2(s);
1620
1932
  };
1621
1933
  let proc;
1622
1934
  try {
@@ -1639,7 +1951,7 @@ function makeRealShellTool(options) {
1639
1951
  proc.stderr?.on("data", collect);
1640
1952
  proc.on("error", (err) => {
1641
1953
  if (err?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
1642
- log5.debug("shell spawn error", err);
1954
+ log7.debug("shell spawn error", err);
1643
1955
  finish(`[exit 1] ${err?.message ?? err}${out ? "\n" + clean(out) : ""}`);
1644
1956
  });
1645
1957
  proc.on("close", (code) => {
@@ -1701,7 +2013,7 @@ ${clean(out) || "(no output yet)"}`;
1701
2013
  }
1702
2014
  ];
1703
2015
  }
1704
- var log5, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
2016
+ var log7, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
1705
2017
  var init_tools_shell = __esm({
1706
2018
  "src/tools.shell.ts"() {
1707
2019
  "use strict";
@@ -1709,7 +2021,7 @@ var init_tools_shell = __esm({
1709
2021
  init_redact();
1710
2022
  init_logging();
1711
2023
  init_shell_sandbox();
1712
- log5 = forComponent("shell");
2024
+ log7 = forComponent("shell");
1713
2025
  clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
1714
2026
  DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
1715
2027
  SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
@@ -1789,18 +2101,8 @@ var init_tools_shell = __esm({
1789
2101
  }
1790
2102
  });
1791
2103
 
1792
- // src/llm.ts
1793
- function contentText(content) {
1794
- if (content == null) return "";
1795
- if (typeof content === "string") return content;
1796
- return content.map((p) => p.type === "text" ? p.text ?? "" : "[image]").join(p_sep(content));
1797
- }
1798
- var p_sep = (parts) => parts.length > 1 ? "\n" : "";
1799
- function imagePart(url) {
1800
- return { type: "image_url", image_url: { url } };
1801
- }
1802
-
1803
2104
  // src/Agent.ts
2105
+ init_llm();
1804
2106
  init_tools();
1805
2107
 
1806
2108
  // src/tools.jobs.ts
@@ -2492,7 +2794,7 @@ init_OverlayFilesystem();
2492
2794
 
2493
2795
  // src/worktree.ts
2494
2796
  import { spawnSync } from "child_process";
2495
- import { symlinkSync, mkdirSync, statSync, readFileSync, rmSync } from "fs";
2797
+ import { symlinkSync, mkdirSync, statSync as statSync2, readFileSync as readFileSync2, rmSync } from "fs";
2496
2798
  import { join } from "path";
2497
2799
  var VALID_SEGMENT = /^[a-zA-Z0-9._-]+$/;
2498
2800
  var MAX_SLUG_LENGTH = 64;
@@ -2531,16 +2833,16 @@ function findGitRoot(from) {
2531
2833
  }
2532
2834
  function readWorktreeHead(worktreePath) {
2533
2835
  try {
2534
- const gitFile = readFileSync(join(worktreePath, ".git"), "utf-8").trim();
2836
+ const gitFile = readFileSync2(join(worktreePath, ".git"), "utf-8").trim();
2535
2837
  const m = gitFile.match(/^gitdir:\s*(.+)$/);
2536
2838
  if (!m) return null;
2537
2839
  const headPath = join(m[1], "HEAD");
2538
- const head = readFileSync(headPath, "utf-8").trim();
2840
+ const head = readFileSync2(headPath, "utf-8").trim();
2539
2841
  const refMatch = head.match(/^ref:\s*(.+)$/);
2540
2842
  if (!refMatch) return head.length === 40 ? head : null;
2541
2843
  const refPath = join(m[1], "..", refMatch[1]);
2542
2844
  try {
2543
- return readFileSync(refPath, "utf-8").trim();
2845
+ return readFileSync2(refPath, "utf-8").trim();
2544
2846
  } catch {
2545
2847
  }
2546
2848
  const r = git(["rev-parse", "HEAD"], worktreePath);
@@ -2576,7 +2878,7 @@ function symlinkDirs(repoRoot, worktreePath, dirs) {
2576
2878
  const src = join(repoRoot, dir);
2577
2879
  const dst = join(worktreePath, dir);
2578
2880
  try {
2579
- statSync(src);
2881
+ statSync2(src);
2580
2882
  } catch {
2581
2883
  continue;
2582
2884
  }
@@ -2592,7 +2894,7 @@ function cleanupWorktree(repoRoot, slug2, opts) {
2592
2894
  const worktreePath = worktreePathFor(repoRoot, slug2);
2593
2895
  const branch = worktreeBranchName(slug2);
2594
2896
  try {
2595
- statSync(worktreePath);
2897
+ statSync2(worktreePath);
2596
2898
  } catch {
2597
2899
  return { removed: false, reason: "not found" };
2598
2900
  }
@@ -2642,7 +2944,7 @@ function exitWorktree() {
2642
2944
  // src/subagent.ts
2643
2945
  init_NodeDiskFilesystem();
2644
2946
  init_logging();
2645
- var log3 = forComponent("subagent");
2947
+ var log5 = forComponent("subagent");
2646
2948
  async function boundedPool(items, limit, fn) {
2647
2949
  const out = new Array(items.length);
2648
2950
  let next = 0;
@@ -2684,7 +2986,7 @@ function traceFileFor(dir, label, depth) {
2684
2986
  const id = `${Date.now().toString(36)}-${(traceSeq++).toString(36)}`;
2685
2987
  return join3(dir, `${id}-d${depth}-${sanitizeSlug(label) || "sub"}.json`);
2686
2988
  } catch (e) {
2687
- log3.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
2989
+ log5.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
2688
2990
  return void 0;
2689
2991
  }
2690
2992
  }
@@ -2692,7 +2994,7 @@ function flushTrace(file, meta, messages, status) {
2692
2994
  try {
2693
2995
  writeFileSync(file, JSON.stringify({ ...meta, status, updated: Date.now(), messages }, null, 2));
2694
2996
  } catch (e) {
2695
- log3.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
2997
+ log5.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
2696
2998
  }
2697
2999
  }
2698
3000
  function installTrace(child, file, meta) {
@@ -3153,7 +3455,11 @@ function reasoningToChatFragment(model, effort) {
3153
3455
  }
3154
3456
 
3155
3457
  // src/Agent.ts
3156
- var log4 = forComponent("Agent");
3458
+ var log6 = forComponent("Agent");
3459
+ function envInt(key, fallback) {
3460
+ const n = typeof process !== "undefined" ? Number(process.env?.[key]) : NaN;
3461
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
3462
+ }
3157
3463
  function isBrowserTool(name) {
3158
3464
  return name.startsWith("mcp__") && /browser/i.test(name.slice(5).split("__")[0]);
3159
3465
  }
@@ -3179,8 +3485,9 @@ var AgentOptions = class {
3179
3485
  /** Hard ceiling on accumulated tokens (prompt+completion) across the run; cache READS count at 0.1×
3180
3486
  * (their real price) so a fat cached context doesn't trip the guard on healthy turns. 0 = unbounded. */
3181
3487
  maxTokens = 2e5;
3182
- /** Wall-clock ceiling in ms for the whole run. 0 = unbounded. */
3183
- timeoutMs = 12e4;
3488
+ /** Idle ceiling in ms: max time since the last productive step before the run is killed (a long/active
3489
+ * step resets it — see runLoop's `lastProgressAt`). Not a whole-run wall clock. 0 = unbounded. */
3490
+ timeoutMs = 3e5;
3184
3491
  /** Idle-stall watchdog for a SINGLE streamed model request: abort it if no chunk (text OR reasoning)
3185
3492
  * arrives for this long. The between-steps `timeoutMs` can't preempt an in-flight `chat()`, so a
3186
3493
  * provider that goes silent mid-stream would otherwise park the loop forever. 0 = off. */
@@ -3191,15 +3498,33 @@ var AgentOptions = class {
3191
3498
  maxToolCalls = 100;
3192
3499
  /** External cancellation — abort the loop between steps (e.g. a UI "Cancel" button). */
3193
3500
  signal;
3501
+ /** Retry budget for an EMPTY/silent-turn (a delegated-runtime wedge: cursor's warm helper spins then
3502
+ * returns a content-less end, surfaced as `code:'empty'`/`code:'cursor_silent'`). Tuned WIDER than the
3503
+ * network-drop budget (fixed 2) because clearing a wedge needs several reap+respawn cycles — each retry
3504
+ * hits a FRESH cursor session (the adapter evicts the wedged warm helper on the silent-end). Still bounded:
3505
+ * after exhaustion the turn surfaces `finishReason:'empty'` (non-zero exit), never infinite, never silent.
3506
+ * Env: AGENTX_EMPTY_RETRIES. */
3507
+ emptyRetries = envInt("AGENTX_EMPTY_RETRIES", 4);
3508
+ /** Base backoff (ms) between empty-turn retries; grows linearly per attempt (longer than the network
3509
+ * class's fixed 1s so a wedged helper has time to be reaped/respawned). Env: AGENTX_EMPTY_RETRY_BASE_MS. */
3510
+ emptyRetryBaseMs = envInt("AGENTX_EMPTY_RETRY_BASE_MS", 1500);
3194
3511
  /** 0 = never trim. Otherwise cap the messages sent per turn (system + most recent). */
3195
3512
  maxContextMessages = 0;
3196
3513
  /** Note-taking: keep the most-recent N tool-result outputs verbatim; collapse OLDER ones to a one-line
3197
3514
  * stub in the sent context (the model already consumed them — it can re-Read/re-run). The stored
3198
3515
  * transcript is never mutated. 0 = keep all verbatim. */
3199
3516
  keepToolOutputs = 0;
3517
+ /** How many of the most-recent image-bearing messages get their image REF expanded to real base64 in
3518
+ * the outgoing request. Older ones (and legacy inline base64) go on the wire as the elision stub. */
3519
+ keepRecentImages = 1;
3200
3520
  /** Token-aware backstop (~4 chars/token estimate). After note-taking, drop oldest messages from the
3201
3521
  * sent context until the estimate is under this ceiling (pairing-safe). 0 = off. */
3202
3522
  maxContextTokens = 0;
3523
+ /** The MODEL's context window (tokens) — what the context % is measured against and what `/compact`
3524
+ * targets 50% of. Filled by the CLI from model metadata; 0 = fall back to 200k. Deliberately NOT
3525
+ * `maxTokens`: that is the cumulative SPEND kill-switch, and a spend budget must never be able to
3526
+ * drive destructive compaction (`--max-tokens 50000` would otherwise shred the transcript). */
3527
+ contextWindow = 0;
3203
3528
  /** Pagination ceiling for a SINGLE tool result (bytes). A result over this is cropped to page 1 with
3204
3529
  * a marker telling the model it was cropped (refine the query, or page further). Guards against one
3205
3530
  * Grep/Read/MCP call blowing the whole context window. 0 = off. Default 60k (~15k tokens). */
@@ -3443,7 +3768,7 @@ var Agent = class _Agent {
3443
3768
  const disk = new NodeDiskFilesystem2(process.cwd());
3444
3769
  await disk.init();
3445
3770
  this.options.fs = new JailedFilesystem2(disk);
3446
- log4.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
3771
+ log6.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
3447
3772
  }
3448
3773
  this.buildCtx();
3449
3774
  }
@@ -3561,19 +3886,42 @@ var Agent = class _Agent {
3561
3886
  return this.runLoop();
3562
3887
  }
3563
3888
  /**
3564
- * Fold the conversation in place (manual `/compact`): keep the system message + the
3565
- * most-recent window, summarizing the dropped middle (deterministic, no LLM call).
3566
- * No-op when the transcript already fits. Returns the number of messages removed.
3567
- * `focus` (e.g. from `/compact keep the API details`) preserves matching lines from the
3568
- * dropped span verbatim in the summary, instead of losing them to the generic recap.
3889
+ * Fold the conversation in place (manual `/compact`) until it fits a TOKEN budget — not a message
3890
+ * count. Message-count folding alone frees almost nothing when a few recent tool results are huge,
3891
+ * so we escalate: fold the middle into a synthetic recap → elide oversized tool-result bodies in the
3892
+ * retained window → halve the window → drop-oldest backstop. Deterministic, no LLM call.
3893
+ * `focus` (e.g. `/compact keep the API details`) preserves matching lines from the dropped span verbatim.
3894
+ * Returns before/after token estimates so the caller can report honestly (0 freed => say so).
3569
3895
  */
3570
3896
  compactNow(maxMessages = 12, focus) {
3571
- const max = Math.max(2, maxMessages);
3572
- if (this.transcript.length <= max) return 0;
3897
+ const cap = _Agent.contextCap(this.options);
3898
+ const target = Math.round(cap * _Agent.COMPACT_TARGET);
3899
+ const kr = this.options.keepRecentImages ?? 1;
3900
+ const tokensBefore = estimateTokens(this.transcript, kr);
3901
+ const msgsBefore = this.transcript.length;
3902
+ let max = Math.max(2, maxMessages);
3903
+ if (this.transcript.length <= max && tokensBefore <= target)
3904
+ return { tokensBefore, tokensAfter: tokensBefore, target, cap, removed: 0 };
3573
3905
  void this.activeHooks?.onPreCompact?.(this.transcript);
3574
- const before = this.transcript.length;
3575
- this.transcript = compact(this.transcript, max, focus);
3576
- return before - this.transcript.length;
3906
+ let out = this.transcript;
3907
+ if (out.length > max) out = compact(out, max, focus);
3908
+ if (estimateTokens(out, kr) > target) out = elideOversizedImages(out, target, true, kr);
3909
+ if (estimateTokens(out, kr) > target) out = capOversizedBodies(out, target, kr);
3910
+ if (estimateTokens(out, kr) > target) out = elideOversizedImages(out, target, false, kr);
3911
+ while (estimateTokens(out, kr) > target && max > 2 && out.length > 2) {
3912
+ max = Math.max(2, Math.floor(max / 2));
3913
+ out = capOversizedBodies(compact(out, max, focus), target, kr);
3914
+ }
3915
+ if (estimateTokens(out, kr) > target) out = fitTokenBudget(out, target, kr);
3916
+ out = dropOrphanToolResults(out);
3917
+ this.transcript = out;
3918
+ this.releaseStaleImages();
3919
+ return { tokensBefore, tokensAfter: estimateTokens(this.transcript, kr), target, cap, removed: msgsBefore - this.transcript.length };
3920
+ }
3921
+ /** Drop inline base64 images from older messages so Bun/JSC can reclaim the heap (see contentBytes). */
3922
+ releaseStaleImages(keepRecent = 1) {
3923
+ const next = elideStaleImages(this.transcript, keepRecent);
3924
+ if (next !== this.transcript) this.transcript = next;
3577
3925
  }
3578
3926
  async runLoop() {
3579
3927
  const o = this.options;
@@ -3589,15 +3937,16 @@ var Agent = class _Agent {
3589
3937
  let lastFp = "";
3590
3938
  let repeats = 0;
3591
3939
  let closureNudged = false;
3940
+ let lastProgressAt = start;
3592
3941
  const kill = (finishReason) => {
3593
- log4.warn(`kill-switch: ${finishReason} (steps=${steps}, tokens=${usage.totalTokens}, budgetTokens=${Math.round(usage.totalTokens - 0.9 * usage.cacheReadTokens)}, ms=${Date.now() - start - this.parkedMs}${this.parkedMs ? ` +${this.parkedMs} parked` : ""})`);
3942
+ log6.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` : ""})`);
3594
3943
  this.ctx.jobs?.killAll();
3595
3944
  return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated };
3596
3945
  };
3597
3946
  while (true) {
3598
3947
  if (o.signal?.aborted) return kill("aborted");
3599
3948
  if (steps >= o.maxSteps) return kill("max_steps");
3600
- if (o.timeoutMs && Date.now() - start - this.parkedMs >= o.timeoutMs) return kill("timeout");
3949
+ if (o.timeoutMs && Date.now() - lastProgressAt - this.parkedMs >= o.timeoutMs) return kill("timeout");
3601
3950
  const budgetTokens = usage.totalTokens - 0.9 * usage.cacheReadTokens;
3602
3951
  if (o.maxTokens && budgetTokens >= o.maxTokens) return kill("budget");
3603
3952
  steps++;
@@ -3637,27 +3986,38 @@ var Agent = class _Agent {
3637
3986
  const r = await o.ai.chat({ model: o.model, messages: sent, tools: sentTools, stream: false, signal: o.signal, ...o.toolChoice ? { toolChoice: o.toolChoice } : {}, ...reasonOpts });
3638
3987
  res = r;
3639
3988
  }
3989
+ if (steps === 1 && this.isEmptyStop(res) && !this.pendingInjections.length) {
3990
+ const e = new Error("model returned an empty turn (finishReason=stop, no text, no tools)");
3991
+ e.code = "empty";
3992
+ throw e;
3993
+ }
3640
3994
  break;
3641
3995
  } catch (err) {
3642
3996
  const sc = err?.statusCode;
3643
3997
  const serverSide = sc >= 500 || sc === 408 || /Service Unavailable|overloaded|Internal server error|Bad gateway|Gateway time/i.test(String(err?.message ?? ""));
3644
3998
  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));
3645
3999
  const stalled = err?.code === "stall";
3646
- const transient = !o.signal?.aborted && !isAbortError(err) && attempt < 2 && (network || serverSide || stalled);
4000
+ const empty = err?.code === "empty" || err?.code === "cursor_silent";
4001
+ const maxAttempts = empty ? Math.max(o.emptyRetries, 0) : 2;
4002
+ const transient = !o.signal?.aborted && !isAbortError(err) && attempt < maxAttempts && (network || serverSide || stalled || empty);
3647
4003
  if (!transient) throw err;
3648
- const waitMs = 1e3 * (attempt + 1);
3649
- log4.warn(`${stalled ? "model stall" : "network drop"} mid-step (${err?.message ?? err}) \u2014 retrying in ${waitMs}ms`);
3650
- o.host?.notify?.({ kind: "retry", message: `${stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
4004
+ const waitMs = (empty ? o.emptyRetryBaseMs : 1e3) * (attempt + 1);
4005
+ log6.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err?.message ?? err}) \u2014 retrying in ${waitMs}ms`);
4006
+ o.host?.notify?.({ kind: "retry", message: `${empty ? "model returned nothing" : stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
3651
4007
  await new Promise((r) => setTimeout(r, waitMs));
3652
4008
  }
3653
4009
  }
3654
4010
  } catch (err) {
3655
4011
  if (err?.code === "budget") return kill("budget");
3656
4012
  if (err?.code === "stall") {
3657
- log4.warn(`model stalled out after retries: ${err?.message ?? err}`);
4013
+ log6.warn(`model stalled out after retries: ${err?.message ?? err}`);
3658
4014
  return kill("timeout");
3659
4015
  }
3660
4016
  if (o.signal?.aborted || isAbortError(err)) return kill("aborted");
4017
+ if (err?.code === "empty" || err?.code === "cursor_silent") {
4018
+ log6.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
4019
+ return { text: "", steps, finishReason: "empty", messages: this.transcript, usage, usageEstimated, error: err };
4020
+ }
3661
4021
  const body = err?.body ?? err?.response?.data ?? err?.error;
3662
4022
  let bodyStr;
3663
4023
  try {
@@ -3667,7 +4027,7 @@ var Agent = class _Agent {
3667
4027
  }
3668
4028
  if (bodyStr && err instanceof Error && !err.message.includes(bodyStr)) err.detail = bodyStr;
3669
4029
  const errLog = err instanceof Error ? { message: err.message, ...err.detail ? { detail: err.detail } : {}, stack: err.stack } : err;
3670
- log4.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
4030
+ log6.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
3671
4031
  return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err };
3672
4032
  }
3673
4033
  if (o.signal?.aborted) return kill("aborted");
@@ -3711,6 +4071,7 @@ var Agent = class _Agent {
3711
4071
  }
3712
4072
  const toolCalls = res.toolCalls ?? [];
3713
4073
  const emptyTurn = toolCalls.length === 0 && delegatedTools.length === 0 && contentText(res.content ?? "").trim() === "";
4074
+ if (!emptyTurn) lastProgressAt = Date.now();
3714
4075
  if (!emptyTurn) {
3715
4076
  this.transcript.push({
3716
4077
  role: "assistant",
@@ -3722,12 +4083,13 @@ var Agent = class _Agent {
3722
4083
  if (this.drainInjections()) continue;
3723
4084
  if (o.closeDelegatedTurns && res.endedOnToolNoText && !closureNudged && steps < o.maxSteps) {
3724
4085
  closureNudged = true;
3725
- log4.verbose("delegated turn went silent after a tool \u2014 nudging a closing summary");
4086
+ log6.verbose("delegated turn went silent after a tool \u2014 nudging a closing summary");
3726
4087
  this.transcript.push({ role: "user", content: "Summarize what you did this turn and what (if anything) still needs doing. Be concise." });
3727
4088
  continue;
3728
4089
  }
3729
- log4.verbose(`completed in ${steps} step(s)`);
4090
+ log6.verbose(`completed in ${steps} step(s)`);
3730
4091
  await this.ctx.jobs?.drain();
4092
+ this.releaseStaleImages();
3731
4093
  await this.activeHooks?.onStop?.(res.content ?? "");
3732
4094
  return { text: res.content ?? "", steps, finishReason: "stop", messages: this.transcript, usage, usageEstimated };
3733
4095
  }
@@ -3753,7 +4115,7 @@ var Agent = class _Agent {
3753
4115
  if (this.ctx.needsInput) {
3754
4116
  const question = this.ctx.needsInput;
3755
4117
  this.ctx.needsInput = void 0;
3756
- log4.warn(`needs_input: ${question.question} (steps=${steps})`);
4118
+ log6.warn(`needs_input: ${question.question} (steps=${steps})`);
3757
4119
  await this.ctx.jobs?.drain();
3758
4120
  const text = [
3759
4121
  `OPEN QUESTION (run paused \u2014 no human was reachable): ${question.question}`,
@@ -3810,6 +4172,12 @@ var Agent = class _Agent {
3810
4172
  }
3811
4173
  };
3812
4174
  }
4175
+ /** A clean-stop turn that carries no assistant text AND no tool calls (native or delegated) — a
4176
+ * non-result the model produced by going silent. Callers retry it rather than report "done". */
4177
+ isEmptyStop(res) {
4178
+ const stop = res.finishReason === "stop" || res.finishReason == null;
4179
+ return stop && (res.toolCalls?.length ?? 0) === 0 && (res.delegatedTools?.length ?? 0) === 0 && contentText(res.content ?? "").trim() === "";
4180
+ }
3813
4181
  async consumeStream(stream, poke) {
3814
4182
  let content = "";
3815
4183
  let finishReason;
@@ -3868,13 +4236,13 @@ var Agent = class _Agent {
3868
4236
  const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
3869
4237
  if (decision?.block) {
3870
4238
  const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
3871
- log4.debug(`${tc.function.name} -> ${blocked}`);
4239
+ log6.debug(`${tc.function.name} -> ${blocked}`);
3872
4240
  await hooks?.postToolUse?.(call, blocked, meta);
3873
4241
  return blocked;
3874
4242
  }
3875
4243
  this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
3876
4244
  if (earlyError) {
3877
- log4.debug(`${tc.function.name} -> ${earlyError}`);
4245
+ log6.debug(`${tc.function.name} -> ${earlyError}`);
3878
4246
  await hooks?.postToolUse?.(call, earlyError, meta);
3879
4247
  this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
3880
4248
  return earlyError;
@@ -3883,12 +4251,12 @@ var Agent = class _Agent {
3883
4251
  let images;
3884
4252
  let threw = false;
3885
4253
  try {
3886
- log4.debug(`${tc.function.name}(${tc.function.arguments})`);
4254
+ log6.debug(`${tc.function.name}(${tc.function.arguments})`);
3887
4255
  this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
3888
4256
  try {
3889
4257
  hooks.onToolOutput(call, chunk, meta);
3890
4258
  } catch (e) {
3891
- log4.debug(`onToolOutput hook error: ${e}`);
4259
+ log6.debug(`onToolOutput hook error: ${e}`);
3892
4260
  }
3893
4261
  } : void 0;
3894
4262
  const raw = await tool.run(args, this.ctx);
@@ -3900,7 +4268,7 @@ var Agent = class _Agent {
3900
4268
  }
3901
4269
  } catch (e) {
3902
4270
  const msg = e instanceof Error ? e.message : String(e);
3903
- log4.debug(`${tc.function.name} -> error: ${msg}`);
4271
+ log6.debug(`${tc.function.name} -> error: ${msg}`);
3904
4272
  result = `Error: ${msg}`;
3905
4273
  threw = true;
3906
4274
  } finally {
@@ -3926,6 +4294,16 @@ var Agent = class _Agent {
3926
4294
  }
3927
4295
  return images?.length ? { text: result, images } : result;
3928
4296
  }
4297
+ /** `/compact` aims to land the transcript at this fraction of the context window. */
4298
+ static COMPACT_TARGET = 0.5;
4299
+ /** THE context-window cap resolution — `/compact`'s budget and the CLI's context footer/warnings both
4300
+ * route here, so the percentage the user reads and the one compaction targets can never drift. */
4301
+ static contextCap(o) {
4302
+ if (o.maxContextTokens) return o.maxContextTokens;
4303
+ const w = o.contextWindow || 0;
4304
+ if (w > ANTHROPIC_WINDOW_WITHOUT_1M_BETA && isAnthropicModel(o.model)) return ANTHROPIC_WINDOW_WITHOUT_1M_BETA;
4305
+ return w || 2e5;
4306
+ }
3929
4307
  static WRITE_CLASS = ["Write", "Edit", "MultiEdit", "ApplyEdits"];
3930
4308
  /** Append an autoTest failure section to a write-class tool result, if configured. */
3931
4309
  async maybeAutoTest(toolName, result) {
@@ -3959,26 +4337,27 @@ ${out}`;
3959
4337
  const browserKeep = this.browserActive ? o.browserTrim?.keepOutputs : void 0;
3960
4338
  const keep = browserKeep != null ? o.keepToolOutputs ? Math.min(o.keepToolOutputs, browserKeep) : browserKeep : o.keepToolOutputs;
3961
4339
  if (keep) out = stubOldToolResults(out ?? m, keep);
4340
+ out = expandImagesForSend(dropOrphanToolResults(out ?? m), o.keepRecentImages ?? 1);
3962
4341
  if (o.maxContextTokens) {
3963
- const pre = (out ?? m).length;
3964
- out = fitTokenBudget(out ?? m, o.maxContextTokens);
4342
+ const pre = out.length;
4343
+ out = fitTokenBudget(out, o.maxContextTokens, o.keepRecentImages ?? 1);
3965
4344
  const dropped = pre - out.length;
3966
4345
  if (dropped > 0 && dropped !== this.lastTrimNotified) {
3967
4346
  this.lastTrimNotified = dropped;
3968
4347
  o.host?.notify?.({ kind: "compaction", message: `context full \u2014 auto-trimmed ${dropped} oldest message(s) to fit ~${Math.round(o.maxContextTokens / 1e3)}k tokens` });
3969
4348
  }
3970
4349
  }
3971
- return dropOrphanToolResults(out ?? m);
4350
+ return dropOrphanToolResults(out);
3972
4351
  }
3973
4352
  };
3974
4353
  function dropOldest(m, max) {
3975
4354
  const head = m[0]?.role === "system" ? [m[0]] : [];
3976
4355
  return [...head, ...m.slice(-(max - head.length))];
3977
4356
  }
3978
- function estimateTokens(m) {
3979
- let chars = 0;
3980
- for (const x of m) chars += contentText(x.content).length + (x.tool_calls ? JSON.stringify(x.tool_calls).length : 0);
3981
- return Math.ceil(chars / 4);
4357
+ var ANTHROPIC_WINDOW_WITHOUT_1M_BETA = 2e5;
4358
+ var isAnthropicModel = (m) => !!m && (m.startsWith("anthropic/") || /(^|\/)claude-/.test(m));
4359
+ function estimateTokens(m, keepRecentImages = 1) {
4360
+ return Math.ceil(sendBytes(m, keepRecentImages) / 4);
3982
4361
  }
3983
4362
  function cropResult(result, cap) {
3984
4363
  const head = result.slice(0, cap);
@@ -4012,6 +4391,60 @@ function stubOldToolResults(messages, keep) {
4012
4391
  return { ...x, content: `[${x.name ?? "tool"}${where ? ` ${where}` : ""} output elided \u2014 ${lines} lines; re-run the tool to view]` };
4013
4392
  });
4014
4393
  }
4394
+ function elideOversizedImages(messages, target, protectNewest = false, keepRecentImages = 1) {
4395
+ let out = messages;
4396
+ const withImages = out.map((_, i) => i).filter((i) => messageHasImage(out[i].content));
4397
+ const newest = protectNewest ? withImages[withImages.length - 1] : -1;
4398
+ const weight = sendBytesPer(out, keepRecentImages);
4399
+ const heaviestFirst = withImages.filter((i) => i !== newest).sort((a, b) => weight[b] - weight[a]);
4400
+ for (const i of heaviestFirst) {
4401
+ if (estimateTokens(out, keepRecentImages) <= target) break;
4402
+ const c = elideImages(out[i].content);
4403
+ if (c === out[i].content) continue;
4404
+ out = out.map((m, j) => j === i ? { ...m, content: c } : m);
4405
+ }
4406
+ return out;
4407
+ }
4408
+ function capOversizedBodies(messages, target, keepRecentImages = 1) {
4409
+ let out = messages;
4410
+ for (const cap of [8e3, 4e3, 2e3, 800, 300, 120]) {
4411
+ if (estimateTokens(out, keepRecentImages) <= target) break;
4412
+ out = out.map((x) => x.role === "system" ? x : elideBody(x, cap));
4413
+ }
4414
+ return out;
4415
+ }
4416
+ function elideBody(x, cap) {
4417
+ const note = (bytes, imgs2) => `
4418
+
4419
+ [${bytes} bytes elided by /compact${x.role === "tool" ? " \u2014 re-run the tool to view" : ""}${imgs2 ? `; ${imgs2} image part(s) KEPT in full \u2014 they still cost context` : ""}]`;
4420
+ if (!Array.isArray(x.content)) {
4421
+ const text = contentText(x.content);
4422
+ if (text.length <= cap) return x;
4423
+ return { ...x, content: `${text.slice(0, cap)}${note(text.length - cap, 0)}` };
4424
+ }
4425
+ const imgs = x.content.filter((p) => p.type !== "text").length;
4426
+ let left = cap;
4427
+ let elided = 0;
4428
+ const parts = [];
4429
+ for (const p of x.content) {
4430
+ if (p.type !== "text") {
4431
+ parts.push(p);
4432
+ continue;
4433
+ }
4434
+ const t = p.text ?? "";
4435
+ if (t.length <= left) {
4436
+ left -= t.length;
4437
+ if (t) parts.push(p);
4438
+ continue;
4439
+ }
4440
+ const keep = left;
4441
+ left = 0;
4442
+ elided += t.length - keep;
4443
+ if (keep > 0) parts.push({ ...p, text: t.slice(0, keep) });
4444
+ }
4445
+ if (!elided) return parts.length === x.content.length || !parts.length ? x : { ...x, content: parts };
4446
+ return { ...x, content: [...parts, { type: "text", text: note(elided, imgs).trimStart() }] };
4447
+ }
4015
4448
  var callIdSet = (msgs) => {
4016
4449
  const ids = /* @__PURE__ */ new Set();
4017
4450
  for (const m of msgs) if (m.role === "assistant") for (const tc of m.tool_calls ?? []) ids.add(tc.id);
@@ -4022,8 +4455,8 @@ function dropOrphanToolResults(messages) {
4022
4455
  const ok = (m) => m.role !== "tool" || ids.has(m.tool_call_id ?? "");
4023
4456
  return messages.every(ok) ? messages : messages.filter(ok);
4024
4457
  }
4025
- function fitTokenBudget(messages, maxTokens) {
4026
- const per = messages.map((x) => estimateTokens([x]));
4458
+ function fitTokenBudget(messages, maxTokens, keepRecentImages = 1) {
4459
+ const per = sendBytesPer(messages, keepRecentImages).map((b) => Math.ceil(b / 4));
4027
4460
  let total = per.reduce((a, b) => a + b, 0);
4028
4461
  if (total <= maxTokens) return messages;
4029
4462
  const head = messages[0]?.role === "system" ? [messages[0]] : [];
@@ -4032,7 +4465,7 @@ function fitTokenBudget(messages, maxTokens) {
4032
4465
  const ids = callIdSet(messages.slice(from));
4033
4466
  while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
4034
4467
  if (total > maxTokens)
4035
- log4.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
4468
+ log6.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
4036
4469
  return [...head, ...messages.slice(from)];
4037
4470
  }
4038
4471
  function compact(m, max, focus) {
@@ -4044,11 +4477,24 @@ function compact(m, max, focus) {
4044
4477
  if (dropped.length === 0) return [...head, ...tail];
4045
4478
  return [...head, { role: "system", content: summarize(dropped, focus) }, ...tail];
4046
4479
  }
4480
+ var SUMMARY_HEAD = "[summary of ";
4481
+ var MAX_CARRIED_COUNT = 1e4;
4482
+ var isSyntheticSummary = (m) => m.role === "system" && typeof m.content === "string" && m.content.startsWith(SUMMARY_HEAD);
4047
4483
  function summarize(messages, focus) {
4048
4484
  const tools = /* @__PURE__ */ new Set();
4049
4485
  const files = /* @__PURE__ */ new Set();
4486
+ const carried = [];
4487
+ let carriedCount = 0;
4488
+ let priors = 0;
4050
4489
  let lastAssistant = "";
4051
4490
  for (const msg of messages) {
4491
+ if (isSyntheticSummary(msg)) {
4492
+ priors++;
4493
+ const [head, ...rest] = String(msg.content).split("\n");
4494
+ carriedCount += Math.min(Math.max(Number(/of (\d+)/.exec(head)?.[1]) || 0, 0), MAX_CARRIED_COUNT);
4495
+ for (const l of rest) if (l.trim() && !carried.includes(l) && carried.length < 20) carried.push(l);
4496
+ continue;
4497
+ }
4052
4498
  for (const tc of msg.tool_calls ?? []) {
4053
4499
  tools.add(tc.function.name);
4054
4500
  try {
@@ -4059,14 +4505,16 @@ function summarize(messages, focus) {
4059
4505
  }
4060
4506
  if (msg.role === "assistant" && msg.content) lastAssistant = contentText(msg.content);
4061
4507
  }
4062
- const lines = [`[summary of ${messages.length} earlier message(s)]`];
4508
+ const lines = [`${SUMMARY_HEAD}${messages.length - priors + carriedCount} earlier message(s)]`];
4063
4509
  if (tools.size) lines.push(`tools used: ${[...tools].join(", ")}`);
4064
4510
  if (files.size) lines.push(`files touched: ${[...files].join(", ")}`);
4065
4511
  if (lastAssistant) lines.push(`last assistant: ${lastAssistant}`);
4512
+ if (carried.length) lines.push("earlier recap:", ...carried.map((l) => ` ${l}`));
4066
4513
  if (focus?.trim()) {
4067
4514
  const words = focus.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
4068
4515
  const kept = [];
4069
- outer: for (const msg of messages) {
4516
+ const ordered = [...messages.filter((m) => !isSyntheticSummary(m)), ...messages.filter(isSyntheticSummary)];
4517
+ outer: for (const msg of ordered) {
4070
4518
  if (msg.role === "tool") continue;
4071
4519
  for (const line of contentText(msg.content).split("\n")) {
4072
4520
  const l = line.toLowerCase();
@@ -4732,7 +5180,7 @@ init_tools_web();
4732
5180
  init_tools();
4733
5181
  init_tools_structured();
4734
5182
  init_logging();
4735
- var log6 = forComponent("scratch");
5183
+ var log8 = forComponent("scratch");
4736
5184
  var SCRATCH_DIR = "/scratch";
4737
5185
  function shortArgs(args) {
4738
5186
  try {
@@ -4772,7 +5220,7 @@ var Scratch = class {
4772
5220
  await (this.dirReady ??= mkdirp(this.fs, dir));
4773
5221
  await this.fs.writeFile(path, header + raw);
4774
5222
  } catch (e) {
4775
- log6.debug("scratch write failed; returning raw", e);
5223
+ log8.debug("scratch write failed; returning raw", e);
4776
5224
  return raw;
4777
5225
  }
4778
5226
  const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
@@ -4802,7 +5250,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
4802
5250
  await (this.dirReady ??= mkdirp(this.fs, dir));
4803
5251
  await this.fs.writeFile(path, header + full);
4804
5252
  } catch (e) {
4805
- log6.debug("scratch spill failed; cropping lossy", e);
5253
+ log8.debug("scratch spill failed; cropping lossy", e);
4806
5254
  return full.slice(0, pageBytes) + `
4807
5255
 
4808
5256
  [output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
@@ -4848,7 +5296,7 @@ Question: ${q}`);
4848
5296
  const answer = (res.text ?? "").trim();
4849
5297
  return answer || "(no answer found in scratch)";
4850
5298
  } catch (e) {
4851
- log6.debug("Ask peek failed", e);
5299
+ log8.debug("Ask peek failed", e);
4852
5300
  return `Error querying scratch: ${e?.message ?? e}`;
4853
5301
  }
4854
5302
  }
@@ -4857,7 +5305,7 @@ Question: ${q}`);
4857
5305
 
4858
5306
  // src/lessons.ts
4859
5307
  init_logging();
4860
- var log7 = forComponent("Lessons");
5308
+ var log9 = forComponent("Lessons");
4861
5309
  var LessonOptionsDefaults = class {
4862
5310
  minRepeats = 2;
4863
5311
  };
@@ -4882,15 +5330,16 @@ function lessonCapture(options) {
4882
5330
  counts.set(lesson.slug, n);
4883
5331
  if (n < o.minRepeats) return;
4884
5332
  written.add(lesson.slug);
4885
- await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log7.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
4886
- log7.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
5333
+ await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log9.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
5334
+ log9.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
4887
5335
  }
4888
5336
  };
4889
5337
  }
4890
5338
 
4891
5339
  // src/reflect.ts
5340
+ init_llm();
4892
5341
  init_logging();
4893
- var log8 = forComponent("Reflect");
5342
+ var log10 = forComponent("Reflect");
4894
5343
  async function reflectOnRun(o) {
4895
5344
  const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
4896
5345
  if (!digest.trim()) return null;
@@ -4906,7 +5355,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
4906
5355
  const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
4907
5356
  text = r?.content ?? "";
4908
5357
  } catch (e) {
4909
- log8.warn(`reflection call failed: ${e?.message ?? e}`);
5358
+ log10.warn(`reflection call failed: ${e?.message ?? e}`);
4910
5359
  return null;
4911
5360
  }
4912
5361
  const m = text.match(/LESSON:\s*(.+)/i);
@@ -4916,10 +5365,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
4916
5365
  try {
4917
5366
  await writeFact(o.fs, o.dir, slug2, lesson);
4918
5367
  } catch (e) {
4919
- log8.warn(`could not persist lesson: ${e?.message ?? e}`);
5368
+ log10.warn(`could not persist lesson: ${e?.message ?? e}`);
4920
5369
  return null;
4921
5370
  }
4922
- log8.debug(`reflection persisted ${slug2}`);
5371
+ log10.debug(`reflection persisted ${slug2}`);
4923
5372
  return slug2;
4924
5373
  }
4925
5374
  function digestRun(messages, maxChars) {
@@ -4935,6 +5384,7 @@ function digestRun(messages, maxChars) {
4935
5384
 
4936
5385
  // src/duplex.ts
4937
5386
  import { MemFilesystem as MemFilesystem2 } from "@livx.cc/wcli/core";
5387
+ init_llm();
4938
5388
  init_logging();
4939
5389
 
4940
5390
  // node_modules/@bod.ee/voice/src/core/logging.ts
@@ -4967,7 +5417,7 @@ function forComponent2(name) {
4967
5417
  }
4968
5418
 
4969
5419
  // node_modules/@bod.ee/voice/src/core/emotion.ts
4970
- var log9 = forComponent2("Emotion");
5420
+ var log11 = forComponent2("Emotion");
4971
5421
  var EMOTIONS = [
4972
5422
  // primary (best results)
4973
5423
  "neutral",
@@ -5121,7 +5571,7 @@ var EmotionStream = class {
5121
5571
  if (tag) {
5122
5572
  this.pending = tag;
5123
5573
  if (this.show) display += m[0];
5124
- } else log9.debug(`dropping unknown emotion tag ${m[0]}`);
5574
+ } else log11.debug(`dropping unknown emotion tag ${m[0]}`);
5125
5575
  last = m.index + m[0].length;
5126
5576
  }
5127
5577
  this.emit(body.slice(last), (s, d, p) => {
@@ -5144,7 +5594,7 @@ var EmotionStream = class {
5144
5594
  };
5145
5595
 
5146
5596
  // node_modules/@bod.ee/voice/src/core/engine.ts
5147
- var log10 = forComponent2("VoiceEngine");
5597
+ var log12 = forComponent2("VoiceEngine");
5148
5598
  var realClock = {
5149
5599
  now: () => performance.now(),
5150
5600
  setTimeout: (fn, ms) => setTimeout(fn, ms),
@@ -5419,7 +5869,7 @@ var VoiceEngine = class _VoiceEngine {
5419
5869
  await Promise.all([this.tts.connect(), this.stt.start()]);
5420
5870
  this.tts.warmup?.();
5421
5871
  this.setState("listening");
5422
- log10.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
5872
+ log12.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
5423
5873
  }
5424
5874
  get usingAec() {
5425
5875
  return this.stt.usingAec;
@@ -5441,7 +5891,7 @@ var VoiceEngine = class _VoiceEngine {
5441
5891
  this.options.onDiag({ t: this.clock.now(), kind, ...fields });
5442
5892
  } catch (e) {
5443
5893
  this.diagOn = false;
5444
- log10.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
5894
+ log12.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
5445
5895
  }
5446
5896
  }
5447
5897
  idleWaiters = [];
@@ -5550,7 +6000,7 @@ var VoiceEngine = class _VoiceEngine {
5550
6000
  if (prose.trim()) this.repliedSinceDispatch = true;
5551
6001
  for (const w of this.words(this.reply)) this.echoWords.add(w);
5552
6002
  this.tts.speak(forSpeech(speech), true);
5553
- if (!this.spokeDeltas && this.turnStartAt) log10.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
6003
+ if (!this.spokeDeltas && this.turnStartAt) log12.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
5554
6004
  this.spokeDeltas = true;
5555
6005
  this.setState("speaking");
5556
6006
  if (this.options.revealMode !== "off" && display) {
@@ -5627,7 +6077,7 @@ var VoiceEngine = class _VoiceEngine {
5627
6077
  this.drainTimer = null;
5628
6078
  this.speaking = false;
5629
6079
  this.stopWordReveal(true);
5630
- if (this.turnStartAt) log10.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
6080
+ if (this.turnStartAt) log12.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
5631
6081
  this.echoUntil = this.clock.now() + 2500;
5632
6082
  if (!this.usingAec) this.stt.reset();
5633
6083
  this.setState("listening");
@@ -5715,7 +6165,7 @@ var VoiceEngine = class _VoiceEngine {
5715
6165
  this.clearAckTimer("interrupt");
5716
6166
  this.adaptiveAckFired = false;
5717
6167
  this.ackProbe = null;
5718
- if (this.uttQueue.length) log10.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
6168
+ if (this.uttQueue.length) log12.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
5719
6169
  const droppedQueued = this.uttQueue.length;
5720
6170
  this.uttQueue = [];
5721
6171
  if (!this.speaking && !this.drainTimer) return;
@@ -5861,7 +6311,7 @@ var VoiceEngine = class _VoiceEngine {
5861
6311
  if (flat && this.lastDispatchFlat && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS && (this.lastDispatchFlat.startsWith(flat) || flat.startsWith(this.lastDispatchFlat))) return;
5862
6312
  this.specSpent = true;
5863
6313
  this.specText = this.specPartial;
5864
- log10.debug(`speculate: "${this.specText.slice(0, 60)}"`);
6314
+ log12.debug(`speculate: "${this.specText.slice(0, 60)}"`);
5865
6315
  this.diag("speculate", { text: this.specText });
5866
6316
  this.options.onSpeculate(this.specText);
5867
6317
  }, this.options.speculativeMs);
@@ -5930,7 +6380,7 @@ var VoiceEngine = class _VoiceEngine {
5930
6380
  };
5931
6381
  this.tts.newContext();
5932
6382
  this.tts.speak(phrase, false);
5933
- log10.debug(`backchannel: "${phrase}"`);
6383
+ log12.debug(`backchannel: "${phrase}"`);
5934
6384
  this.diag("backchannel", { phrase });
5935
6385
  this.options.onBackchannel(phrase);
5936
6386
  }
@@ -5963,7 +6413,7 @@ var VoiceEngine = class _VoiceEngine {
5963
6413
  });
5964
6414
  if (idx < 0) return text;
5965
6415
  toks.splice(idx, 1);
5966
- log10.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
6416
+ log12.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
5967
6417
  return toks.join(" ");
5968
6418
  }
5969
6419
  /** Merge a resumed utterance into the pending one, deduping any word-overlap. Soniox re-finalizes
@@ -6054,7 +6504,7 @@ var VoiceEngine = class _VoiceEngine {
6054
6504
  this.pendingUtt = this.mergeUtterance(this.pendingUtt, text);
6055
6505
  if (this.pendingTimer) this.clock.clearTimeout(this.pendingTimer);
6056
6506
  if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
6057
- log10.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
6507
+ log12.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
6058
6508
  this.diag("hold", { reason: "incomplete", tail: this.pendingUtt.slice(-40) });
6059
6509
  this.options.onHold();
6060
6510
  if (this.options.holdFiller && !this.speaking) this.speakFiller(this.options.holdFiller);
@@ -6089,13 +6539,13 @@ var VoiceEngine = class _VoiceEngine {
6089
6539
  if (text) {
6090
6540
  const flat = text.toLowerCase().replace(/[^a-z0-9]/g, "");
6091
6541
  if (flat && flat === this.lastDispatchFlat && !this.repliedSinceDispatch && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS) {
6092
- log10.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
6542
+ log12.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
6093
6543
  this.diag("utterance_drop", { reason: "dup_final", text: text.slice(0, 60) });
6094
6544
  return;
6095
6545
  }
6096
6546
  const refinal = this.refinalizes(text);
6097
6547
  if (refinal && this.repliedSinceDispatch) {
6098
- log10.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
6548
+ log12.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
6099
6549
  this.diag("utterance_drop", { reason: "refinalized", text: text.slice(0, 60) });
6100
6550
  this.lastDispatchFlat = flat;
6101
6551
  this.lastDispatchWords = text.trim().split(/\s+/);
@@ -6255,7 +6705,7 @@ var SpokenSplitter = class {
6255
6705
  };
6256
6706
 
6257
6707
  // node_modules/@bod.ee/voice/src/adapters/soniox.ts
6258
- var log11 = forComponent2("SonioxSTT");
6708
+ var log13 = forComponent2("SonioxSTT");
6259
6709
  var now = () => performance.now();
6260
6710
  var SonioxSTTOptions = class {
6261
6711
  auth = "";
@@ -6296,7 +6746,7 @@ var SonioxSTT = class {
6296
6746
  this.onDiag({ t: now(), kind, ...fields });
6297
6747
  } catch (e) {
6298
6748
  this.diagOn = false;
6299
- log11.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6749
+ log13.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6300
6750
  }
6301
6751
  }
6302
6752
  lastChunkAt = 0;
@@ -6338,11 +6788,11 @@ var SonioxSTT = class {
6338
6788
  this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
6339
6789
  this.ws.onclose = (ev) => {
6340
6790
  if (this.stopped) return;
6341
- log11.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
6791
+ log13.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
6342
6792
  this.diag("stt_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: true });
6343
6793
  this.reset();
6344
6794
  this.connectWs().catch((e) => {
6345
- log11.error(`soniox reconnect failed: ${e.message}`);
6795
+ log13.error(`soniox reconnect failed: ${e.message}`);
6346
6796
  this.diag("stt_reconnect_failed", { message: e.message });
6347
6797
  });
6348
6798
  };
@@ -6354,7 +6804,7 @@ var SonioxSTT = class {
6354
6804
  this.endpointTimer = setInterval(() => {
6355
6805
  const combined = (this.finalText + this.partialText).trim();
6356
6806
  if (!combined || now() - this.lastChangeAt < this.options.silenceEndpointMs) return;
6357
- if (this.firstTokenAt) log11.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
6807
+ if (this.firstTokenAt) log13.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
6358
6808
  this.reset();
6359
6809
  this.onUtterance(combined, now());
6360
6810
  }, 120);
@@ -6366,7 +6816,7 @@ var SonioxSTT = class {
6366
6816
  if (this.stopped) return;
6367
6817
  const ref = this.lastChunkAt || this.startedChunksAt;
6368
6818
  if (now() - ref > noAudioMs) {
6369
- log11.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
6819
+ log13.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
6370
6820
  this.diag("stt_watchdog_fatal", { noAudioMs });
6371
6821
  this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
6372
6822
  this.stop();
@@ -6389,7 +6839,7 @@ var SonioxSTT = class {
6389
6839
  handle(m) {
6390
6840
  if (m.error_message) {
6391
6841
  this.diag("stt_error", { message: String(m.error_message), code: m.error_code });
6392
- return log11.error(`soniox: ${m.error_message}`);
6842
+ return log13.error(`soniox: ${m.error_message}`);
6393
6843
  }
6394
6844
  let endpoint = false;
6395
6845
  for (const t of m.tokens ?? []) {
@@ -6406,7 +6856,7 @@ var SonioxSTT = class {
6406
6856
  this.onPartial(combined);
6407
6857
  if (endpoint && this.finalText.trim()) {
6408
6858
  const utterance = this.finalText.trim();
6409
- if (this.firstTokenAt) log11.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
6859
+ if (this.firstTokenAt) log13.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
6410
6860
  this.reset();
6411
6861
  this.onUtterance(utterance, now());
6412
6862
  }
@@ -6428,7 +6878,7 @@ var SonioxSTT = class {
6428
6878
  };
6429
6879
 
6430
6880
  // node_modules/@bod.ee/voice/src/adapters/cartesia.ts
6431
- var log12 = forComponent2("CartesiaTTS");
6881
+ var log14 = forComponent2("CartesiaTTS");
6432
6882
  var now2 = () => performance.now();
6433
6883
  var CartesiaTTSOptions = class {
6434
6884
  auth = "";
@@ -6464,7 +6914,7 @@ var CartesiaTTS = class _CartesiaTTS {
6464
6914
  this.onDiag({ t: now2(), kind, ...fields });
6465
6915
  } catch (e) {
6466
6916
  this.diagOn = false;
6467
- log12.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6917
+ log14.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6468
6918
  }
6469
6919
  }
6470
6920
  firstAudioAt = 0;
@@ -6499,11 +6949,11 @@ var CartesiaTTS = class _CartesiaTTS {
6499
6949
  this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
6500
6950
  });
6501
6951
  this.ws.onclose = (ev) => {
6502
- log12.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
6952
+ log14.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
6503
6953
  this.diag("tts_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: !this.closed });
6504
6954
  if (!this.closed) {
6505
6955
  this.connecting = this.doConnect().catch((e) => {
6506
- log12.error(`cartesia reconnect failed: ${e.message}`);
6956
+ log14.error(`cartesia reconnect failed: ${e.message}`);
6507
6957
  this.diag("tts_reconnect_failed", { message: e.message });
6508
6958
  });
6509
6959
  }
@@ -6531,12 +6981,12 @@ var CartesiaTTS = class _CartesiaTTS {
6531
6981
  this.down = true;
6532
6982
  this.downAt = now2();
6533
6983
  this.consecutiveOk = 0;
6534
- log12.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
6984
+ log14.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
6535
6985
  this.diag("tts_breaker_open", { errors: this.consecutiveErrors });
6536
6986
  this.onDone();
6537
6987
  this.startProbe();
6538
6988
  } else if (!this.down) {
6539
- (/No valid transcripts/i.test(m.message || "") ? log12.debug : log12.warn)(`cartesia: ${JSON.stringify(m)}`);
6989
+ (/No valid transcripts/i.test(m.message || "") ? log14.debug : log14.warn)(`cartesia: ${JSON.stringify(m)}`);
6540
6990
  }
6541
6991
  }
6542
6992
  };
@@ -6551,7 +7001,7 @@ var CartesiaTTS = class _CartesiaTTS {
6551
7001
  this.stopProbe();
6552
7002
  const downMs = this.downAt ? now2() - this.downAt : 0;
6553
7003
  this.diag("tts_breaker_close", { downMs: Math.round(downMs) });
6554
- (downMs < 2e3 ? log12.debug : log12.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
7004
+ (downMs < 2e3 ? log14.debug : log14.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
6555
7005
  }
6556
7006
  /** Ensure the WS is open before sending — reconnects if idle-closed. */
6557
7007
  async ensureConnected() {
@@ -6642,7 +7092,7 @@ configureLogging((name) => {
6642
7092
  });
6643
7093
 
6644
7094
  // src/duplex.ts
6645
- var log13 = forComponent("DuplexAgent");
7095
+ var log15 = forComponent("DuplexAgent");
6646
7096
  function describeCall(call) {
6647
7097
  const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
6648
7098
  const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
@@ -6853,7 +7303,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
6853
7303
  const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
6854
7304
  if (m) {
6855
7305
  this.fabricationCut = true;
6856
- log13.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
7306
+ log15.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
6857
7307
  const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
6858
7308
  if (!safe) return;
6859
7309
  if (safe.trim()) this.spokeThisTurn = true;
@@ -6946,7 +7396,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
6946
7396
  return {
6947
7397
  preToolUse: (call) => {
6948
7398
  if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
6949
- log13.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
7399
+ log15.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
6950
7400
  this.abortSpeculation();
6951
7401
  return { block: true, reason: "Speculative turn aborted." };
6952
7402
  }
@@ -6984,7 +7434,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
6984
7434
  try {
6985
7435
  await this.voice.send(fallback ? "[reminder] You said nothing to the user this turn. Tell them, in ONE short spoken sentence, what just happened \u2014 no tools." : dispatched ? "[reminder] You dispatched a task but said nothing to the user. Say ONE short spoken acknowledgement now \u2014 no tools." : "[reminder] You said nothing to the user this turn. Give your ONE short spoken reply now \u2014 no tools.");
6986
7436
  } catch (e) {
6987
- log13.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
7437
+ log15.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
6988
7438
  } finally {
6989
7439
  this.nudging = false;
6990
7440
  }
@@ -7077,7 +7527,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7077
7527
  try {
7078
7528
  res = await this.voice.send(spec.text);
7079
7529
  } catch (e) {
7080
- log13.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
7530
+ log15.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
7081
7531
  } finally {
7082
7532
  this.voice.options.signal = prevSignal;
7083
7533
  }
@@ -7091,7 +7541,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7091
7541
  if (d === "abort") {
7092
7542
  if (this.voice.transcript.length > base) this.voice.transcript.length = base;
7093
7543
  this.speculativeAbortedCalls++;
7094
- log13.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
7544
+ log15.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
7095
7545
  if (this.spec === spec) this.spec = void 0;
7096
7546
  return res ?? empty;
7097
7547
  }
@@ -7356,7 +7806,7 @@ Another agent just implemented the above. Independently check the CURRENT state
7356
7806
  this.notify("task_verify", `task ${id}: verifying`, { id });
7357
7807
  const cres = await new Agent(checkerOpts).run(checkBrief);
7358
7808
  if (cres.finishReason !== "stop") {
7359
- log13.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
7809
+ log15.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
7360
7810
  this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
7361
7811
  }
7362
7812
  const sum = (a = 0, b = 0) => a + b;
@@ -7422,14 +7872,14 @@ Another agent just implemented the above. Independently check the CURRENT state
7422
7872
  * '[task <id> asks] …' and resolves with the user's answer via AnswerTask — or '' on timeout/
7423
7873
  * task settle (callers map '' to deny / best-judgment). Workers never block forever. */
7424
7874
  parkQuestion(askId, question) {
7425
- return new Promise((resolve) => {
7875
+ return new Promise((resolve2) => {
7426
7876
  let settled = false;
7427
7877
  const finish = (answer) => {
7428
7878
  if (settled) return;
7429
7879
  settled = true;
7430
7880
  clearTimeout(timer);
7431
7881
  this.pendingAsks.delete(askId);
7432
- resolve(answer);
7882
+ resolve2(answer);
7433
7883
  };
7434
7884
  const timer = setTimeout(() => {
7435
7885
  this.notify("task_ask_timeout", `task ${askId}: question timed out \u2014 proceeding without an answer`);
@@ -7491,8 +7941,8 @@ Another agent just implemented the above. Independently check the CURRENT state
7491
7941
  }
7492
7942
  rec.status = "done";
7493
7943
  rec.result = res.text;
7494
- const incomplete = res.finishReason !== "stop";
7495
- log13.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
7944
+ const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
7945
+ log15.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
7496
7946
  this.notify("task_done", `task ${id} (${rec.label}) completed`, {
7497
7947
  id,
7498
7948
  text: res.text,
@@ -7534,14 +7984,14 @@ Another agent just implemented the above. Independently check the CURRENT state
7534
7984
  foldIfSuperseded(rec, note) {
7535
7985
  if (!(rec.deliveryParked && this.suppressParkedResume)) return false;
7536
7986
  if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
7537
- log13.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
7987
+ log15.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
7538
7988
  return true;
7539
7989
  }
7540
7990
  failTask(rec, msg) {
7541
7991
  this.dropAsk(rec.id);
7542
7992
  rec.status = "error";
7543
7993
  rec.result = msg;
7544
- log13.warn(`task ${rec.id} failed: ${msg}`);
7994
+ log15.warn(`task ${rec.id} failed: ${msg}`);
7545
7995
  this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
7546
7996
  if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
7547
7997
  this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
@@ -7846,11 +8296,11 @@ function buildMcpCatalog(servers) {
7846
8296
  }
7847
8297
  return { specs, routes };
7848
8298
  }
7849
- function searchOverCatalog(servers, specs, routes, resolve, options) {
8299
+ function searchOverCatalog(servers, specs, routes, resolve2, options) {
7850
8300
  const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
7851
8301
  const r = routes.get(name);
7852
8302
  if (!r) throw new Error(`unknown MCP tool '${name}' \u2014 use ToolSearch to find valid names`);
7853
- return resolve(r.server, r.rawName, args ?? {});
8303
+ return resolve2(r.server, r.rawName, args ?? {});
7854
8304
  }, options) : [];
7855
8305
  return { tools, serverNames: servers, toolCount: specs.length };
7856
8306
  }
@@ -7859,9 +8309,9 @@ function makeMcpToolSearchFromMounted(mounted, options) {
7859
8309
  const byName = new Map(mounted.map((m) => [m.name, m]));
7860
8310
  return searchOverCatalog(mounted.map((m) => m.name), specs, routes, (server, rawName, args) => byName.get(server).client.callTool(rawName, args), options);
7861
8311
  }
7862
- function makeLazyMcpToolSearch(servers, resolve, options) {
8312
+ function makeLazyMcpToolSearch(servers, resolve2, options) {
7863
8313
  const { specs, routes } = buildMcpCatalog(servers);
7864
- return searchOverCatalog(servers.map((s) => s.name), specs, routes, resolve, options);
8314
+ return searchOverCatalog(servers.map((s) => s.name), specs, routes, resolve2, options);
7865
8315
  }
7866
8316
 
7867
8317
  // src/hooks.ts
@@ -7920,6 +8370,7 @@ var RecordingLifecycle = class {
7920
8370
 
7921
8371
  // src/index.ts
7922
8372
  init_logging();
8373
+ init_llm();
7923
8374
  import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor as CommandExecutor2, registerHeadlessCommands as registerHeadlessCommands2 } from "@livx.cc/wcli/core";
7924
8375
  export {
7925
8376
  Agent,
@@ -7934,6 +8385,8 @@ export {
7934
8385
  DuplexAgent,
7935
8386
  DuplexAgentOptions,
7936
8387
  FakeAIClient,
8388
+ IMAGE_ELIDE_STUB,
8389
+ IMAGE_REF_SCHEME,
7937
8390
  IndexedDbFilesystem,
7938
8391
  JailOptions,
7939
8392
  JailedFilesystem,
@@ -7968,19 +8421,26 @@ export {
7968
8421
  checkpointTool,
7969
8422
  checkpointTools,
7970
8423
  cleanupWorktree,
8424
+ clearImageRefs,
7971
8425
  compileSynthesizedTool,
7972
8426
  composeHooks,
8427
+ contentBytes,
7973
8428
  contentText,
7974
8429
  cronMatches,
7975
8430
  decodeDdgUrl,
7976
8431
  defaultTools,
7977
8432
  diskAgentOptions,
7978
8433
  editTool,
8434
+ elideImages,
8435
+ elideInlineImages,
8436
+ elideStaleImages,
7979
8437
  enterWorktree,
8438
+ estimateTokens,
7980
8439
  exitSessionTool,
7981
8440
  exitWorktree,
7982
8441
  expandCommand,
7983
8442
  expandEntry,
8443
+ expandImagesForSend,
7984
8444
  expandTemplate,
7985
8445
  findGitRoot,
7986
8446
  forComponent,
@@ -7992,6 +8452,8 @@ export {
7992
8452
  htmlToText,
7993
8453
  idfWeights,
7994
8454
  imagePart,
8455
+ imageRefPart,
8456
+ imageRefResult,
7995
8457
  lessonCapture,
7996
8458
  loadAgents,
7997
8459
  loadCommands,
@@ -8013,12 +8475,17 @@ export {
8013
8475
  makeWebSearchTool,
8014
8476
  mcpToolToAgentTool,
8015
8477
  mcpToolsToAgentTools,
8478
+ messageHasImage,
8479
+ messageHasImageRef,
8480
+ messageHasInlineImage,
8481
+ mintImageRef,
8016
8482
  mkdirp,
8017
8483
  modelShortLabel,
8018
8484
  multiEditTool,
8019
8485
  nextCronAfter,
8020
8486
  parseCron,
8021
8487
  parseDdgHtml,
8488
+ parseImageRef,
8022
8489
  planMode,
8023
8490
  raceAttempts,
8024
8491
  readTool,
@@ -8026,6 +8493,7 @@ export {
8026
8493
  reflectOnRun,
8027
8494
  registerHeadlessCommands2 as registerHeadlessCommands,
8028
8495
  relevanceScore,
8496
+ remintImageRefs,
8029
8497
  repoIndex,
8030
8498
  repoMapTool,
8031
8499
  resolveAuth,
@@ -8035,11 +8503,14 @@ export {
8035
8503
  sandboxAgentOptions,
8036
8504
  scanCommands,
8037
8505
  scanSkills,
8506
+ sendBytes,
8507
+ sendBytesPer,
8038
8508
  slugify,
8039
8509
  toWireTools,
8040
8510
  todoWriteTool,
8041
8511
  tokenize,
8042
8512
  toolCall,
8513
+ toolImageRef,
8043
8514
  toolRegistry,
8044
8515
  toolsByName,
8045
8516
  topByRelevance,