@livx.cc/agentx 0.99.27 → 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/cli.js CHANGED
@@ -9,6 +9,271 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/logging.ts
13
+ import { log } from "libx.js/src/modules/log";
14
+ var forComponent;
15
+ var init_logging = __esm({
16
+ "src/logging.ts"() {
17
+ "use strict";
18
+ forComponent = (name) => log.forComponent(name);
19
+ }
20
+ });
21
+
22
+ // src/llm.ts
23
+ import { readFileSync, statSync as statSync2, realpathSync } from "fs";
24
+ import { resolve, sep } from "path";
25
+ import { randomBytes } from "crypto";
26
+ function contentText(content) {
27
+ if (content == null) return "";
28
+ if (typeof content === "string") return content;
29
+ return content.map((p) => p.type === "text" ? p.text ?? "" : "[image]").join(p_sep(content));
30
+ }
31
+ function imagePart(url) {
32
+ return { type: "image_url", image_url: { url } };
33
+ }
34
+ function messageHasInlineImage(content) {
35
+ if (content == null || typeof content === "string") return false;
36
+ return content.some((p) => p.type === "image_url" && (p.image_url?.url ?? "").startsWith("data:"));
37
+ }
38
+ function contentBytes(content) {
39
+ if (content == null) return 0;
40
+ if (typeof content === "string") return content.length;
41
+ let n = 0;
42
+ for (const p of content) {
43
+ if (p.type === "text") {
44
+ n += (p.text ?? "").length;
45
+ continue;
46
+ }
47
+ const url = p.image_url?.url ?? "";
48
+ const ref = parseImageRef(url);
49
+ n += (ref ? ref.bytes : url.length) || 7;
50
+ }
51
+ return n;
52
+ }
53
+ function elideInlineImages(content, stub = IMAGE_ELIDE_STUB) {
54
+ return elideImageParts(content, (url) => url.startsWith("data:"), stub);
55
+ }
56
+ function elideImages(content, stub = IMAGE_ELIDE_STUB) {
57
+ return elideImageParts(content, (url) => url.startsWith("data:") || isImageRefUrl(url), stub);
58
+ }
59
+ function elideImageParts(content, hit, stub) {
60
+ if (typeof content === "string") return content;
61
+ let changed = false;
62
+ const out = [];
63
+ for (const p of content) {
64
+ if (p.type !== "image_url" || !hit(p.image_url?.url ?? "")) {
65
+ out.push(p);
66
+ continue;
67
+ }
68
+ changed = true;
69
+ out.push({ type: "text", text: stub });
70
+ }
71
+ return changed ? out : content;
72
+ }
73
+ function mintImageRef(path, mime) {
74
+ const token = randomBytes(16).toString("hex");
75
+ let bytes = 0;
76
+ try {
77
+ bytes = Math.ceil(statSync2(path).size * 4 / 3);
78
+ } catch (e) {
79
+ log2.warn(`image ref stat failed, weighing as 0: ${path}`, e);
80
+ }
81
+ refRegistry.set(token, { mime, path, bytes });
82
+ while (refRegistry.size > REF_REGISTRY_MAX) refRegistry.delete(refRegistry.keys().next().value);
83
+ return `${IMAGE_REF_SCHEME}${mime};${token};${path}`;
84
+ }
85
+ function imageRefPart(path, mime) {
86
+ return { type: "image_url", image_url: { url: mintImageRef(path, mime) } };
87
+ }
88
+ function parseImageRef(url) {
89
+ const parts = refUrlParts(url);
90
+ const ref = parts && refRegistry.get(parts.token);
91
+ if (!ref) {
92
+ log2.debug("image ref not in registry \u2014 treating as non-image text");
93
+ return null;
94
+ }
95
+ return ref;
96
+ }
97
+ function refUrlParts(url) {
98
+ if (!isImageRefUrl(url)) return null;
99
+ const rest = url.slice(IMAGE_REF_SCHEME.length);
100
+ const a = rest.indexOf(";");
101
+ if (a < 0) return null;
102
+ const b = rest.indexOf(";", a + 1);
103
+ 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) };
104
+ }
105
+ function remintImageRefs(messages, roots) {
106
+ const allowed = [];
107
+ for (const r of roots) {
108
+ try {
109
+ allowed.push(realpathSync(resolve(r)));
110
+ } catch (e) {
111
+ log2.debug(`image-ref root unavailable: ${r}`, e);
112
+ }
113
+ }
114
+ if (allowed.length === 0) return messages;
115
+ const inside = (p) => allowed.some((root) => p === root || p.startsWith(root + sep));
116
+ let changed = false;
117
+ const out = messages.map((m) => {
118
+ if (m.role !== "user" && m.role !== "assistant" || typeof m.content === "string" || m.content == null) return m;
119
+ let touched = false;
120
+ const parts = m.content.map((p) => {
121
+ if (p.type !== "image_url") return p;
122
+ const f = refUrlParts(p.image_url?.url);
123
+ if (!f || refRegistry.has(f.token)) return p;
124
+ let real;
125
+ try {
126
+ real = realpathSync(f.path);
127
+ } catch (e) {
128
+ log2.warn(`resumed image ref unreadable, will stub: ${f.path}`, e);
129
+ return p;
130
+ }
131
+ if (!inside(real)) {
132
+ log2.warn(`resumed image ref outside the allowed roots, will stub: ${f.path} -> ${real}`);
133
+ return p;
134
+ }
135
+ touched = true;
136
+ return { type: "image_url", image_url: { url: mintImageRef(real, f.mime) } };
137
+ });
138
+ if (!touched) return m;
139
+ changed = true;
140
+ return { ...m, content: parts };
141
+ });
142
+ return changed ? out : messages;
143
+ }
144
+ function messageHasImageRef(content) {
145
+ if (content == null || typeof content === "string") return false;
146
+ return content.some((p) => p.type === "image_url" && isImageRefUrl(p.image_url?.url));
147
+ }
148
+ function messageHasImage(content) {
149
+ return messageHasInlineImage(content) || messageHasImageRef(content);
150
+ }
151
+ function imageRefResult(path, mime) {
152
+ return JSON.stringify({ imageRef: mintImageRef(path, mime), image: path });
153
+ }
154
+ function toolImageRef(content) {
155
+ if (typeof content !== "string" || !content.startsWith('{"imageRef":')) return null;
156
+ try {
157
+ return parseImageRef(JSON.parse(content).imageRef);
158
+ } catch (e) {
159
+ log2.debug("bad imageRef tool result", e);
160
+ return null;
161
+ }
162
+ }
163
+ function elideStaleImages(messages, keepRecent = 1) {
164
+ const idx = messages.map((m, i) => messageHasImage(m.content) ? i : -1).filter((i) => i >= 0);
165
+ const drop = new Set(idx.slice(0, Math.max(0, idx.length - keepRecent)));
166
+ if (drop.size === 0) return messages;
167
+ let changed = false;
168
+ const out = messages.map((m, i) => {
169
+ if (!drop.has(i)) return m;
170
+ const c = elideInlineImages(m.content);
171
+ if (c === m.content) return m;
172
+ changed = true;
173
+ return { ...m, content: c };
174
+ });
175
+ return changed ? out : messages;
176
+ }
177
+ function imageWindows(messages, keepRecent) {
178
+ const attach = [];
179
+ const tool = [];
180
+ messages.forEach((m, i) => {
181
+ if (toolImageRef(m.content)) tool.push(i);
182
+ else if (messageHasImage(m.content)) attach.push(i);
183
+ });
184
+ const k = Math.max(0, keepRecent);
185
+ return /* @__PURE__ */ new Set([...attach.slice(Math.max(0, attach.length - k)), ...tool.slice(Math.max(0, tool.length - k))]);
186
+ }
187
+ function expandImagesForSend(messages, keepRecent = 1, stub = IMAGE_ELIDE_STUB) {
188
+ if (!messages.some(bearsImage)) return messages;
189
+ const keep = imageWindows(messages, keepRecent);
190
+ let changed = false;
191
+ const out = messages.map((m, i) => {
192
+ const content = keep.has(i) ? expandContent(m.content, stub) : stripContent(m.content, stub);
193
+ if (content === m.content) return m;
194
+ changed = true;
195
+ return { ...m, content };
196
+ });
197
+ return changed ? out : messages;
198
+ }
199
+ function sendBytes(messages, keepRecent = 1, stub = IMAGE_ELIDE_STUB) {
200
+ let n = 0;
201
+ for (const b of sendBytesPer(messages, keepRecent, stub)) n += b;
202
+ return n;
203
+ }
204
+ function sendBytesPer(messages, keepRecent = 1, stub = IMAGE_ELIDE_STUB) {
205
+ const keep = messages.some(bearsImage) ? imageWindows(messages, keepRecent) : null;
206
+ return messages.map((m, i) => wireBytes(m.content, keep == null || keep.has(i), stub) + (m.tool_calls ? JSON.stringify(m.tool_calls).length : 0));
207
+ }
208
+ function wireBytes(content, kept, stub) {
209
+ const tool = toolImageRef(content);
210
+ if (tool) return kept ? tool.bytes : stub.length;
211
+ if (content == null || typeof content === "string") return contentBytes(content);
212
+ if (kept) return contentBytes(content);
213
+ let n = 0;
214
+ for (const p of content) {
215
+ if (p.type === "text") {
216
+ n += (p.text ?? "").length;
217
+ continue;
218
+ }
219
+ const url = p.image_url?.url ?? "";
220
+ n += url.startsWith("data:") || isImageRefUrl(url) ? stub.length : url.length || 7;
221
+ }
222
+ return n;
223
+ }
224
+ function readRefDataUrl(ref) {
225
+ try {
226
+ return `data:${ref.mime};base64,${readFileSync(ref.path).toString("base64")}`;
227
+ } catch (e) {
228
+ log2.warn(`image ref unreadable, sending stub instead: ${ref.path}`, e);
229
+ return null;
230
+ }
231
+ }
232
+ function expandContent(content, stub) {
233
+ const tool = toolImageRef(content);
234
+ if (tool) {
235
+ const url = readRefDataUrl(tool);
236
+ return url ? JSON.stringify({ dataUrl: url, image: tool.path }) : stub;
237
+ }
238
+ if (typeof content === "string") return content;
239
+ let changed = false;
240
+ const out = content.map((p) => {
241
+ if (p.type !== "image_url" || !isImageRefUrl(p.image_url?.url)) return p;
242
+ changed = true;
243
+ const ref = parseImageRef(p.image_url?.url);
244
+ const url = ref && readRefDataUrl(ref);
245
+ return url ? imagePart(url) : { type: "text", text: stub };
246
+ });
247
+ return changed ? out : content;
248
+ }
249
+ function stripContent(content, stub) {
250
+ if (toolImageRef(content)) return stub;
251
+ if (typeof content === "string") return content;
252
+ let changed = false;
253
+ const out = content.map((p) => {
254
+ const url = p.type === "image_url" ? p.image_url?.url ?? "" : "";
255
+ if (!url.startsWith("data:") && !isImageRefUrl(url)) return p;
256
+ changed = true;
257
+ return { type: "text", text: stub };
258
+ });
259
+ return changed ? out : content;
260
+ }
261
+ var log2, p_sep, IMAGE_ELIDE_STUB, IMAGE_REF_SCHEME, REF_REGISTRY_MAX, refRegistry, isImageRefUrl, bearsImage;
262
+ var init_llm = __esm({
263
+ "src/llm.ts"() {
264
+ "use strict";
265
+ init_logging();
266
+ log2 = forComponent("llm");
267
+ p_sep = (parts) => parts.length > 1 ? "\n" : "";
268
+ IMAGE_ELIDE_STUB = "[image elided \u2014 re-attach @path or /paste to view again]";
269
+ IMAGE_REF_SCHEME = "agentx-image:";
270
+ REF_REGISTRY_MAX = 256;
271
+ refRegistry = /* @__PURE__ */ new Map();
272
+ isImageRefUrl = (url) => !!url && url.startsWith(IMAGE_REF_SCHEME);
273
+ bearsImage = (m) => messageHasImage(m.content) || toolImageRef(m.content) != null;
274
+ }
275
+ });
276
+
12
277
  // src/redact.ts
13
278
  function redactSecrets(text) {
14
279
  if (!text) return text;
@@ -475,16 +740,6 @@ var init_todo = __esm({
475
740
  }
476
741
  });
477
742
 
478
- // src/logging.ts
479
- import { log } from "libx.js/src/modules/log";
480
- var forComponent;
481
- var init_logging = __esm({
482
- "src/logging.ts"() {
483
- "use strict";
484
- forComponent = (name) => log.forComponent(name);
485
- }
486
- });
487
-
488
743
  // src/tools.web.ts
489
744
  function htmlToText(html) {
490
745
  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, " ");
@@ -606,7 +861,7 @@ function makeWebFetchTool(options = {}) {
606
861
 
607
862
  ${capped}`;
608
863
  } catch (e) {
609
- log2.debug(`WebFetch ${u} failed`, e);
864
+ log3.debug(`WebFetch ${u} failed`, e);
610
865
  return `Error fetching ${u}: ${e?.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e?.message ?? e}`;
611
866
  } finally {
612
867
  clearTimeout(timer);
@@ -730,7 +985,7 @@ function makeWebSearchTool(options = {}) {
730
985
  if (!res.ok) return `Error: search returned ${res.status} ${res.statusText}`;
731
986
  return formatHits(parseDdgHtml(await res.text(), maxResults));
732
987
  } catch (e) {
733
- log2.debug("WebSearch failed", e);
988
+ log3.debug("WebSearch failed", e);
734
989
  return `Error searching: ${e?.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e?.message ?? e}`;
735
990
  } finally {
736
991
  clearTimeout(timer);
@@ -741,12 +996,12 @@ function makeWebSearchTool(options = {}) {
741
996
  function makeWebSearchAnthropicTool(opts = {}) {
742
997
  return makeWebSearchTool({ provider: "anthropic", name: "WebSearchAnthropic", description: ANTHROPIC_SEARCH_DESC, anthropicApiKey: opts.anthropicApiKey, model: opts.model });
743
998
  }
744
- var log2, _dnsLookup, webFetchTool, webSearchTool, ANTHROPIC_SEARCH_DESC, webSearchAnthropicTool;
999
+ var log3, _dnsLookup, webFetchTool, webSearchTool, ANTHROPIC_SEARCH_DESC, webSearchAnthropicTool;
745
1000
  var init_tools_web = __esm({
746
1001
  "src/tools.web.ts"() {
747
1002
  "use strict";
748
1003
  init_logging();
749
- log2 = forComponent("web");
1004
+ log3 = forComponent("web");
750
1005
  webFetchTool = makeWebFetchTool();
751
1006
  webSearchTool = makeWebSearchTool();
752
1007
  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).";
@@ -961,6 +1216,36 @@ ${out}`.trim() : out || "(command succeeded, no output)";
961
1216
  );
962
1217
  return `Started background job ${id} \u2014 poll with JobOutput({id:"${id}"}) / JobStatus, stop with JobKill.`;
963
1218
  }
1219
+ function realPathOf(fs, path) {
1220
+ if (typeof fs.realPath !== "function") return null;
1221
+ try {
1222
+ return fs.realPath(path);
1223
+ } catch (e) {
1224
+ log4.debug(`realPath unavailable for ${path} \u2014 inlining image bytes instead`, e);
1225
+ return null;
1226
+ }
1227
+ }
1228
+ function isBinary(raw) {
1229
+ if (raw === "") return false;
1230
+ const head = raw.length > 8192 ? raw.slice(0, 8192) : raw;
1231
+ let repl2 = 0;
1232
+ for (let i = 0; i < head.length; i++) {
1233
+ const c = head.charCodeAt(i);
1234
+ if (c === 0) return true;
1235
+ if (c === 65533) repl2++;
1236
+ }
1237
+ return repl2 / head.length > 0.1;
1238
+ }
1239
+ async function byteSize(fs, path, raw) {
1240
+ const bfs = fs;
1241
+ if (typeof bfs.readFileBytes === "function") {
1242
+ try {
1243
+ return (await bfs.readFileBytes(path)).length;
1244
+ } catch {
1245
+ }
1246
+ }
1247
+ return Buffer.byteLength(raw, "utf8");
1248
+ }
964
1249
  function exitSessionTool(onExit) {
965
1250
  return {
966
1251
  name: "ExitSession",
@@ -987,15 +1272,18 @@ function toolsByName(names) {
987
1272
  return t;
988
1273
  });
989
1274
  }
990
- var numberLines, bashTool, IMG_MIME, readTool, editTool;
1275
+ var log4, numberLines, bashTool, IMG_MIME, readTool, editTool;
991
1276
  var init_tools = __esm({
992
1277
  "src/tools.ts"() {
993
1278
  "use strict";
1279
+ init_llm();
1280
+ init_logging();
994
1281
  init_tools_structured();
995
1282
  init_todo();
996
1283
  init_tools_web();
997
1284
  init_OverlayFilesystem();
998
1285
  init_redact();
1286
+ log4 = forComponent("tools");
999
1287
  numberLines = (content, offset = 0, limit) => {
1000
1288
  const lines = content.split("\n");
1001
1289
  const start = Math.max(0, offset);
@@ -1050,12 +1338,22 @@ var init_tools = __esm({
1050
1338
  if (typeof fs.readFileBytes !== "function") {
1051
1339
  return `[${path} is an image, but this filesystem can't read binary \u2014 attach it as @${path} instead, or run on disk.]`;
1052
1340
  }
1341
+ const real = realPathOf(fs, path);
1342
+ if (real) {
1343
+ if (!await ctx.fs.exists(path)) return `Error: File not found: ${path}`;
1344
+ return imageRefResult(real, IMG_MIME[ext]);
1345
+ }
1053
1346
  const bytes = await fs.readFileBytes(path);
1054
1347
  const b64 = Buffer.from(bytes).toString("base64");
1055
1348
  return JSON.stringify({ dataUrl: `data:${IMG_MIME[ext]};base64,${b64}`, image: path });
1056
1349
  }
1057
1350
  const raw = await ctx.fs.readFile(path);
1351
+ if (isBinary(raw)) {
1352
+ const bytes = await byteSize(ctx.fs, path, raw);
1353
+ 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}.]`;
1354
+ }
1058
1355
  ctx.readState.set(ctx.fs.resolvePath(path), raw);
1356
+ if (raw === "") return `[empty file: ${path} \u2014 0 bytes]`;
1059
1357
  const content = CONFIG_FILE_RE.test(ctx.fs.resolvePath(path)) ? redactSecrets(raw) : raw;
1060
1358
  const total = content === "" ? 0 : content.split("\n").length;
1061
1359
  const start = Math.max(0, offset ?? 0);
@@ -1123,7 +1421,7 @@ __export(worktree_exports, {
1123
1421
  worktreeBranchName: () => worktreeBranchName
1124
1422
  });
1125
1423
  import { spawnSync } from "child_process";
1126
- import { symlinkSync, mkdirSync, statSync as statSync2, readFileSync, rmSync } from "fs";
1424
+ import { symlinkSync, mkdirSync, statSync as statSync3, readFileSync as readFileSync2, rmSync } from "fs";
1127
1425
  import { join as join2 } from "path";
1128
1426
  function validateWorktreeSlug(slug2) {
1129
1427
  if (!slug2) throw new Error("worktree slug must not be empty");
@@ -1159,16 +1457,16 @@ function findGitRoot(from) {
1159
1457
  }
1160
1458
  function readWorktreeHead(worktreePath) {
1161
1459
  try {
1162
- const gitFile = readFileSync(join2(worktreePath, ".git"), "utf-8").trim();
1460
+ const gitFile = readFileSync2(join2(worktreePath, ".git"), "utf-8").trim();
1163
1461
  const m = gitFile.match(/^gitdir:\s*(.+)$/);
1164
1462
  if (!m) return null;
1165
1463
  const headPath = join2(m[1], "HEAD");
1166
- const head = readFileSync(headPath, "utf-8").trim();
1464
+ const head = readFileSync2(headPath, "utf-8").trim();
1167
1465
  const refMatch = head.match(/^ref:\s*(.+)$/);
1168
1466
  if (!refMatch) return head.length === 40 ? head : null;
1169
1467
  const refPath = join2(m[1], "..", refMatch[1]);
1170
1468
  try {
1171
- return readFileSync(refPath, "utf-8").trim();
1469
+ return readFileSync2(refPath, "utf-8").trim();
1172
1470
  } catch {
1173
1471
  }
1174
1472
  const r = git(["rev-parse", "HEAD"], worktreePath);
@@ -1204,7 +1502,7 @@ function symlinkDirs(repoRoot, worktreePath, dirs) {
1204
1502
  const src = join2(repoRoot, dir);
1205
1503
  const dst = join2(worktreePath, dir);
1206
1504
  try {
1207
- statSync2(src);
1505
+ statSync3(src);
1208
1506
  } catch {
1209
1507
  continue;
1210
1508
  }
@@ -1220,7 +1518,7 @@ function cleanupWorktree(repoRoot, slug2, opts) {
1220
1518
  const worktreePath = worktreePathFor(repoRoot, slug2);
1221
1519
  const branch = worktreeBranchName(slug2);
1222
1520
  try {
1223
- statSync2(worktreePath);
1521
+ statSync3(worktreePath);
1224
1522
  } catch {
1225
1523
  return { removed: false, reason: "not found" };
1226
1524
  }
@@ -1355,6 +1653,11 @@ var init_NodeDiskFilesystem = __esm({
1355
1653
  throw new Error(`File not found: ${path}`);
1356
1654
  }
1357
1655
  }
1656
+ /** The REAL on-disk path behind a VFS path — an optional capability (not on IFilesystem) the Read
1657
+ * tool duck-types so it can persist an image REFERENCE instead of megabytes of base64. */
1658
+ realPath(path) {
1659
+ return this.real(this.resolvePath(path));
1660
+ }
1358
1661
  /** Read raw bytes (for binary files like images). Not on the base IFilesystem (which is utf8-only) — an
1359
1662
  * optional capability the Read tool duck-types to return image blocks. Same symlink/jail guards as readFile. */
1360
1663
  async readFileBytes(path) {
@@ -1537,6 +1840,12 @@ var init_JailedFilesystem = __esm({
1537
1840
  if (typeof inner.readFileBytes !== "function") throw new Error("binary read not supported by this filesystem");
1538
1841
  return inner.readFileBytes(this.guardRead("readFileBytes", path));
1539
1842
  }
1843
+ /** Forward the optional real-path capability (image REFS), jail-guarded like readFile. */
1844
+ realPath(path) {
1845
+ const inner = this.inner;
1846
+ if (typeof inner.realPath !== "function") throw new Error("realPath not supported by this filesystem");
1847
+ return inner.realPath(this.guardRead("realPath", path));
1848
+ }
1540
1849
  async writeFile(path, content) {
1541
1850
  return this.inner.writeFile(this.guardWrite("writeFile", path), content);
1542
1851
  }
@@ -1777,13 +2086,13 @@ function makeRealShellTool(options) {
1777
2086
  }
1778
2087
  };
1779
2088
  try {
1780
- return await new Promise((resolve4) => {
2089
+ return await new Promise((resolve5) => {
1781
2090
  let out = "";
1782
2091
  let settled = false;
1783
2092
  const finish = (s) => {
1784
2093
  if (settled) return;
1785
2094
  settled = true;
1786
- resolve4(s);
2095
+ resolve5(s);
1787
2096
  };
1788
2097
  let proc;
1789
2098
  try {
@@ -1806,7 +2115,7 @@ function makeRealShellTool(options) {
1806
2115
  proc.stderr?.on("data", collect);
1807
2116
  proc.on("error", (err2) => {
1808
2117
  if (err2?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
1809
- log14.debug("shell spawn error", err2);
2118
+ log16.debug("shell spawn error", err2);
1810
2119
  finish(`[exit 1] ${err2?.message ?? err2}${out ? "\n" + clean(out) : ""}`);
1811
2120
  });
1812
2121
  proc.on("close", (code) => {
@@ -1868,7 +2177,7 @@ ${clean(out) || "(no output yet)"}`;
1868
2177
  }
1869
2178
  ];
1870
2179
  }
1871
- var log14, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
2180
+ var log16, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
1872
2181
  var init_tools_shell = __esm({
1873
2182
  "src/tools.shell.ts"() {
1874
2183
  "use strict";
@@ -1876,7 +2185,7 @@ var init_tools_shell = __esm({
1876
2185
  init_redact();
1877
2186
  init_logging();
1878
2187
  init_shell_sandbox();
1879
- log14 = forComponent("shell");
2188
+ log16 = forComponent("shell");
1880
2189
  clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
1881
2190
  DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
1882
2191
  SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
@@ -1958,8 +2267,8 @@ var init_tools_shell = __esm({
1958
2267
 
1959
2268
  // cli/cli.ts
1960
2269
  import { createInterface } from "readline/promises";
1961
- import { existsSync as existsSync9, readFileSync as readFileSync8, appendFileSync, mkdirSync as mkdirSync12, writeFileSync as writeFileSync9, readdirSync as readdirSync4, statSync as statSync4, unlinkSync as unlinkSync5 } from "fs";
1962
- import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
2270
+ import { existsSync as existsSync9, readFileSync as readFileSync9, appendFileSync, mkdirSync as mkdirSync12, writeFileSync as writeFileSync9, readdirSync as readdirSync4, statSync as statSync5, unlinkSync as unlinkSync5 } from "fs";
2271
+ import { homedir as homedir10 } from "os";
1963
2272
  import { spawnSync as spawnSync6 } from "child_process";
1964
2273
 
1965
2274
  // cli/clipboard.ts
@@ -2027,21 +2336,11 @@ function copyTextToClipboard(text, platform2 = process.platform) {
2027
2336
  }
2028
2337
 
2029
2338
  // cli/cli.ts
2030
- import { join as join14, resolve as resolve3, basename as basename2, extname, dirname as dirname5 } from "path";
2339
+ import { join as join14, resolve as resolve4, basename as basename2, extname, dirname as dirname5 } from "path";
2031
2340
  import { AIClient, listModels, listProviders, getProviderFromModel, getModelInfo, resolveModel, isModelSupported, disposeCursorSessions, disposeClaudeCodeSessions } from "ai.libx.js";
2032
2341
 
2033
- // src/llm.ts
2034
- function contentText(content) {
2035
- if (content == null) return "";
2036
- if (typeof content === "string") return content;
2037
- return content.map((p) => p.type === "text" ? p.text ?? "" : "[image]").join(p_sep(content));
2038
- }
2039
- var p_sep = (parts) => parts.length > 1 ? "\n" : "";
2040
- function imagePart(url) {
2041
- return { type: "image_url", image_url: { url } };
2042
- }
2043
-
2044
2342
  // src/Agent.ts
2343
+ init_llm();
2045
2344
  init_tools();
2046
2345
 
2047
2346
  // src/tools.jobs.ts
@@ -2680,7 +2979,7 @@ init_OverlayFilesystem();
2680
2979
  init_worktree();
2681
2980
  init_NodeDiskFilesystem();
2682
2981
  init_logging();
2683
- var log3 = forComponent("subagent");
2982
+ var log5 = forComponent("subagent");
2684
2983
  async function boundedPool(items, limit, fn) {
2685
2984
  const out = new Array(items.length);
2686
2985
  let next = 0;
@@ -2722,7 +3021,7 @@ function traceFileFor(dir, label, depth) {
2722
3021
  const id = `${Date.now().toString(36)}-${(traceSeq++).toString(36)}`;
2723
3022
  return join4(dir, `${id}-d${depth}-${sanitizeSlug(label) || "sub"}.json`);
2724
3023
  } catch (e) {
2725
- log3.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
3024
+ log5.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
2726
3025
  return void 0;
2727
3026
  }
2728
3027
  }
@@ -2730,7 +3029,7 @@ function flushTrace(file, meta, messages, status) {
2730
3029
  try {
2731
3030
  writeFileSync2(file, JSON.stringify({ ...meta, status, updated: Date.now(), messages }, null, 2));
2732
3031
  } catch (e) {
2733
- log3.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
3032
+ log5.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
2734
3033
  }
2735
3034
  }
2736
3035
  function installTrace(child, file, meta) {
@@ -3191,7 +3490,11 @@ function reasoningToChatFragment(model, effort) {
3191
3490
  }
3192
3491
 
3193
3492
  // src/Agent.ts
3194
- var log4 = forComponent("Agent");
3493
+ var log6 = forComponent("Agent");
3494
+ function envInt(key, fallback) {
3495
+ const n = typeof process !== "undefined" ? Number(process.env?.[key]) : NaN;
3496
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
3497
+ }
3195
3498
  function isBrowserTool(name) {
3196
3499
  return name.startsWith("mcp__") && /browser/i.test(name.slice(5).split("__")[0]);
3197
3500
  }
@@ -3217,8 +3520,9 @@ var AgentOptions = class {
3217
3520
  /** Hard ceiling on accumulated tokens (prompt+completion) across the run; cache READS count at 0.1×
3218
3521
  * (their real price) so a fat cached context doesn't trip the guard on healthy turns. 0 = unbounded. */
3219
3522
  maxTokens = 2e5;
3220
- /** Wall-clock ceiling in ms for the whole run. 0 = unbounded. */
3221
- timeoutMs = 12e4;
3523
+ /** Idle ceiling in ms: max time since the last productive step before the run is killed (a long/active
3524
+ * step resets it — see runLoop's `lastProgressAt`). Not a whole-run wall clock. 0 = unbounded. */
3525
+ timeoutMs = 3e5;
3222
3526
  /** Idle-stall watchdog for a SINGLE streamed model request: abort it if no chunk (text OR reasoning)
3223
3527
  * arrives for this long. The between-steps `timeoutMs` can't preempt an in-flight `chat()`, so a
3224
3528
  * provider that goes silent mid-stream would otherwise park the loop forever. 0 = off. */
@@ -3229,15 +3533,33 @@ var AgentOptions = class {
3229
3533
  maxToolCalls = 100;
3230
3534
  /** External cancellation — abort the loop between steps (e.g. a UI "Cancel" button). */
3231
3535
  signal;
3536
+ /** Retry budget for an EMPTY/silent-turn (a delegated-runtime wedge: cursor's warm helper spins then
3537
+ * returns a content-less end, surfaced as `code:'empty'`/`code:'cursor_silent'`). Tuned WIDER than the
3538
+ * network-drop budget (fixed 2) because clearing a wedge needs several reap+respawn cycles — each retry
3539
+ * hits a FRESH cursor session (the adapter evicts the wedged warm helper on the silent-end). Still bounded:
3540
+ * after exhaustion the turn surfaces `finishReason:'empty'` (non-zero exit), never infinite, never silent.
3541
+ * Env: AGENTX_EMPTY_RETRIES. */
3542
+ emptyRetries = envInt("AGENTX_EMPTY_RETRIES", 4);
3543
+ /** Base backoff (ms) between empty-turn retries; grows linearly per attempt (longer than the network
3544
+ * class's fixed 1s so a wedged helper has time to be reaped/respawned). Env: AGENTX_EMPTY_RETRY_BASE_MS. */
3545
+ emptyRetryBaseMs = envInt("AGENTX_EMPTY_RETRY_BASE_MS", 1500);
3232
3546
  /** 0 = never trim. Otherwise cap the messages sent per turn (system + most recent). */
3233
3547
  maxContextMessages = 0;
3234
3548
  /** Note-taking: keep the most-recent N tool-result outputs verbatim; collapse OLDER ones to a one-line
3235
3549
  * stub in the sent context (the model already consumed them — it can re-Read/re-run). The stored
3236
3550
  * transcript is never mutated. 0 = keep all verbatim. */
3237
3551
  keepToolOutputs = 0;
3552
+ /** How many of the most-recent image-bearing messages get their image REF expanded to real base64 in
3553
+ * the outgoing request. Older ones (and legacy inline base64) go on the wire as the elision stub. */
3554
+ keepRecentImages = 1;
3238
3555
  /** Token-aware backstop (~4 chars/token estimate). After note-taking, drop oldest messages from the
3239
3556
  * sent context until the estimate is under this ceiling (pairing-safe). 0 = off. */
3240
3557
  maxContextTokens = 0;
3558
+ /** The MODEL's context window (tokens) — what the context % is measured against and what `/compact`
3559
+ * targets 50% of. Filled by the CLI from model metadata; 0 = fall back to 200k. Deliberately NOT
3560
+ * `maxTokens`: that is the cumulative SPEND kill-switch, and a spend budget must never be able to
3561
+ * drive destructive compaction (`--max-tokens 50000` would otherwise shred the transcript). */
3562
+ contextWindow = 0;
3241
3563
  /** Pagination ceiling for a SINGLE tool result (bytes). A result over this is cropped to page 1 with
3242
3564
  * a marker telling the model it was cropped (refine the query, or page further). Guards against one
3243
3565
  * Grep/Read/MCP call blowing the whole context window. 0 = off. Default 60k (~15k tokens). */
@@ -3481,7 +3803,7 @@ var Agent = class _Agent {
3481
3803
  const disk = new NodeDiskFilesystem2(process.cwd());
3482
3804
  await disk.init();
3483
3805
  this.options.fs = new JailedFilesystem2(disk);
3484
- log4.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
3806
+ log6.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
3485
3807
  }
3486
3808
  this.buildCtx();
3487
3809
  }
@@ -3599,19 +3921,42 @@ var Agent = class _Agent {
3599
3921
  return this.runLoop();
3600
3922
  }
3601
3923
  /**
3602
- * Fold the conversation in place (manual `/compact`): keep the system message + the
3603
- * most-recent window, summarizing the dropped middle (deterministic, no LLM call).
3604
- * No-op when the transcript already fits. Returns the number of messages removed.
3605
- * `focus` (e.g. from `/compact keep the API details`) preserves matching lines from the
3606
- * dropped span verbatim in the summary, instead of losing them to the generic recap.
3924
+ * Fold the conversation in place (manual `/compact`) until it fits a TOKEN budget — not a message
3925
+ * count. Message-count folding alone frees almost nothing when a few recent tool results are huge,
3926
+ * so we escalate: fold the middle into a synthetic recap → elide oversized tool-result bodies in the
3927
+ * retained window → halve the window → drop-oldest backstop. Deterministic, no LLM call.
3928
+ * `focus` (e.g. `/compact keep the API details`) preserves matching lines from the dropped span verbatim.
3929
+ * Returns before/after token estimates so the caller can report honestly (0 freed => say so).
3607
3930
  */
3608
3931
  compactNow(maxMessages = 12, focus) {
3609
- const max = Math.max(2, maxMessages);
3610
- if (this.transcript.length <= max) return 0;
3932
+ const cap = _Agent.contextCap(this.options);
3933
+ const target = Math.round(cap * _Agent.COMPACT_TARGET);
3934
+ const kr = this.options.keepRecentImages ?? 1;
3935
+ const tokensBefore = estimateTokens(this.transcript, kr);
3936
+ const msgsBefore = this.transcript.length;
3937
+ let max = Math.max(2, maxMessages);
3938
+ if (this.transcript.length <= max && tokensBefore <= target)
3939
+ return { tokensBefore, tokensAfter: tokensBefore, target, cap, removed: 0 };
3611
3940
  void this.activeHooks?.onPreCompact?.(this.transcript);
3612
- const before = this.transcript.length;
3613
- this.transcript = compact(this.transcript, max, focus);
3614
- return before - this.transcript.length;
3941
+ let out = this.transcript;
3942
+ if (out.length > max) out = compact(out, max, focus);
3943
+ if (estimateTokens(out, kr) > target) out = elideOversizedImages(out, target, true, kr);
3944
+ if (estimateTokens(out, kr) > target) out = capOversizedBodies(out, target, kr);
3945
+ if (estimateTokens(out, kr) > target) out = elideOversizedImages(out, target, false, kr);
3946
+ while (estimateTokens(out, kr) > target && max > 2 && out.length > 2) {
3947
+ max = Math.max(2, Math.floor(max / 2));
3948
+ out = capOversizedBodies(compact(out, max, focus), target, kr);
3949
+ }
3950
+ if (estimateTokens(out, kr) > target) out = fitTokenBudget(out, target, kr);
3951
+ out = dropOrphanToolResults(out);
3952
+ this.transcript = out;
3953
+ this.releaseStaleImages();
3954
+ return { tokensBefore, tokensAfter: estimateTokens(this.transcript, kr), target, cap, removed: msgsBefore - this.transcript.length };
3955
+ }
3956
+ /** Drop inline base64 images from older messages so Bun/JSC can reclaim the heap (see contentBytes). */
3957
+ releaseStaleImages(keepRecent = 1) {
3958
+ const next = elideStaleImages(this.transcript, keepRecent);
3959
+ if (next !== this.transcript) this.transcript = next;
3615
3960
  }
3616
3961
  async runLoop() {
3617
3962
  const o = this.options;
@@ -3627,15 +3972,16 @@ var Agent = class _Agent {
3627
3972
  let lastFp = "";
3628
3973
  let repeats = 0;
3629
3974
  let closureNudged = false;
3975
+ let lastProgressAt = start;
3630
3976
  const kill = (finishReason) => {
3631
- 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` : ""})`);
3977
+ 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` : ""})`);
3632
3978
  this.ctx.jobs?.killAll();
3633
3979
  return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated };
3634
3980
  };
3635
3981
  while (true) {
3636
3982
  if (o.signal?.aborted) return kill("aborted");
3637
3983
  if (steps >= o.maxSteps) return kill("max_steps");
3638
- if (o.timeoutMs && Date.now() - start - this.parkedMs >= o.timeoutMs) return kill("timeout");
3984
+ if (o.timeoutMs && Date.now() - lastProgressAt - this.parkedMs >= o.timeoutMs) return kill("timeout");
3639
3985
  const budgetTokens = usage.totalTokens - 0.9 * usage.cacheReadTokens;
3640
3986
  if (o.maxTokens && budgetTokens >= o.maxTokens) return kill("budget");
3641
3987
  steps++;
@@ -3675,27 +4021,38 @@ var Agent = class _Agent {
3675
4021
  const r = await o.ai.chat({ model: o.model, messages: sent, tools: sentTools, stream: false, signal: o.signal, ...o.toolChoice ? { toolChoice: o.toolChoice } : {}, ...reasonOpts });
3676
4022
  res = r;
3677
4023
  }
4024
+ if (steps === 1 && this.isEmptyStop(res) && !this.pendingInjections.length) {
4025
+ const e = new Error("model returned an empty turn (finishReason=stop, no text, no tools)");
4026
+ e.code = "empty";
4027
+ throw e;
4028
+ }
3678
4029
  break;
3679
4030
  } catch (err2) {
3680
4031
  const sc = err2?.statusCode;
3681
4032
  const serverSide = sc >= 500 || sc === 408 || /Service Unavailable|overloaded|Internal server error|Bad gateway|Gateway time/i.test(String(err2?.message ?? ""));
3682
4033
  const network = !sc && /ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|EPIPE|socket hang up|fetch failed|network|terminated|UND_ERR/i.test(String(err2?.message ?? err2?.code ?? err2));
3683
4034
  const stalled = err2?.code === "stall";
3684
- const transient = !o.signal?.aborted && !isAbortError(err2) && attempt < 2 && (network || serverSide || stalled);
4035
+ const empty = err2?.code === "empty" || err2?.code === "cursor_silent";
4036
+ const maxAttempts = empty ? Math.max(o.emptyRetries, 0) : 2;
4037
+ const transient = !o.signal?.aborted && !isAbortError(err2) && attempt < maxAttempts && (network || serverSide || stalled || empty);
3685
4038
  if (!transient) throw err2;
3686
- const waitMs = 1e3 * (attempt + 1);
3687
- log4.warn(`${stalled ? "model stall" : "network drop"} mid-step (${err2?.message ?? err2}) \u2014 retrying in ${waitMs}ms`);
3688
- o.host?.notify?.({ kind: "retry", message: `${stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
4039
+ const waitMs = (empty ? o.emptyRetryBaseMs : 1e3) * (attempt + 1);
4040
+ log6.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err2?.message ?? err2}) \u2014 retrying in ${waitMs}ms`);
4041
+ o.host?.notify?.({ kind: "retry", message: `${empty ? "model returned nothing" : stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
3689
4042
  await new Promise((r) => setTimeout(r, waitMs));
3690
4043
  }
3691
4044
  }
3692
4045
  } catch (err2) {
3693
4046
  if (err2?.code === "budget") return kill("budget");
3694
4047
  if (err2?.code === "stall") {
3695
- log4.warn(`model stalled out after retries: ${err2?.message ?? err2}`);
4048
+ log6.warn(`model stalled out after retries: ${err2?.message ?? err2}`);
3696
4049
  return kill("timeout");
3697
4050
  }
3698
4051
  if (o.signal?.aborted || isAbortError(err2)) return kill("aborted");
4052
+ if (err2?.code === "empty" || err2?.code === "cursor_silent") {
4053
+ log6.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
4054
+ return { text: "", steps, finishReason: "empty", messages: this.transcript, usage, usageEstimated, error: err2 };
4055
+ }
3699
4056
  const body = err2?.body ?? err2?.response?.data ?? err2?.error;
3700
4057
  let bodyStr;
3701
4058
  try {
@@ -3705,7 +4062,7 @@ var Agent = class _Agent {
3705
4062
  }
3706
4063
  if (bodyStr && err2 instanceof Error && !err2.message.includes(bodyStr)) err2.detail = bodyStr;
3707
4064
  const errLog = err2 instanceof Error ? { message: err2.message, ...err2.detail ? { detail: err2.detail } : {}, stack: err2.stack } : err2;
3708
- log4.error(`chat() failed: ${err2?.message ?? err2}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
4065
+ log6.error(`chat() failed: ${err2?.message ?? err2}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
3709
4066
  return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err2 };
3710
4067
  }
3711
4068
  if (o.signal?.aborted) return kill("aborted");
@@ -3749,6 +4106,7 @@ var Agent = class _Agent {
3749
4106
  }
3750
4107
  const toolCalls = res.toolCalls ?? [];
3751
4108
  const emptyTurn = toolCalls.length === 0 && delegatedTools.length === 0 && contentText(res.content ?? "").trim() === "";
4109
+ if (!emptyTurn) lastProgressAt = Date.now();
3752
4110
  if (!emptyTurn) {
3753
4111
  this.transcript.push({
3754
4112
  role: "assistant",
@@ -3760,12 +4118,13 @@ var Agent = class _Agent {
3760
4118
  if (this.drainInjections()) continue;
3761
4119
  if (o.closeDelegatedTurns && res.endedOnToolNoText && !closureNudged && steps < o.maxSteps) {
3762
4120
  closureNudged = true;
3763
- log4.verbose("delegated turn went silent after a tool \u2014 nudging a closing summary");
4121
+ log6.verbose("delegated turn went silent after a tool \u2014 nudging a closing summary");
3764
4122
  this.transcript.push({ role: "user", content: "Summarize what you did this turn and what (if anything) still needs doing. Be concise." });
3765
4123
  continue;
3766
4124
  }
3767
- log4.verbose(`completed in ${steps} step(s)`);
4125
+ log6.verbose(`completed in ${steps} step(s)`);
3768
4126
  await this.ctx.jobs?.drain();
4127
+ this.releaseStaleImages();
3769
4128
  await this.activeHooks?.onStop?.(res.content ?? "");
3770
4129
  return { text: res.content ?? "", steps, finishReason: "stop", messages: this.transcript, usage, usageEstimated };
3771
4130
  }
@@ -3791,7 +4150,7 @@ var Agent = class _Agent {
3791
4150
  if (this.ctx.needsInput) {
3792
4151
  const question = this.ctx.needsInput;
3793
4152
  this.ctx.needsInput = void 0;
3794
- log4.warn(`needs_input: ${question.question} (steps=${steps})`);
4153
+ log6.warn(`needs_input: ${question.question} (steps=${steps})`);
3795
4154
  await this.ctx.jobs?.drain();
3796
4155
  const text = [
3797
4156
  `OPEN QUESTION (run paused \u2014 no human was reachable): ${question.question}`,
@@ -3848,6 +4207,12 @@ var Agent = class _Agent {
3848
4207
  }
3849
4208
  };
3850
4209
  }
4210
+ /** A clean-stop turn that carries no assistant text AND no tool calls (native or delegated) — a
4211
+ * non-result the model produced by going silent. Callers retry it rather than report "done". */
4212
+ isEmptyStop(res) {
4213
+ const stop = res.finishReason === "stop" || res.finishReason == null;
4214
+ return stop && (res.toolCalls?.length ?? 0) === 0 && (res.delegatedTools?.length ?? 0) === 0 && contentText(res.content ?? "").trim() === "";
4215
+ }
3851
4216
  async consumeStream(stream, poke) {
3852
4217
  let content = "";
3853
4218
  let finishReason;
@@ -3906,13 +4271,13 @@ var Agent = class _Agent {
3906
4271
  const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
3907
4272
  if (decision?.block) {
3908
4273
  const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
3909
- log4.debug(`${tc.function.name} -> ${blocked}`);
4274
+ log6.debug(`${tc.function.name} -> ${blocked}`);
3910
4275
  await hooks?.postToolUse?.(call, blocked, meta);
3911
4276
  return blocked;
3912
4277
  }
3913
4278
  this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
3914
4279
  if (earlyError) {
3915
- log4.debug(`${tc.function.name} -> ${earlyError}`);
4280
+ log6.debug(`${tc.function.name} -> ${earlyError}`);
3916
4281
  await hooks?.postToolUse?.(call, earlyError, meta);
3917
4282
  this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
3918
4283
  return earlyError;
@@ -3921,12 +4286,12 @@ var Agent = class _Agent {
3921
4286
  let images;
3922
4287
  let threw = false;
3923
4288
  try {
3924
- log4.debug(`${tc.function.name}(${tc.function.arguments})`);
4289
+ log6.debug(`${tc.function.name}(${tc.function.arguments})`);
3925
4290
  this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
3926
4291
  try {
3927
4292
  hooks.onToolOutput(call, chunk, meta);
3928
4293
  } catch (e) {
3929
- log4.debug(`onToolOutput hook error: ${e}`);
4294
+ log6.debug(`onToolOutput hook error: ${e}`);
3930
4295
  }
3931
4296
  } : void 0;
3932
4297
  const raw = await tool.run(args, this.ctx);
@@ -3938,7 +4303,7 @@ var Agent = class _Agent {
3938
4303
  }
3939
4304
  } catch (e) {
3940
4305
  const msg = e instanceof Error ? e.message : String(e);
3941
- log4.debug(`${tc.function.name} -> error: ${msg}`);
4306
+ log6.debug(`${tc.function.name} -> error: ${msg}`);
3942
4307
  result = `Error: ${msg}`;
3943
4308
  threw = true;
3944
4309
  } finally {
@@ -3964,6 +4329,16 @@ var Agent = class _Agent {
3964
4329
  }
3965
4330
  return images?.length ? { text: result, images } : result;
3966
4331
  }
4332
+ /** `/compact` aims to land the transcript at this fraction of the context window. */
4333
+ static COMPACT_TARGET = 0.5;
4334
+ /** THE context-window cap resolution — `/compact`'s budget and the CLI's context footer/warnings both
4335
+ * route here, so the percentage the user reads and the one compaction targets can never drift. */
4336
+ static contextCap(o) {
4337
+ if (o.maxContextTokens) return o.maxContextTokens;
4338
+ const w = o.contextWindow || 0;
4339
+ if (w > ANTHROPIC_WINDOW_WITHOUT_1M_BETA && isAnthropicModel(o.model)) return ANTHROPIC_WINDOW_WITHOUT_1M_BETA;
4340
+ return w || 2e5;
4341
+ }
3967
4342
  static WRITE_CLASS = ["Write", "Edit", "MultiEdit", "ApplyEdits"];
3968
4343
  /** Append an autoTest failure section to a write-class tool result, if configured. */
3969
4344
  async maybeAutoTest(toolName, result) {
@@ -3997,26 +4372,27 @@ ${out}`;
3997
4372
  const browserKeep = this.browserActive ? o.browserTrim?.keepOutputs : void 0;
3998
4373
  const keep = browserKeep != null ? o.keepToolOutputs ? Math.min(o.keepToolOutputs, browserKeep) : browserKeep : o.keepToolOutputs;
3999
4374
  if (keep) out = stubOldToolResults(out ?? m, keep);
4375
+ out = expandImagesForSend(dropOrphanToolResults(out ?? m), o.keepRecentImages ?? 1);
4000
4376
  if (o.maxContextTokens) {
4001
- const pre = (out ?? m).length;
4002
- out = fitTokenBudget(out ?? m, o.maxContextTokens);
4377
+ const pre = out.length;
4378
+ out = fitTokenBudget(out, o.maxContextTokens, o.keepRecentImages ?? 1);
4003
4379
  const dropped = pre - out.length;
4004
4380
  if (dropped > 0 && dropped !== this.lastTrimNotified) {
4005
4381
  this.lastTrimNotified = dropped;
4006
4382
  o.host?.notify?.({ kind: "compaction", message: `context full \u2014 auto-trimmed ${dropped} oldest message(s) to fit ~${Math.round(o.maxContextTokens / 1e3)}k tokens` });
4007
4383
  }
4008
4384
  }
4009
- return dropOrphanToolResults(out ?? m);
4385
+ return dropOrphanToolResults(out);
4010
4386
  }
4011
4387
  };
4012
4388
  function dropOldest(m, max) {
4013
4389
  const head = m[0]?.role === "system" ? [m[0]] : [];
4014
4390
  return [...head, ...m.slice(-(max - head.length))];
4015
4391
  }
4016
- function estimateTokens(m) {
4017
- let chars = 0;
4018
- for (const x of m) chars += contentText(x.content).length + (x.tool_calls ? JSON.stringify(x.tool_calls).length : 0);
4019
- return Math.ceil(chars / 4);
4392
+ var ANTHROPIC_WINDOW_WITHOUT_1M_BETA = 2e5;
4393
+ var isAnthropicModel = (m) => !!m && (m.startsWith("anthropic/") || /(^|\/)claude-/.test(m));
4394
+ function estimateTokens(m, keepRecentImages = 1) {
4395
+ return Math.ceil(sendBytes(m, keepRecentImages) / 4);
4020
4396
  }
4021
4397
  function cropResult(result, cap) {
4022
4398
  const head = result.slice(0, cap);
@@ -4050,6 +4426,60 @@ function stubOldToolResults(messages, keep) {
4050
4426
  return { ...x, content: `[${x.name ?? "tool"}${where ? ` ${where}` : ""} output elided \u2014 ${lines} lines; re-run the tool to view]` };
4051
4427
  });
4052
4428
  }
4429
+ function elideOversizedImages(messages, target, protectNewest = false, keepRecentImages = 1) {
4430
+ let out = messages;
4431
+ const withImages = out.map((_, i) => i).filter((i) => messageHasImage(out[i].content));
4432
+ const newest = protectNewest ? withImages[withImages.length - 1] : -1;
4433
+ const weight = sendBytesPer(out, keepRecentImages);
4434
+ const heaviestFirst = withImages.filter((i) => i !== newest).sort((a, b) => weight[b] - weight[a]);
4435
+ for (const i of heaviestFirst) {
4436
+ if (estimateTokens(out, keepRecentImages) <= target) break;
4437
+ const c = elideImages(out[i].content);
4438
+ if (c === out[i].content) continue;
4439
+ out = out.map((m, j) => j === i ? { ...m, content: c } : m);
4440
+ }
4441
+ return out;
4442
+ }
4443
+ function capOversizedBodies(messages, target, keepRecentImages = 1) {
4444
+ let out = messages;
4445
+ for (const cap of [8e3, 4e3, 2e3, 800, 300, 120]) {
4446
+ if (estimateTokens(out, keepRecentImages) <= target) break;
4447
+ out = out.map((x) => x.role === "system" ? x : elideBody(x, cap));
4448
+ }
4449
+ return out;
4450
+ }
4451
+ function elideBody(x, cap) {
4452
+ const note = (bytes, imgs2) => `
4453
+
4454
+ [${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` : ""}]`;
4455
+ if (!Array.isArray(x.content)) {
4456
+ const text = contentText(x.content);
4457
+ if (text.length <= cap) return x;
4458
+ return { ...x, content: `${text.slice(0, cap)}${note(text.length - cap, 0)}` };
4459
+ }
4460
+ const imgs = x.content.filter((p) => p.type !== "text").length;
4461
+ let left = cap;
4462
+ let elided = 0;
4463
+ const parts = [];
4464
+ for (const p of x.content) {
4465
+ if (p.type !== "text") {
4466
+ parts.push(p);
4467
+ continue;
4468
+ }
4469
+ const t = p.text ?? "";
4470
+ if (t.length <= left) {
4471
+ left -= t.length;
4472
+ if (t) parts.push(p);
4473
+ continue;
4474
+ }
4475
+ const keep = left;
4476
+ left = 0;
4477
+ elided += t.length - keep;
4478
+ if (keep > 0) parts.push({ ...p, text: t.slice(0, keep) });
4479
+ }
4480
+ if (!elided) return parts.length === x.content.length || !parts.length ? x : { ...x, content: parts };
4481
+ return { ...x, content: [...parts, { type: "text", text: note(elided, imgs).trimStart() }] };
4482
+ }
4053
4483
  var callIdSet = (msgs) => {
4054
4484
  const ids = /* @__PURE__ */ new Set();
4055
4485
  for (const m of msgs) if (m.role === "assistant") for (const tc of m.tool_calls ?? []) ids.add(tc.id);
@@ -4060,8 +4490,8 @@ function dropOrphanToolResults(messages) {
4060
4490
  const ok = (m) => m.role !== "tool" || ids.has(m.tool_call_id ?? "");
4061
4491
  return messages.every(ok) ? messages : messages.filter(ok);
4062
4492
  }
4063
- function fitTokenBudget(messages, maxTokens) {
4064
- const per = messages.map((x) => estimateTokens([x]));
4493
+ function fitTokenBudget(messages, maxTokens, keepRecentImages = 1) {
4494
+ const per = sendBytesPer(messages, keepRecentImages).map((b) => Math.ceil(b / 4));
4065
4495
  let total = per.reduce((a, b) => a + b, 0);
4066
4496
  if (total <= maxTokens) return messages;
4067
4497
  const head = messages[0]?.role === "system" ? [messages[0]] : [];
@@ -4070,7 +4500,7 @@ function fitTokenBudget(messages, maxTokens) {
4070
4500
  const ids = callIdSet(messages.slice(from));
4071
4501
  while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
4072
4502
  if (total > maxTokens)
4073
- log4.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
4503
+ log6.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
4074
4504
  return [...head, ...messages.slice(from)];
4075
4505
  }
4076
4506
  function compact(m, max, focus) {
@@ -4082,11 +4512,24 @@ function compact(m, max, focus) {
4082
4512
  if (dropped.length === 0) return [...head, ...tail];
4083
4513
  return [...head, { role: "system", content: summarize(dropped, focus) }, ...tail];
4084
4514
  }
4515
+ var SUMMARY_HEAD = "[summary of ";
4516
+ var MAX_CARRIED_COUNT = 1e4;
4517
+ var isSyntheticSummary = (m) => m.role === "system" && typeof m.content === "string" && m.content.startsWith(SUMMARY_HEAD);
4085
4518
  function summarize(messages, focus) {
4086
4519
  const tools = /* @__PURE__ */ new Set();
4087
4520
  const files = /* @__PURE__ */ new Set();
4521
+ const carried = [];
4522
+ let carriedCount = 0;
4523
+ let priors = 0;
4088
4524
  let lastAssistant = "";
4089
4525
  for (const msg of messages) {
4526
+ if (isSyntheticSummary(msg)) {
4527
+ priors++;
4528
+ const [head, ...rest] = String(msg.content).split("\n");
4529
+ carriedCount += Math.min(Math.max(Number(/of (\d+)/.exec(head)?.[1]) || 0, 0), MAX_CARRIED_COUNT);
4530
+ for (const l of rest) if (l.trim() && !carried.includes(l) && carried.length < 20) carried.push(l);
4531
+ continue;
4532
+ }
4090
4533
  for (const tc of msg.tool_calls ?? []) {
4091
4534
  tools.add(tc.function.name);
4092
4535
  try {
@@ -4097,14 +4540,16 @@ function summarize(messages, focus) {
4097
4540
  }
4098
4541
  if (msg.role === "assistant" && msg.content) lastAssistant = contentText(msg.content);
4099
4542
  }
4100
- const lines = [`[summary of ${messages.length} earlier message(s)]`];
4543
+ const lines = [`${SUMMARY_HEAD}${messages.length - priors + carriedCount} earlier message(s)]`];
4101
4544
  if (tools.size) lines.push(`tools used: ${[...tools].join(", ")}`);
4102
4545
  if (files.size) lines.push(`files touched: ${[...files].join(", ")}`);
4103
4546
  if (lastAssistant) lines.push(`last assistant: ${lastAssistant}`);
4547
+ if (carried.length) lines.push("earlier recap:", ...carried.map((l) => ` ${l}`));
4104
4548
  if (focus?.trim()) {
4105
4549
  const words = focus.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
4106
4550
  const kept = [];
4107
- outer: for (const msg of messages) {
4551
+ const ordered = [...messages.filter((m) => !isSyntheticSummary(m)), ...messages.filter(isSyntheticSummary)];
4552
+ outer: for (const msg of ordered) {
4108
4553
  if (msg.role === "tool") continue;
4109
4554
  for (const line of contentText(msg.content).split("\n")) {
4110
4555
  const l = line.toLowerCase();
@@ -4627,7 +5072,7 @@ init_tools_web();
4627
5072
  init_tools();
4628
5073
  init_tools_structured();
4629
5074
  init_logging();
4630
- var log5 = forComponent("scratch");
5075
+ var log7 = forComponent("scratch");
4631
5076
  var SCRATCH_DIR = "/scratch";
4632
5077
  function shortArgs(args) {
4633
5078
  try {
@@ -4667,7 +5112,7 @@ var Scratch = class {
4667
5112
  await (this.dirReady ??= mkdirp(this.fs, dir));
4668
5113
  await this.fs.writeFile(path, header + raw);
4669
5114
  } catch (e) {
4670
- log5.debug("scratch write failed; returning raw", e);
5115
+ log7.debug("scratch write failed; returning raw", e);
4671
5116
  return raw;
4672
5117
  }
4673
5118
  const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
@@ -4697,7 +5142,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
4697
5142
  await (this.dirReady ??= mkdirp(this.fs, dir));
4698
5143
  await this.fs.writeFile(path, header + full);
4699
5144
  } catch (e) {
4700
- log5.debug("scratch spill failed; cropping lossy", e);
5145
+ log7.debug("scratch spill failed; cropping lossy", e);
4701
5146
  return full.slice(0, pageBytes) + `
4702
5147
 
4703
5148
  [output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
@@ -4743,7 +5188,7 @@ Question: ${q2}`);
4743
5188
  const answer = (res.text ?? "").trim();
4744
5189
  return answer || "(no answer found in scratch)";
4745
5190
  } catch (e) {
4746
- log5.debug("Ask peek failed", e);
5191
+ log7.debug("Ask peek failed", e);
4747
5192
  return `Error querying scratch: ${e?.message ?? e}`;
4748
5193
  }
4749
5194
  }
@@ -4752,7 +5197,7 @@ Question: ${q2}`);
4752
5197
 
4753
5198
  // src/lessons.ts
4754
5199
  init_logging();
4755
- var log6 = forComponent("Lessons");
5200
+ var log8 = forComponent("Lessons");
4756
5201
  var LessonOptionsDefaults = class {
4757
5202
  minRepeats = 2;
4758
5203
  };
@@ -4777,15 +5222,16 @@ function lessonCapture(options) {
4777
5222
  counts.set(lesson.slug, n);
4778
5223
  if (n < o.minRepeats) return;
4779
5224
  written.add(lesson.slug);
4780
- await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log6.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
4781
- log6.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
5225
+ await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log8.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
5226
+ log8.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
4782
5227
  }
4783
5228
  };
4784
5229
  }
4785
5230
 
4786
5231
  // src/reflect.ts
5232
+ init_llm();
4787
5233
  init_logging();
4788
- var log7 = forComponent("Reflect");
5234
+ var log9 = forComponent("Reflect");
4789
5235
  async function reflectOnRun(o) {
4790
5236
  const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
4791
5237
  if (!digest.trim()) return null;
@@ -4801,7 +5247,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
4801
5247
  const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
4802
5248
  text = r?.content ?? "";
4803
5249
  } catch (e) {
4804
- log7.warn(`reflection call failed: ${e?.message ?? e}`);
5250
+ log9.warn(`reflection call failed: ${e?.message ?? e}`);
4805
5251
  return null;
4806
5252
  }
4807
5253
  const m = text.match(/LESSON:\s*(.+)/i);
@@ -4811,10 +5257,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
4811
5257
  try {
4812
5258
  await writeFact(o.fs, o.dir, slug2, lesson);
4813
5259
  } catch (e) {
4814
- log7.warn(`could not persist lesson: ${e?.message ?? e}`);
5260
+ log9.warn(`could not persist lesson: ${e?.message ?? e}`);
4815
5261
  return null;
4816
5262
  }
4817
- log7.debug(`reflection persisted ${slug2}`);
5263
+ log9.debug(`reflection persisted ${slug2}`);
4818
5264
  return slug2;
4819
5265
  }
4820
5266
  function digestRun(messages, maxChars) {
@@ -4830,6 +5276,7 @@ function digestRun(messages, maxChars) {
4830
5276
 
4831
5277
  // src/duplex.ts
4832
5278
  import { MemFilesystem as MemFilesystem2 } from "@livx.cc/wcli/core";
5279
+ init_llm();
4833
5280
  init_logging();
4834
5281
 
4835
5282
  // node_modules/@bod.ee/voice/src/core/logging.ts
@@ -4862,7 +5309,7 @@ function forComponent2(name) {
4862
5309
  }
4863
5310
 
4864
5311
  // node_modules/@bod.ee/voice/src/core/emotion.ts
4865
- var log8 = forComponent2("Emotion");
5312
+ var log10 = forComponent2("Emotion");
4866
5313
  var EMOTIONS = [
4867
5314
  // primary (best results)
4868
5315
  "neutral",
@@ -4994,7 +5441,7 @@ function renderEmotions(text, opts = { show: true }) {
4994
5441
  speech += cartesiaTag(tag);
4995
5442
  if (opts.show) display += m[0];
4996
5443
  } else {
4997
- log8.debug(`dropping unknown emotion tag ${m[0]}`);
5444
+ log10.debug(`dropping unknown emotion tag ${m[0]}`);
4998
5445
  }
4999
5446
  last = m.index + m[0].length;
5000
5447
  }
@@ -5036,7 +5483,7 @@ var EmotionStream = class {
5036
5483
  if (tag) {
5037
5484
  this.pending = tag;
5038
5485
  if (this.show) display += m[0];
5039
- } else log8.debug(`dropping unknown emotion tag ${m[0]}`);
5486
+ } else log10.debug(`dropping unknown emotion tag ${m[0]}`);
5040
5487
  last = m.index + m[0].length;
5041
5488
  }
5042
5489
  this.emit(body.slice(last), (s, d, p) => {
@@ -5059,7 +5506,7 @@ var EmotionStream = class {
5059
5506
  };
5060
5507
 
5061
5508
  // node_modules/@bod.ee/voice/src/core/engine.ts
5062
- var log9 = forComponent2("VoiceEngine");
5509
+ var log11 = forComponent2("VoiceEngine");
5063
5510
  var realClock = {
5064
5511
  now: () => performance.now(),
5065
5512
  setTimeout: (fn, ms) => setTimeout(fn, ms),
@@ -5334,7 +5781,7 @@ var VoiceEngine = class _VoiceEngine {
5334
5781
  await Promise.all([this.tts.connect(), this.stt.start()]);
5335
5782
  this.tts.warmup?.();
5336
5783
  this.setState("listening");
5337
- log9.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
5784
+ log11.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
5338
5785
  }
5339
5786
  get usingAec() {
5340
5787
  return this.stt.usingAec;
@@ -5356,7 +5803,7 @@ var VoiceEngine = class _VoiceEngine {
5356
5803
  this.options.onDiag({ t: this.clock.now(), kind, ...fields });
5357
5804
  } catch (e) {
5358
5805
  this.diagOn = false;
5359
- log9.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
5806
+ log11.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
5360
5807
  }
5361
5808
  }
5362
5809
  idleWaiters = [];
@@ -5465,7 +5912,7 @@ var VoiceEngine = class _VoiceEngine {
5465
5912
  if (prose.trim()) this.repliedSinceDispatch = true;
5466
5913
  for (const w of this.words(this.reply)) this.echoWords.add(w);
5467
5914
  this.tts.speak(forSpeech(speech), true);
5468
- if (!this.spokeDeltas && this.turnStartAt) log9.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
5915
+ if (!this.spokeDeltas && this.turnStartAt) log11.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
5469
5916
  this.spokeDeltas = true;
5470
5917
  this.setState("speaking");
5471
5918
  if (this.options.revealMode !== "off" && display) {
@@ -5542,7 +5989,7 @@ var VoiceEngine = class _VoiceEngine {
5542
5989
  this.drainTimer = null;
5543
5990
  this.speaking = false;
5544
5991
  this.stopWordReveal(true);
5545
- if (this.turnStartAt) log9.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
5992
+ if (this.turnStartAt) log11.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
5546
5993
  this.echoUntil = this.clock.now() + 2500;
5547
5994
  if (!this.usingAec) this.stt.reset();
5548
5995
  this.setState("listening");
@@ -5630,7 +6077,7 @@ var VoiceEngine = class _VoiceEngine {
5630
6077
  this.clearAckTimer("interrupt");
5631
6078
  this.adaptiveAckFired = false;
5632
6079
  this.ackProbe = null;
5633
- if (this.uttQueue.length) log9.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
6080
+ if (this.uttQueue.length) log11.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
5634
6081
  const droppedQueued = this.uttQueue.length;
5635
6082
  this.uttQueue = [];
5636
6083
  if (!this.speaking && !this.drainTimer) return;
@@ -5776,7 +6223,7 @@ var VoiceEngine = class _VoiceEngine {
5776
6223
  if (flat && this.lastDispatchFlat && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS && (this.lastDispatchFlat.startsWith(flat) || flat.startsWith(this.lastDispatchFlat))) return;
5777
6224
  this.specSpent = true;
5778
6225
  this.specText = this.specPartial;
5779
- log9.debug(`speculate: "${this.specText.slice(0, 60)}"`);
6226
+ log11.debug(`speculate: "${this.specText.slice(0, 60)}"`);
5780
6227
  this.diag("speculate", { text: this.specText });
5781
6228
  this.options.onSpeculate(this.specText);
5782
6229
  }, this.options.speculativeMs);
@@ -5845,7 +6292,7 @@ var VoiceEngine = class _VoiceEngine {
5845
6292
  };
5846
6293
  this.tts.newContext();
5847
6294
  this.tts.speak(phrase, false);
5848
- log9.debug(`backchannel: "${phrase}"`);
6295
+ log11.debug(`backchannel: "${phrase}"`);
5849
6296
  this.diag("backchannel", { phrase });
5850
6297
  this.options.onBackchannel(phrase);
5851
6298
  }
@@ -5878,7 +6325,7 @@ var VoiceEngine = class _VoiceEngine {
5878
6325
  });
5879
6326
  if (idx < 0) return text;
5880
6327
  toks.splice(idx, 1);
5881
- log9.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
6328
+ log11.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
5882
6329
  return toks.join(" ");
5883
6330
  }
5884
6331
  /** Merge a resumed utterance into the pending one, deduping any word-overlap. Soniox re-finalizes
@@ -5969,7 +6416,7 @@ var VoiceEngine = class _VoiceEngine {
5969
6416
  this.pendingUtt = this.mergeUtterance(this.pendingUtt, text);
5970
6417
  if (this.pendingTimer) this.clock.clearTimeout(this.pendingTimer);
5971
6418
  if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
5972
- log9.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
6419
+ log11.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
5973
6420
  this.diag("hold", { reason: "incomplete", tail: this.pendingUtt.slice(-40) });
5974
6421
  this.options.onHold();
5975
6422
  if (this.options.holdFiller && !this.speaking) this.speakFiller(this.options.holdFiller);
@@ -6004,13 +6451,13 @@ var VoiceEngine = class _VoiceEngine {
6004
6451
  if (text) {
6005
6452
  const flat = text.toLowerCase().replace(/[^a-z0-9]/g, "");
6006
6453
  if (flat && flat === this.lastDispatchFlat && !this.repliedSinceDispatch && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS) {
6007
- log9.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
6454
+ log11.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
6008
6455
  this.diag("utterance_drop", { reason: "dup_final", text: text.slice(0, 60) });
6009
6456
  return;
6010
6457
  }
6011
6458
  const refinal = this.refinalizes(text);
6012
6459
  if (refinal && this.repliedSinceDispatch) {
6013
- log9.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
6460
+ log11.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
6014
6461
  this.diag("utterance_drop", { reason: "refinalized", text: text.slice(0, 60) });
6015
6462
  this.lastDispatchFlat = flat;
6016
6463
  this.lastDispatchWords = text.trim().split(/\s+/);
@@ -6170,7 +6617,7 @@ var SpokenSplitter = class {
6170
6617
  };
6171
6618
 
6172
6619
  // node_modules/@bod.ee/voice/src/adapters/soniox.ts
6173
- var log10 = forComponent2("SonioxSTT");
6620
+ var log12 = forComponent2("SonioxSTT");
6174
6621
  var now = () => performance.now();
6175
6622
  var SonioxSTTOptions = class {
6176
6623
  auth = "";
@@ -6211,7 +6658,7 @@ var SonioxSTT = class {
6211
6658
  this.onDiag({ t: now(), kind, ...fields });
6212
6659
  } catch (e) {
6213
6660
  this.diagOn = false;
6214
- log10.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6661
+ log12.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6215
6662
  }
6216
6663
  }
6217
6664
  lastChunkAt = 0;
@@ -6253,11 +6700,11 @@ var SonioxSTT = class {
6253
6700
  this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
6254
6701
  this.ws.onclose = (ev) => {
6255
6702
  if (this.stopped) return;
6256
- log10.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
6703
+ log12.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
6257
6704
  this.diag("stt_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: true });
6258
6705
  this.reset();
6259
6706
  this.connectWs().catch((e) => {
6260
- log10.error(`soniox reconnect failed: ${e.message}`);
6707
+ log12.error(`soniox reconnect failed: ${e.message}`);
6261
6708
  this.diag("stt_reconnect_failed", { message: e.message });
6262
6709
  });
6263
6710
  };
@@ -6269,7 +6716,7 @@ var SonioxSTT = class {
6269
6716
  this.endpointTimer = setInterval(() => {
6270
6717
  const combined = (this.finalText + this.partialText).trim();
6271
6718
  if (!combined || now() - this.lastChangeAt < this.options.silenceEndpointMs) return;
6272
- if (this.firstTokenAt) log10.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
6719
+ if (this.firstTokenAt) log12.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
6273
6720
  this.reset();
6274
6721
  this.onUtterance(combined, now());
6275
6722
  }, 120);
@@ -6281,7 +6728,7 @@ var SonioxSTT = class {
6281
6728
  if (this.stopped) return;
6282
6729
  const ref = this.lastChunkAt || this.startedChunksAt;
6283
6730
  if (now() - ref > noAudioMs) {
6284
- log10.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
6731
+ log12.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
6285
6732
  this.diag("stt_watchdog_fatal", { noAudioMs });
6286
6733
  this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
6287
6734
  this.stop();
@@ -6304,7 +6751,7 @@ var SonioxSTT = class {
6304
6751
  handle(m) {
6305
6752
  if (m.error_message) {
6306
6753
  this.diag("stt_error", { message: String(m.error_message), code: m.error_code });
6307
- return log10.error(`soniox: ${m.error_message}`);
6754
+ return log12.error(`soniox: ${m.error_message}`);
6308
6755
  }
6309
6756
  let endpoint = false;
6310
6757
  for (const t of m.tokens ?? []) {
@@ -6321,7 +6768,7 @@ var SonioxSTT = class {
6321
6768
  this.onPartial(combined);
6322
6769
  if (endpoint && this.finalText.trim()) {
6323
6770
  const utterance = this.finalText.trim();
6324
- if (this.firstTokenAt) log10.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
6771
+ if (this.firstTokenAt) log12.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
6325
6772
  this.reset();
6326
6773
  this.onUtterance(utterance, now());
6327
6774
  }
@@ -6343,7 +6790,7 @@ var SonioxSTT = class {
6343
6790
  };
6344
6791
 
6345
6792
  // node_modules/@bod.ee/voice/src/adapters/cartesia.ts
6346
- var log11 = forComponent2("CartesiaTTS");
6793
+ var log13 = forComponent2("CartesiaTTS");
6347
6794
  var now2 = () => performance.now();
6348
6795
  var CartesiaTTSOptions = class {
6349
6796
  auth = "";
@@ -6379,7 +6826,7 @@ var CartesiaTTS = class _CartesiaTTS {
6379
6826
  this.onDiag({ t: now2(), kind, ...fields });
6380
6827
  } catch (e) {
6381
6828
  this.diagOn = false;
6382
- log11.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6829
+ log13.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6383
6830
  }
6384
6831
  }
6385
6832
  firstAudioAt = 0;
@@ -6414,11 +6861,11 @@ var CartesiaTTS = class _CartesiaTTS {
6414
6861
  this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
6415
6862
  });
6416
6863
  this.ws.onclose = (ev) => {
6417
- log11.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
6864
+ log13.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
6418
6865
  this.diag("tts_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: !this.closed });
6419
6866
  if (!this.closed) {
6420
6867
  this.connecting = this.doConnect().catch((e) => {
6421
- log11.error(`cartesia reconnect failed: ${e.message}`);
6868
+ log13.error(`cartesia reconnect failed: ${e.message}`);
6422
6869
  this.diag("tts_reconnect_failed", { message: e.message });
6423
6870
  });
6424
6871
  }
@@ -6446,12 +6893,12 @@ var CartesiaTTS = class _CartesiaTTS {
6446
6893
  this.down = true;
6447
6894
  this.downAt = now2();
6448
6895
  this.consecutiveOk = 0;
6449
- log11.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
6896
+ log13.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
6450
6897
  this.diag("tts_breaker_open", { errors: this.consecutiveErrors });
6451
6898
  this.onDone();
6452
6899
  this.startProbe();
6453
6900
  } else if (!this.down) {
6454
- (/No valid transcripts/i.test(m.message || "") ? log11.debug : log11.warn)(`cartesia: ${JSON.stringify(m)}`);
6901
+ (/No valid transcripts/i.test(m.message || "") ? log13.debug : log13.warn)(`cartesia: ${JSON.stringify(m)}`);
6455
6902
  }
6456
6903
  }
6457
6904
  };
@@ -6466,7 +6913,7 @@ var CartesiaTTS = class _CartesiaTTS {
6466
6913
  this.stopProbe();
6467
6914
  const downMs = this.downAt ? now2() - this.downAt : 0;
6468
6915
  this.diag("tts_breaker_close", { downMs: Math.round(downMs) });
6469
- (downMs < 2e3 ? log11.debug : log11.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
6916
+ (downMs < 2e3 ? log13.debug : log13.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
6470
6917
  }
6471
6918
  /** Ensure the WS is open before sending — reconnects if idle-closed. */
6472
6919
  async ensureConnected() {
@@ -6557,7 +7004,7 @@ configureLogging((name) => {
6557
7004
  });
6558
7005
 
6559
7006
  // src/duplex.ts
6560
- var log12 = forComponent("DuplexAgent");
7007
+ var log14 = forComponent("DuplexAgent");
6561
7008
  function describeCall(call) {
6562
7009
  const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
6563
7010
  const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
@@ -6768,7 +7215,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
6768
7215
  const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
6769
7216
  if (m) {
6770
7217
  this.fabricationCut = true;
6771
- log12.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
7218
+ log14.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
6772
7219
  const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
6773
7220
  if (!safe) return;
6774
7221
  if (safe.trim()) this.spokeThisTurn = true;
@@ -6861,7 +7308,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
6861
7308
  return {
6862
7309
  preToolUse: (call) => {
6863
7310
  if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
6864
- log12.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
7311
+ log14.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
6865
7312
  this.abortSpeculation();
6866
7313
  return { block: true, reason: "Speculative turn aborted." };
6867
7314
  }
@@ -6899,7 +7346,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
6899
7346
  try {
6900
7347
  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.");
6901
7348
  } catch (e) {
6902
- log12.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
7349
+ log14.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
6903
7350
  } finally {
6904
7351
  this.nudging = false;
6905
7352
  }
@@ -6992,7 +7439,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
6992
7439
  try {
6993
7440
  res = await this.voice.send(spec.text);
6994
7441
  } catch (e) {
6995
- log12.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
7442
+ log14.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
6996
7443
  } finally {
6997
7444
  this.voice.options.signal = prevSignal;
6998
7445
  }
@@ -7006,7 +7453,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7006
7453
  if (d === "abort") {
7007
7454
  if (this.voice.transcript.length > base) this.voice.transcript.length = base;
7008
7455
  this.speculativeAbortedCalls++;
7009
- log12.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
7456
+ log14.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
7010
7457
  if (this.spec === spec) this.spec = void 0;
7011
7458
  return res ?? empty;
7012
7459
  }
@@ -7271,7 +7718,7 @@ Another agent just implemented the above. Independently check the CURRENT state
7271
7718
  this.notify("task_verify", `task ${id}: verifying`, { id });
7272
7719
  const cres = await new Agent(checkerOpts).run(checkBrief);
7273
7720
  if (cres.finishReason !== "stop") {
7274
- log12.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
7721
+ log14.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
7275
7722
  this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
7276
7723
  }
7277
7724
  const sum = (a = 0, b = 0) => a + b;
@@ -7337,14 +7784,14 @@ Another agent just implemented the above. Independently check the CURRENT state
7337
7784
  * '[task <id> asks] …' and resolves with the user's answer via AnswerTask — or '' on timeout/
7338
7785
  * task settle (callers map '' to deny / best-judgment). Workers never block forever. */
7339
7786
  parkQuestion(askId, question) {
7340
- return new Promise((resolve4) => {
7787
+ return new Promise((resolve5) => {
7341
7788
  let settled = false;
7342
7789
  const finish = (answer) => {
7343
7790
  if (settled) return;
7344
7791
  settled = true;
7345
7792
  clearTimeout(timer);
7346
7793
  this.pendingAsks.delete(askId);
7347
- resolve4(answer);
7794
+ resolve5(answer);
7348
7795
  };
7349
7796
  const timer = setTimeout(() => {
7350
7797
  this.notify("task_ask_timeout", `task ${askId}: question timed out \u2014 proceeding without an answer`);
@@ -7406,8 +7853,8 @@ Another agent just implemented the above. Independently check the CURRENT state
7406
7853
  }
7407
7854
  rec.status = "done";
7408
7855
  rec.result = res.text;
7409
- const incomplete = res.finishReason !== "stop";
7410
- log12.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
7856
+ const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
7857
+ log14.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
7411
7858
  this.notify("task_done", `task ${id} (${rec.label}) completed`, {
7412
7859
  id,
7413
7860
  text: res.text,
@@ -7449,14 +7896,14 @@ Another agent just implemented the above. Independently check the CURRENT state
7449
7896
  foldIfSuperseded(rec, note) {
7450
7897
  if (!(rec.deliveryParked && this.suppressParkedResume)) return false;
7451
7898
  if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
7452
- log12.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
7899
+ log14.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
7453
7900
  return true;
7454
7901
  }
7455
7902
  failTask(rec, msg) {
7456
7903
  this.dropAsk(rec.id);
7457
7904
  rec.status = "error";
7458
7905
  rec.result = msg;
7459
- log12.warn(`task ${rec.id} failed: ${msg}`);
7906
+ log14.warn(`task ${rec.id} failed: ${msg}`);
7460
7907
  this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
7461
7908
  if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
7462
7909
  this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
@@ -7761,11 +8208,11 @@ function buildMcpCatalog(servers) {
7761
8208
  }
7762
8209
  return { specs, routes };
7763
8210
  }
7764
- function searchOverCatalog(servers, specs, routes, resolve4, options) {
8211
+ function searchOverCatalog(servers, specs, routes, resolve5, options) {
7765
8212
  const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
7766
8213
  const r = routes.get(name);
7767
8214
  if (!r) throw new Error(`unknown MCP tool '${name}' \u2014 use ToolSearch to find valid names`);
7768
- return resolve4(r.server, r.rawName, args ?? {});
8215
+ return resolve5(r.server, r.rawName, args ?? {});
7769
8216
  }, options) : [];
7770
8217
  return { tools, serverNames: servers, toolCount: specs.length };
7771
8218
  }
@@ -7777,6 +8224,7 @@ function makeMcpToolSearchFromMounted(mounted, options) {
7777
8224
 
7778
8225
  // src/index.ts
7779
8226
  init_logging();
8227
+ init_llm();
7780
8228
  init_worktree();
7781
8229
  import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor as CommandExecutor2, registerHeadlessCommands as registerHeadlessCommands2 } from "@livx.cc/wcli/core";
7782
8230
 
@@ -7784,7 +8232,7 @@ import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor a
7784
8232
  init_logging();
7785
8233
  import { spawn } from "child_process";
7786
8234
  import { createHash } from "crypto";
7787
- var log13 = forComponent("mcp");
8235
+ var log15 = forComponent("mcp");
7788
8236
  var PROTOCOL_VERSION = "2025-06-18";
7789
8237
  var DEFAULT_TIMEOUT_MS = 3e4;
7790
8238
  var StdioTransport = class {
@@ -7803,7 +8251,7 @@ var StdioTransport = class {
7803
8251
  proc.stdout.setEncoding("utf8");
7804
8252
  proc.stdout.on("data", (chunk) => this.onData(chunk));
7805
8253
  proc.stderr.setEncoding("utf8");
7806
- proc.stderr.on("data", (chunk) => log13.debug(`[${command}] stderr:`, chunk.trimEnd()));
8254
+ proc.stderr.on("data", (chunk) => log15.debug(`[${command}] stderr:`, chunk.trimEnd()));
7807
8255
  proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
7808
8256
  proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
7809
8257
  }
@@ -7817,7 +8265,7 @@ var StdioTransport = class {
7817
8265
  try {
7818
8266
  this.dispatch(JSON.parse(line));
7819
8267
  } catch (e) {
7820
- log13.debug("dropping non-JSON line from MCP server:", line, e);
8268
+ log15.debug("dropping non-JSON line from MCP server:", line, e);
7821
8269
  }
7822
8270
  }
7823
8271
  }
@@ -7843,12 +8291,12 @@ var StdioTransport = class {
7843
8291
  request(method, params) {
7844
8292
  const id = this.nextId++;
7845
8293
  const timeoutMs = this.spec.timeoutMs ?? DEFAULT_TIMEOUT_MS;
7846
- return new Promise((resolve4, reject) => {
8294
+ return new Promise((resolve5, reject) => {
7847
8295
  const timer = setTimeout(() => {
7848
8296
  this.pending.delete(id);
7849
8297
  reject(new Error(`MCP request "${method}" timed out after ${timeoutMs}ms`));
7850
8298
  }, timeoutMs);
7851
- this.pending.set(id, { resolve: resolve4, reject, timer });
8299
+ this.pending.set(id, { resolve: resolve5, reject, timer });
7852
8300
  try {
7853
8301
  this.write({ jsonrpc: "2.0", id, method, params });
7854
8302
  } catch (e) {
@@ -7866,7 +8314,7 @@ var StdioTransport = class {
7866
8314
  try {
7867
8315
  this.proc?.stdin?.end();
7868
8316
  } catch (e) {
7869
- log13.debug("stdin end failed", e);
8317
+ log15.debug("stdin end failed", e);
7870
8318
  }
7871
8319
  this.proc?.kill();
7872
8320
  }
@@ -7935,7 +8383,7 @@ function parseSseResponse(body) {
7935
8383
  const obj = JSON.parse(trimmed.slice(5).trim());
7936
8384
  if (obj && (obj.result !== void 0 || obj.error !== void 0)) return obj;
7937
8385
  } catch (e) {
7938
- log13.debug("skipping unparseable SSE data line", e);
8386
+ log15.debug("skipping unparseable SSE data line", e);
7939
8387
  }
7940
8388
  }
7941
8389
  return {};
@@ -7981,12 +8429,12 @@ function buildTransport(cfg) {
7981
8429
  }
7982
8430
  function withTimeout(p, ms, label) {
7983
8431
  if (!ms || ms <= 0) return p;
7984
- return new Promise((resolve4, reject) => {
8432
+ return new Promise((resolve5, reject) => {
7985
8433
  const timer = setTimeout(() => reject(new Error(`MCP "${label}" mount exceeded ${ms}ms`)), ms);
7986
8434
  timer.unref?.();
7987
8435
  p.then((v) => {
7988
8436
  clearTimeout(timer);
7989
- resolve4(v);
8437
+ resolve5(v);
7990
8438
  }, (e) => {
7991
8439
  clearTimeout(timer);
7992
8440
  reject(e);
@@ -8003,7 +8451,7 @@ async function mountWithDeadline(name, cfg, mountTimeoutMs) {
8003
8451
  return { name, client, tools, specs, serverInfo: init?.serverInfo, config: cfg };
8004
8452
  })(), mountTimeoutMs, name);
8005
8453
  } catch (e) {
8006
- await client.close().catch((err2) => log13.debug(`close after failed mount of "${name}": ${err2}`));
8454
+ await client.close().catch((err2) => log15.debug(`close after failed mount of "${name}": ${err2}`));
8007
8455
  throw e;
8008
8456
  }
8009
8457
  }
@@ -8014,15 +8462,15 @@ function validEntries(servers) {
8014
8462
  return Object.entries(servers).filter(([name, cfg]) => {
8015
8463
  if (!cfg || cfg.disabled) return false;
8016
8464
  if (!cfg.command && !cfg.url) {
8017
- log13.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
8465
+ log15.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
8018
8466
  return false;
8019
8467
  }
8020
8468
  return true;
8021
8469
  });
8022
8470
  }
8023
8471
  function logMountFailure(name, e) {
8024
- if (e instanceof McpAuthError) log13.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
8025
- else log13.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
8472
+ if (e instanceof McpAuthError) log15.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
8473
+ else log15.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
8026
8474
  }
8027
8475
  async function mountMcpServers(servers = {}, opts = {}) {
8028
8476
  const entries = validEntries(servers);
@@ -8032,7 +8480,7 @@ async function mountMcpServers(servers = {}, opts = {}) {
8032
8480
  const name = entries[i][0];
8033
8481
  if (r.status === "fulfilled") {
8034
8482
  out.push(r.value);
8035
- log13.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
8483
+ log15.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
8036
8484
  } else logMountFailure(name, r.reason);
8037
8485
  });
8038
8486
  return out;
@@ -8072,7 +8520,7 @@ var McpPool = class {
8072
8520
  const prev = this.warm.get(key);
8073
8521
  if (prev) {
8074
8522
  clearTimeout(prev.timer);
8075
- if (prev.client !== client) void prev.client.close().catch((err2) => log13.debug(`warm-pool replace close failed: ${err2}`));
8523
+ if (prev.client !== client) void prev.client.close().catch((err2) => log15.debug(`warm-pool replace close failed: ${err2}`));
8076
8524
  }
8077
8525
  const e = { client, timer: void 0 };
8078
8526
  this.warm.set(key, e);
@@ -8089,7 +8537,7 @@ var McpPool = class {
8089
8537
  const e = this.warm.get(key);
8090
8538
  if (!e) return;
8091
8539
  this.warm.delete(key);
8092
- await e.client.close().catch((err2) => log13.debug(`warm-pool evict close failed: ${err2}`));
8540
+ await e.client.close().catch((err2) => log15.debug(`warm-pool evict close failed: ${err2}`));
8093
8541
  }
8094
8542
  async closeAll() {
8095
8543
  for (const e of this.warm.values()) {
@@ -8105,8 +8553,8 @@ var defaultPool = new McpPool();
8105
8553
 
8106
8554
  // cli/mcpOAuth.ts
8107
8555
  import { createServer } from "http";
8108
- import { randomBytes, createHash as createHash2 } from "crypto";
8109
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync } from "fs";
8556
+ import { randomBytes as randomBytes2, createHash as createHash2 } from "crypto";
8557
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync } from "fs";
8110
8558
  import { dirname as dirname2 } from "path";
8111
8559
  var McpOAuthOptions = class {
8112
8560
  /** Where to persist tokens. Mode 0600. */
@@ -8134,7 +8582,7 @@ var McpOAuth = class {
8134
8582
  // -- store I/O ----------------------------------------------------------------
8135
8583
  load() {
8136
8584
  try {
8137
- return existsSync(this.options.storePath) ? JSON.parse(readFileSync2(this.options.storePath, "utf8")) : {};
8585
+ return existsSync(this.options.storePath) ? JSON.parse(readFileSync3(this.options.storePath, "utf8")) : {};
8138
8586
  } catch {
8139
8587
  return {};
8140
8588
  }
@@ -8148,9 +8596,9 @@ var McpOAuth = class {
8148
8596
  async register(serverUrl) {
8149
8597
  const meta = await this.discover(serverUrl);
8150
8598
  const clientId = this.options.clientId ?? await this.registerClient(meta, serverUrl);
8151
- const verifier = b64url(randomBytes(32));
8599
+ const verifier = b64url(randomBytes2(32));
8152
8600
  const challenge = b64url(createHash2("sha256").update(verifier).digest());
8153
- const state = b64url(randomBytes(16));
8601
+ const state = b64url(randomBytes2(16));
8154
8602
  const { code, redirectUri } = await this.captureCode(meta.authorization_endpoint, clientId, challenge, state);
8155
8603
  const tok = await this.exchange(meta.token_endpoint, {
8156
8604
  grant_type: "authorization_code",
@@ -8232,7 +8680,7 @@ var McpOAuth = class {
8232
8680
  }
8233
8681
  /** Start a one-route loopback server, open the browser, and resolve with the returned auth code. */
8234
8682
  captureCode(authEndpoint, clientId, challenge, state) {
8235
- return new Promise((resolve4, reject) => {
8683
+ return new Promise((resolve5, reject) => {
8236
8684
  const server = createServer((req, res) => {
8237
8685
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
8238
8686
  if (url.pathname !== "/callback") {
@@ -8246,7 +8694,7 @@ var McpOAuth = class {
8246
8694
  clearTimeout(timer);
8247
8695
  if (gotState !== state) return reject(new Error("OAuth state mismatch (possible CSRF) \u2014 aborting"));
8248
8696
  if (!code) return reject(new Error(`authorization failed: ${url.searchParams.get("error") ?? "no code"}`));
8249
- resolve4({ code, redirectUri });
8697
+ resolve5({ code, redirectUri });
8250
8698
  });
8251
8699
  let redirectUri = "";
8252
8700
  const timer = setTimeout(() => {
@@ -8299,7 +8747,7 @@ function defaultOpenBrowser(url) {
8299
8747
  // cli/core.ts
8300
8748
  import { randomUUID } from "crypto";
8301
8749
  import { execFile as execFile2 } from "child_process";
8302
- import { resolve, basename, join as join5 } from "path";
8750
+ import { resolve as resolve2, basename, join as join5 } from "path";
8303
8751
  import { existsSync as existsSync3, mkdirSync as mkdirSync4 } from "fs";
8304
8752
  import { platform, arch, release, userInfo, homedir as homedir2, tmpdir } from "os";
8305
8753
  init_tools_shell();
@@ -8307,7 +8755,7 @@ init_tools_shell();
8307
8755
  // src/tools.notify.ts
8308
8756
  init_logging();
8309
8757
  import { execFile } from "child_process";
8310
- var log15 = forComponent("notify");
8758
+ var log17 = forComponent("notify");
8311
8759
  function makeNotifyTool(opts = {}) {
8312
8760
  const platform2 = opts.platform ?? process.platform;
8313
8761
  const run = opts.exec ?? execFile;
@@ -8328,12 +8776,12 @@ function makeNotifyTool(opts = {}) {
8328
8776
  if (!msg) return "Error: empty message";
8329
8777
  const argv = platform2 === "darwin" ? ["osascript", ["-e", `display notification ${JSON.stringify(msg)} with title ${JSON.stringify(head)}`]] : platform2 === "linux" ? ["notify-send", [head, msg]] : null;
8330
8778
  if (!argv) return `Notifications unavailable on ${platform2}.`;
8331
- return new Promise((resolve4) => {
8779
+ return new Promise((resolve5) => {
8332
8780
  run(argv[0], argv[1], { timeout: 5e3 }, (e) => {
8333
8781
  if (e) {
8334
- log15.debug("notification failed", e);
8335
- resolve4(`Notification failed: ${e.message}`);
8336
- } else resolve4("Notification shown.");
8782
+ log17.debug("notification failed", e);
8783
+ resolve5(`Notification failed: ${e.message}`);
8784
+ } else resolve5("Notification shown.");
8337
8785
  });
8338
8786
  });
8339
8787
  }
@@ -8345,9 +8793,9 @@ import { BodDB as BodDB2 } from "@bod.ee/db";
8345
8793
 
8346
8794
  // cli/util.ts
8347
8795
  init_logging();
8348
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
8796
+ import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
8349
8797
  import { homedir } from "os";
8350
- var log16 = forComponent("cli-util");
8798
+ var log18 = forComponent("cli-util");
8351
8799
  function dotDirs(base, sub, opts = {}) {
8352
8800
  const home = opts.home ?? homedir();
8353
8801
  const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
@@ -8364,7 +8812,7 @@ function parseJson(text, fallback, what = "json") {
8364
8812
  try {
8365
8813
  return JSON.parse(text);
8366
8814
  } catch (e) {
8367
- log16.debug(`parseJson(${what}) failed: ${e.message}`);
8815
+ log18.debug(`parseJson(${what}) failed: ${e.message}`);
8368
8816
  return fallback;
8369
8817
  }
8370
8818
  }
@@ -8372,9 +8820,9 @@ function readJsonFile(path, fallback) {
8372
8820
  if (!existsSync2(path)) return fallback;
8373
8821
  let text;
8374
8822
  try {
8375
- text = readFileSync3(path, "utf8");
8823
+ text = readFileSync4(path, "utf8");
8376
8824
  } catch (e) {
8377
- log16.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
8825
+ log18.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
8378
8826
  return fallback;
8379
8827
  }
8380
8828
  return parseJson(text, fallback, path);
@@ -8466,7 +8914,7 @@ async function buildAgent(o) {
8466
8914
  throw new Error(
8467
8915
  "buildAgent: explicit `sandbox` required \u2014 false = disk (the REAL filesystem, full reach like Claude Code; secrets still hidden) or true = sandbox (an in-memory copy of cwd; the real disk is never modified). This forces a conscious sandboxing decision."
8468
8916
  );
8469
- const cwd = resolve(o.cwd ?? process.cwd());
8917
+ const cwd = resolve2(o.cwd ?? process.cwd());
8470
8918
  const disk = new NodeDiskFilesystem("/", { denySymlinks: false });
8471
8919
  await disk.init();
8472
8920
  const jailedDisk = new JailedFilesystem(disk);
@@ -8487,7 +8935,7 @@ async function buildAgent(o) {
8487
8935
  mem.setCwd(cwd);
8488
8936
  fs = mem;
8489
8937
  } else if (o.boddb) {
8490
- const root = resolve(o.boddb);
8938
+ const root = resolve2(o.boddb);
8491
8939
  mkdirSync4(root, { recursive: true });
8492
8940
  const db = new BodDB2({ path: join5(root, "meta.db"), sweepInterval: 0, vfs: { storageRoot: join5(root, "files") } });
8493
8941
  const bod = new BodDbFilesystem(db);
@@ -8503,7 +8951,7 @@ async function buildAgent(o) {
8503
8951
  const mounts = [];
8504
8952
  const notes = [];
8505
8953
  for (const d of o.addDirs) {
8506
- const abs = resolve(d);
8954
+ const abs = resolve2(d);
8507
8955
  let name = basename(abs) || "dir", prefix = `/${name}`, n = 2;
8508
8956
  while (used.has(prefix)) prefix = `/${name}-${n++}`;
8509
8957
  used.add(prefix);
@@ -8638,6 +9086,7 @@ The filesystem root '/' is the real machine root \u2014 you have full filesystem
8638
9086
  // Context-v2: by default, note-take over a long session (stub tool outputs older than the last 8).
8639
9087
  keepToolOutputs: o.keepToolOutputs ?? 8,
8640
9088
  ...o.maxContextTokens != null ? { maxContextTokens: o.maxContextTokens } : {},
9089
+ ...o.contextWindow ? { contextWindow: o.contextWindow } : {},
8641
9090
  ...o.browserTrim ? { browserTrim: o.browserTrim } : {}
8642
9091
  });
8643
9092
  }
@@ -8658,11 +9107,11 @@ var trunc = (s, n) => (s == null ? "" : String(s).length > n ? String(s).slice(0
8658
9107
  // cli/voice.ts
8659
9108
  init_logging();
8660
9109
  import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
8661
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
9110
+ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
8662
9111
  import { homedir as homedir3 } from "os";
8663
9112
  import { dirname as dirname3, join as join6 } from "path";
8664
9113
  import { fileURLToPath } from "url";
8665
- var log17 = forComponent("VoiceIO");
9114
+ var log19 = forComponent("VoiceIO");
8666
9115
  var now3 = () => performance.now();
8667
9116
  var Player = class {
8668
9117
  proc = null;
@@ -8676,7 +9125,7 @@ var Player = class {
8676
9125
  ["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
8677
9126
  { stdio: ["pipe", "ignore", "ignore"] }
8678
9127
  );
8679
- this.proc.on("error", (e) => log17.warn(`ffplay error: ${e.message}`));
9128
+ this.proc.on("error", (e) => log19.warn(`ffplay error: ${e.message}`));
8680
9129
  this.proc.stdin.on("error", () => {
8681
9130
  });
8682
9131
  this.bytesWritten = 0;
@@ -8714,7 +9163,7 @@ function detectFfmpegMic() {
8714
9163
  const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
8715
9164
  const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
8716
9165
  if (!mic) throw new Error("no audio input device found");
8717
- log17.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
9166
+ log19.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
8718
9167
  return `:${mic.idx}`;
8719
9168
  }
8720
9169
  function detectedInputDevice() {
@@ -8747,18 +9196,18 @@ function resolveAecBinary() {
8747
9196
  if (!existsSync4(src)) return null;
8748
9197
  const cacheDir = join6(homedir3(), ".agent", "cache");
8749
9198
  const bin = join6(cacheDir, "mic-aec");
8750
- if (existsSync4(bin) && statSync3(bin).mtimeMs >= statSync3(src).mtimeMs) return bin;
9199
+ if (existsSync4(bin) && statSync4(bin).mtimeMs >= statSync4(src).mtimeMs) return bin;
8751
9200
  if (spawnSync2("which", ["swiftc"]).status !== 0) return null;
8752
9201
  mkdirSync5(cacheDir, { recursive: true });
8753
- log17.info("compiling AEC mic helper (first run)\u2026");
9202
+ log19.info("compiling AEC mic helper (first run)\u2026");
8754
9203
  const build = spawnSync2("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
8755
9204
  if (build.status !== 0) {
8756
- log17.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
9205
+ log19.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
8757
9206
  return null;
8758
9207
  }
8759
9208
  const sign = spawnSync2("codesign", ["-fs", "-", bin], { encoding: "utf8" });
8760
9209
  if (sign.status !== 0) {
8761
- log17.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
9210
+ log19.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
8762
9211
  return null;
8763
9212
  }
8764
9213
  return bin;
@@ -8800,16 +9249,16 @@ var NodeMicSource = class {
8800
9249
  this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
8801
9250
  } else {
8802
9251
  if (spawnSync2("which", ["ffmpeg"]).status !== 0) throw new Error("voice I/O unavailable: no AEC helper and no ffmpeg on PATH");
8803
- log17.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
9252
+ log19.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
8804
9253
  this.proc = spawn2(
8805
9254
  "ffmpeg",
8806
9255
  ["-loglevel", "error", "-f", "avfoundation", "-i", detectFfmpegMic(), "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
8807
9256
  { stdio: ["ignore", "pipe", "pipe"] }
8808
9257
  );
8809
- this.proc.stderr.on("data", (d) => log17.warn(`ffmpeg: ${String(d).trim()}`));
9258
+ this.proc.stderr.on("data", (d) => log19.warn(`ffmpeg: ${String(d).trim()}`));
8810
9259
  }
8811
9260
  this.proc.on("exit", (c) => {
8812
- if (c && !this.stopped) log17.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
9261
+ if (c && !this.stopped) log19.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
8813
9262
  });
8814
9263
  this.proc.stdout.on("data", (chunk) => onChunk(chunk));
8815
9264
  }
@@ -8875,7 +9324,7 @@ var AecDuplexAudio = class {
8875
9324
  this.proc.stdin.on("error", () => {
8876
9325
  });
8877
9326
  this.proc.on("exit", (c) => {
8878
- if (c && !this.stopped) log17.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
9327
+ if (c && !this.stopped) log19.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
8879
9328
  });
8880
9329
  this.proc.stdout.on("data", (chunk) => {
8881
9330
  this.gotChunk = true;
@@ -8891,7 +9340,7 @@ var AecDuplexAudio = class {
8891
9340
  openMicSettings();
8892
9341
  this.onFatal?.("microphone permission denied \u2014 enable it in System Settings \u2192 Privacy & Security \u2192 Microphone for your terminal, then restart it");
8893
9342
  }
8894
- } else log17.debug(`mic-aec: ${s}`);
9343
+ } else log19.debug(`mic-aec: ${s}`);
8895
9344
  }
8896
9345
  });
8897
9346
  if (!this.noVpio && !this.triedFallback) {
@@ -8900,7 +9349,7 @@ var AecDuplexAudio = class {
8900
9349
  this.triedFallback = true;
8901
9350
  this.noVpio = true;
8902
9351
  this._aec = false;
8903
- log17.warn("mic-aec: VPIO delivered no audio in 2.5s \u2014 falling back to non-VPIO capture (no AEC \u2192 half-duplex, no barge-in)");
9352
+ log19.warn("mic-aec: VPIO delivered no audio in 2.5s \u2014 falling back to non-VPIO capture (no AEC \u2192 half-duplex, no barge-in)");
8904
9353
  this.onDegrade?.();
8905
9354
  this.killProc();
8906
9355
  this.spawnHelper();
@@ -9077,7 +9526,7 @@ function fakeVoiceParts(uttFile) {
9077
9526
  timer = setInterval(() => {
9078
9527
  let buf;
9079
9528
  try {
9080
- buf = readFileSync4(uttFile, "utf8");
9529
+ buf = readFileSync5(uttFile, "utf8");
9081
9530
  } catch {
9082
9531
  return;
9083
9532
  }
@@ -9120,7 +9569,7 @@ function fakeVoiceParts(uttFile) {
9120
9569
  init_logging();
9121
9570
  import { appendFile, mkdirSync as mkdirSync6 } from "fs";
9122
9571
  import { dirname as dirname4 } from "path";
9123
- var log18 = forComponent("VoiceDiag");
9572
+ var log20 = forComponent("VoiceDiag");
9124
9573
  var JsonlDiagSink = class {
9125
9574
  constructor(path) {
9126
9575
  this.path = path;
@@ -9161,7 +9610,7 @@ var JsonlDiagSink = class {
9161
9610
  disable(e) {
9162
9611
  this.disabled = true;
9163
9612
  this.queue.length = 0;
9164
- log18.debug(`voice diag sink disabled (${this.path}): ${e instanceof Error ? e.message : e}`);
9613
+ log20.debug(`voice diag sink disabled (${this.path}): ${e instanceof Error ? e.message : e}`);
9165
9614
  }
9166
9615
  /** True once a failure permanently disabled the sink (tests/inspection). */
9167
9616
  get isDisabled() {
@@ -9175,7 +9624,7 @@ var JsonlDiagSink = class {
9175
9624
 
9176
9625
  // cli/config.ts
9177
9626
  import { homedir as homedir4 } from "os";
9178
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
9627
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
9179
9628
  import { join as join7 } from "path";
9180
9629
  import { pathToFileURL } from "url";
9181
9630
  var FILES = ["config.ts", "config.js", "config.mjs", "config.json"];
@@ -9197,7 +9646,7 @@ function loadSettings(dir) {
9197
9646
  const p = join7(dir, ".agent", "settings.json");
9198
9647
  if (!existsSync5(p)) return {};
9199
9648
  try {
9200
- const raw = JSON.parse(readFileSync5(p, "utf8"));
9649
+ const raw = JSON.parse(readFileSync6(p, "utf8"));
9201
9650
  const cfg = {};
9202
9651
  if (raw.mcpServers && typeof raw.mcpServers === "object") cfg.mcpServers = raw.mcpServers;
9203
9652
  if (raw.permissions && typeof raw.permissions === "object") cfg.permissions = raw.permissions;
@@ -9231,7 +9680,7 @@ async function loadConfig(cwd) {
9231
9680
 
9232
9681
  // cli/hooks-config.ts
9233
9682
  import { spawnSync as spawnSync3 } from "child_process";
9234
- var log19 = forComponent("hooks");
9683
+ var log21 = forComponent("hooks");
9235
9684
  var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
9236
9685
  function ruleMatches(rule, toolName) {
9237
9686
  if (!rule.tool || rule.tool === "*") return true;
@@ -9248,7 +9697,7 @@ function runCmd(rule, env) {
9248
9697
  });
9249
9698
  return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
9250
9699
  } catch (e) {
9251
- log19.debug(`hook command failed: ${rule.command}`, e);
9700
+ log21.debug(`hook command failed: ${rule.command}`, e);
9252
9701
  return { code: 1, out: String(e?.message ?? e) };
9253
9702
  }
9254
9703
  }
@@ -9352,10 +9801,10 @@ function formatDiff(ops, opts = {}) {
9352
9801
  }
9353
9802
 
9354
9803
  // cli/session.ts
9355
- import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync4, readdirSync, renameSync, symlinkSync as symlinkSync2, unlinkSync, readlinkSync } from "fs";
9804
+ import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4, readdirSync, renameSync, symlinkSync as symlinkSync2, unlinkSync, readlinkSync } from "fs";
9356
9805
  import { homedir as homedir5 } from "os";
9357
9806
  import { join as join8 } from "path";
9358
- var log20 = forComponent("session");
9807
+ var log22 = forComponent("session");
9359
9808
  var globalDir = () => join8(homedir5(), ".agent", "sessions");
9360
9809
  var SessionStore = class {
9361
9810
  dir;
@@ -9400,15 +9849,15 @@ var SessionStore = class {
9400
9849
  }
9401
9850
  load(id) {
9402
9851
  if (!this.safeId(id)) {
9403
- log20.debug(`rejecting unsafe session id: ${id}`);
9852
+ log22.debug(`rejecting unsafe session id: ${id}`);
9404
9853
  return void 0;
9405
9854
  }
9406
9855
  const path = join8(this.dir, `${id}.json`);
9407
9856
  if (!existsSync6(path)) return void 0;
9408
9857
  try {
9409
- return JSON.parse(readFileSync6(path, "utf8"));
9858
+ return JSON.parse(readFileSync7(path, "utf8"));
9410
9859
  } catch (e) {
9411
- log20.debug(`unreadable session ${id} \u2014 ignoring`, e);
9860
+ log22.debug(`unreadable session ${id} \u2014 ignoring`, e);
9412
9861
  return void 0;
9413
9862
  }
9414
9863
  }
@@ -9419,9 +9868,9 @@ var SessionStore = class {
9419
9868
  for (const f of readdirSync(this.dir)) {
9420
9869
  if (!f.endsWith(".json")) continue;
9421
9870
  try {
9422
- metas.push(JSON.parse(readFileSync6(join8(this.dir, f), "utf8")).meta);
9871
+ metas.push(JSON.parse(readFileSync7(join8(this.dir, f), "utf8")).meta);
9423
9872
  } catch (e) {
9424
- log20.debug(`skipping unreadable session file ${f}`, e);
9873
+ log22.debug(`skipping unreadable session file ${f}`, e);
9425
9874
  }
9426
9875
  }
9427
9876
  return metas.sort((a, b) => b.updated - a.updated);
@@ -9442,7 +9891,7 @@ function globalSessionLoad(idOrPrefix) {
9442
9891
  if (existsSync6(exact)) {
9443
9892
  try {
9444
9893
  const target = readlinkSync(exact);
9445
- return JSON.parse(readFileSync6(target, "utf8"));
9894
+ return JSON.parse(readFileSync7(target, "utf8"));
9446
9895
  } catch {
9447
9896
  return void 0;
9448
9897
  }
@@ -9453,7 +9902,7 @@ function globalSessionLoad(idOrPrefix) {
9453
9902
  const base = f.slice(0, -5);
9454
9903
  if (base.includes(idOrPrefix) || base.endsWith(idOrPrefix)) {
9455
9904
  const target = readlinkSync(join8(gd, f));
9456
- return JSON.parse(readFileSync6(target, "utf8"));
9905
+ return JSON.parse(readFileSync7(target, "utf8"));
9457
9906
  }
9458
9907
  }
9459
9908
  } catch {
@@ -9475,7 +9924,7 @@ function globalSessionList() {
9475
9924
  }
9476
9925
  continue;
9477
9926
  }
9478
- metas.push(JSON.parse(readFileSync6(target, "utf8")).meta);
9927
+ metas.push(JSON.parse(readFileSync7(target, "utf8")).meta);
9479
9928
  } catch {
9480
9929
  }
9481
9930
  }
@@ -9589,13 +10038,13 @@ import { execFile as execFile3 } from "child_process";
9589
10038
  import { promisify } from "util";
9590
10039
  import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync8 } from "fs";
9591
10040
  import { homedir as homedir6 } from "os";
9592
- import { join as join9, resolve as resolve2, sep as sep2, parse as parsePath } from "path";
9593
- var log21 = forComponent("checkpoints");
10041
+ import { join as join9, resolve as resolve3, sep as sep3, parse as parsePath } from "path";
10042
+ var log23 = forComponent("checkpoints");
9594
10043
  var exec = promisify(execFile3);
9595
10044
  var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
9596
10045
  function isBroadRoot(p) {
9597
- const abs = resolve2(p);
9598
- return abs === resolve2(homedir6()) || abs === parsePath(abs).root;
10046
+ const abs = resolve3(p);
10047
+ return abs === resolve3(homedir6()) || abs === parsePath(abs).root;
9599
10048
  }
9600
10049
  var ShadowRepo = class {
9601
10050
  // undefined = unprobed; false = git/this root unusable
@@ -9636,7 +10085,7 @@ var ShadowRepo = class {
9636
10085
  writeFileSync5(join9(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
9637
10086
  this.ready = true;
9638
10087
  } catch (e) {
9639
- log21.debug(`git checkpoints unavailable for ${this.workTree}`, e);
10088
+ log23.debug(`git checkpoints unavailable for ${this.workTree}`, e);
9640
10089
  this.ready = false;
9641
10090
  }
9642
10091
  return this.ready;
@@ -9647,7 +10096,7 @@ var ShadowRepo = class {
9647
10096
  }
9648
10097
  async commit(label, forced = []) {
9649
10098
  await this.run("add", "-A");
9650
- for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log21.debug(`force-add failed: ${p}`, e));
10099
+ for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log23.debug(`force-add failed: ${p}`, e));
9651
10100
  await this.run("commit", "--allow-empty", "-q", "-m", label);
9652
10101
  }
9653
10102
  /** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
@@ -9655,8 +10104,8 @@ var ShadowRepo = class {
9655
10104
  * turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
9656
10105
  * point per turn, so the REPL's turn↔frame mapping stays intact. */
9657
10106
  async amendForced(paths) {
9658
- for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log21.debug(`force-capture failed: ${p}`, e));
9659
- await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log21.debug("amend failed", e));
10107
+ for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log23.debug(`force-capture failed: ${p}`, e));
10108
+ await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log23.debug("amend failed", e));
9660
10109
  }
9661
10110
  /** Commits on `ref`, oldest-first (canonical index space). */
9662
10111
  async log(ref) {
@@ -9716,7 +10165,7 @@ var ShadowRepo = class {
9716
10165
  await this.run("gc", "--auto", "-q").catch(() => {
9717
10166
  });
9718
10167
  } catch (e) {
9719
- log21.debug("checkpoint prune failed", e);
10168
+ log23.debug("checkpoint prune failed", e);
9720
10169
  }
9721
10170
  }
9722
10171
  };
@@ -9742,12 +10191,12 @@ var GitCheckpoints = class {
9742
10191
  }
9743
10192
  /** cwd + any add-dir mounts that live OUTSIDE cwd (in-cwd mounts are already covered by the cwd root). */
9744
10193
  roots() {
9745
- const cwd = resolve2(this.options.workTree);
10194
+ const cwd = resolve3(this.options.workTree);
9746
10195
  const out = [{ workTree: cwd, gitDir: this.options.gitDir }];
9747
10196
  for (const d of this.options.addDirs ?? []) {
9748
- const abs = resolve2(d);
9749
- if (abs === cwd || abs.startsWith(cwd + sep2)) continue;
9750
- if (cwd.startsWith(abs + sep2)) continue;
10197
+ const abs = resolve3(d);
10198
+ if (abs === cwd || abs.startsWith(cwd + sep3)) continue;
10199
+ if (cwd.startsWith(abs + sep3)) continue;
9751
10200
  out.push({ workTree: abs, gitDir: join9(abs, ".agent", "checkpoints.git") });
9752
10201
  }
9753
10202
  return out;
@@ -9784,7 +10233,7 @@ var GitCheckpoints = class {
9784
10233
  use(sessionId) {
9785
10234
  if (sessionId === this.session) return;
9786
10235
  this.session = sessionId;
9787
- if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log21.debug("re-point failed", e));
10236
+ if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log23.debug("re-point failed", e));
9788
10237
  }
9789
10238
  async begin(label) {
9790
10239
  if (!await this.start()) return;
@@ -9804,7 +10253,7 @@ var GitCheckpoints = class {
9804
10253
  process.stderr.write(`\x1B[2m checkpoints: '${this.repos[i].root}' too large to snapshot in ${Math.round(this.options.opTimeoutMs / 1e3)}s \u2014 file rewind disabled here\x1B[0m
9805
10254
  `);
9806
10255
  } else {
9807
- log21.debug("checkpoint commit failed", e);
10256
+ log23.debug("checkpoint commit failed", e);
9808
10257
  survivors.push(this.repos[i]);
9809
10258
  survivorCaches.push(this.caches[i] ?? []);
9810
10259
  }
@@ -9819,8 +10268,8 @@ var GitCheckpoints = class {
9819
10268
  repoIndexFor(abs) {
9820
10269
  let best = -1, bestLen = -1;
9821
10270
  this.repos.forEach((r, i) => {
9822
- const wt = r.root.endsWith(sep2) ? r.root.slice(0, -1) : r.root;
9823
- if ((abs === wt || abs.startsWith(wt + sep2)) && wt.length > bestLen) {
10271
+ const wt = r.root.endsWith(sep3) ? r.root.slice(0, -1) : r.root;
10272
+ if ((abs === wt || abs.startsWith(wt + sep3)) && wt.length > bestLen) {
9824
10273
  best = i;
9825
10274
  bestLen = wt.length;
9826
10275
  }
@@ -9835,11 +10284,11 @@ var GitCheckpoints = class {
9835
10284
  preToolUse: async (call) => {
9836
10285
  if (!this.snapshotted || !this.repos.length) return;
9837
10286
  for (const rel of mutatedPaths(call)) {
9838
- const abs = resolve2(this.options.workTree, rel);
10287
+ const abs = resolve3(this.options.workTree, rel);
9839
10288
  if (this.forced.has(abs)) continue;
9840
10289
  this.forced.add(abs);
9841
10290
  const i = this.repoIndexFor(abs);
9842
- if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log21.debug("amendForced failed", e));
10291
+ if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log23.debug("amendForced failed", e));
9843
10292
  }
9844
10293
  }
9845
10294
  };
@@ -10039,7 +10488,7 @@ function completePath(listDir, ref) {
10039
10488
  // cli/lineEditor.ts
10040
10489
  import { emitKeypressEvents } from "readline";
10041
10490
  import { spawnSync as spawnSync4 } from "child_process";
10042
- import { writeFileSync as writeFileSync7, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
10491
+ import { writeFileSync as writeFileSync7, readFileSync as readFileSync8, unlinkSync as unlinkSync2 } from "fs";
10043
10492
  import { tmpdir as tmpdir2 } from "os";
10044
10493
  import { join as join11 } from "path";
10045
10494
 
@@ -10635,7 +11084,8 @@ var EditorState = class _EditorState {
10635
11084
  const lines = text.split("\n").length;
10636
11085
  const id = ++this.pasteSeq;
10637
11086
  const measure = multiline ? `+${lines} line${lines === 1 ? "" : "s"}` : `+${text.length} chars`;
10638
- const placeholder = `[Pasted text #${id} ${measure}]`;
11087
+ const preview = pastePreview(text);
11088
+ const placeholder = `[Pasted #${id}${preview ? ` "${preview}"` : ""} ${measure}]`;
10639
11089
  this.pastes.set(placeholder, text);
10640
11090
  this.insert(placeholder);
10641
11091
  } else {
@@ -10914,6 +11364,12 @@ var EditorState = class _EditorState {
10914
11364
  }
10915
11365
  };
10916
11366
  var REWIND = "\0REWIND";
11367
+ var PASTE_PREVIEW_MAX = 40;
11368
+ function pastePreview(text) {
11369
+ const line = text.split("\n").find((l) => l.trim()) ?? "";
11370
+ const clean2 = line.replace(/[[\]]/g, "").replace(/\s+/g, " ").trim();
11371
+ return clean2.length > PASTE_PREVIEW_MAX ? clean2.slice(0, PASTE_PREVIEW_MAX) + "\u2026" : clean2;
11372
+ }
10917
11373
  function applyVimNormal(s, key, str) {
10918
11374
  const k = key?.name;
10919
11375
  if (key?.ctrl || key?.meta) return "pass";
@@ -11228,7 +11684,7 @@ function createLineEditor(out, input = keyInput) {
11228
11684
  out.write("\x1B[?2004l");
11229
11685
  const r = spawnSync4(cmd, [...cargs, file], { stdio: "inherit" });
11230
11686
  if (r.status === 0) {
11231
- const text = readFileSync7(file, "utf8").replace(/\n$/, "");
11687
+ const text = readFileSync8(file, "utf8").replace(/\n$/, "");
11232
11688
  s.reset();
11233
11689
  if (text) s.insert(text);
11234
11690
  }
@@ -11264,10 +11720,10 @@ function createLineEditor(out, input = keyInput) {
11264
11720
  render(s, promptOf(), maxVisible, opts.status);
11265
11721
  };
11266
11722
  process.on("SIGWINCH", onResize);
11267
- return new Promise((resolve4) => {
11723
+ return new Promise((resolve5) => {
11268
11724
  activeAbort = () => {
11269
11725
  finish();
11270
- resolve4(null);
11726
+ resolve5(null);
11271
11727
  };
11272
11728
  const redraw = () => render(s, promptOf(), maxVisible, opts.status);
11273
11729
  let lastStatus = opts.status?.() ?? "";
@@ -11347,20 +11803,20 @@ function createLineEditor(out, input = keyInput) {
11347
11803
  if (action === "submit") {
11348
11804
  finish();
11349
11805
  opts.onSubmit?.();
11350
- return resolve4(s.expand());
11806
+ return resolve5(s.expand());
11351
11807
  }
11352
11808
  if (action === "eof") {
11353
11809
  finish();
11354
- return resolve4(null);
11810
+ return resolve5(null);
11355
11811
  }
11356
11812
  if (action === "rewind") {
11357
11813
  finish();
11358
- return resolve4(REWIND);
11814
+ return resolve5(REWIND);
11359
11815
  }
11360
11816
  if (action === "cancel") {
11361
11817
  if (s.buf.length === 0) {
11362
11818
  finish();
11363
- return resolve4(null);
11819
+ return resolve5(null);
11364
11820
  }
11365
11821
  s.reset();
11366
11822
  s.refresh();
@@ -11473,7 +11929,7 @@ function selectMenu(out, opts) {
11473
11929
  out.write(dim2(` ${filterHint}\u2191/\u2193 select \xB7 ${back}\u23CE choose \xB7 esc cancel${more}`));
11474
11930
  prevLines++;
11475
11931
  };
11476
- return new Promise((resolve4) => {
11932
+ return new Promise((resolve5) => {
11477
11933
  const wasRaw = !!keyInput.isRaw;
11478
11934
  menusOpen++;
11479
11935
  emitKeypressEvents(keyInput);
@@ -11491,7 +11947,7 @@ function selectMenu(out, opts) {
11491
11947
  } catch {
11492
11948
  }
11493
11949
  }
11494
- resolve4(val);
11950
+ resolve5(val);
11495
11951
  };
11496
11952
  const drillIn = () => {
11497
11953
  const it = items[sel];
@@ -11612,19 +12068,19 @@ function windowAround(count, sel, max) {
11612
12068
  var plainBuf = "";
11613
12069
  var plainEnded = false;
11614
12070
  function readPlainLine() {
11615
- return new Promise((resolve4) => {
12071
+ return new Promise((resolve5) => {
11616
12072
  const emit = () => {
11617
12073
  const nl = plainBuf.indexOf("\n");
11618
12074
  if (nl >= 0) {
11619
12075
  const line = plainBuf.slice(0, nl);
11620
12076
  plainBuf = plainBuf.slice(nl + 1);
11621
- resolve4(line);
12077
+ resolve5(line);
11622
12078
  return true;
11623
12079
  }
11624
12080
  if (plainEnded) {
11625
12081
  const rest = plainBuf;
11626
12082
  plainBuf = "";
11627
- resolve4(rest.length ? rest : null);
12083
+ resolve5(rest.length ? rest : null);
11628
12084
  return true;
11629
12085
  }
11630
12086
  return false;
@@ -11655,7 +12111,7 @@ import { spawnSync as spawnSync5 } from "child_process";
11655
12111
  import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
11656
12112
  import { homedir as homedir8 } from "os";
11657
12113
  import { join as join12 } from "path";
11658
- var log22 = forComponent("os-sched");
12114
+ var log24 = forComponent("os-sched");
11659
12115
  var OsScheduler = class {
11660
12116
  options;
11661
12117
  constructor(options) {
@@ -11717,7 +12173,7 @@ var OsScheduler = class {
11717
12173
  }
11718
12174
  }
11719
12175
  } catch (e) {
11720
- log22.debug(`cancel ${id}`, e);
12176
+ log24.debug(`cancel ${id}`, e);
11721
12177
  }
11722
12178
  for (const f of [`${id}.json`, `${id}.sh`]) {
11723
12179
  try {
@@ -11834,7 +12290,7 @@ import { spawn as spawn3 } from "child_process";
11834
12290
  import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "fs";
11835
12291
  import { homedir as homedir9 } from "os";
11836
12292
  import { join as join13 } from "path";
11837
- var log23 = forComponent("remote-trigger");
12293
+ var log25 = forComponent("remote-trigger");
11838
12294
  var TRIGGER_DIR = () => join13(homedir9(), ".agent", "triggers");
11839
12295
  var sockPath = (sessionId, dir = TRIGGER_DIR()) => join13(dir, `${sessionId}.sock`);
11840
12296
  var TriggerServer = class {
@@ -11872,13 +12328,13 @@ var TriggerServer = class {
11872
12328
  conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
11873
12329
  }
11874
12330
  });
11875
- conn.on("error", (e) => log23.debug("trigger conn error", e));
12331
+ conn.on("error", (e) => log25.debug("trigger conn error", e));
11876
12332
  });
11877
- this.server.on("error", (e) => log23.debug("trigger server error", e));
12333
+ this.server.on("error", (e) => log25.debug("trigger server error", e));
11878
12334
  this.server.listen(p);
11879
12335
  this.path = p;
11880
12336
  } catch (e) {
11881
- log23.debug("trigger server unavailable", e);
12337
+ log25.debug("trigger server unavailable", e);
11882
12338
  }
11883
12339
  }
11884
12340
  /** Re-bind on /resume (the session id changed). */
@@ -12000,10 +12456,10 @@ var strike = C("9");
12000
12456
  var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
12001
12457
  var mdPalette = { bold, dim, cyan, italic, strike, green, link };
12002
12458
  var err = (s) => process.stderr.write(s);
12003
- var log24 = forComponent("cli");
12459
+ var log26 = forComponent("cli");
12004
12460
  var VERSION = (() => {
12005
12461
  try {
12006
- return JSON.parse(readFileSync8(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
12462
+ return JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
12007
12463
  } catch {
12008
12464
  return "?";
12009
12465
  }
@@ -12255,7 +12711,7 @@ REPL multi-line: Option/Alt+Enter inserts a newline, or end a line with \\ to co
12255
12711
  REPL shortcuts: Shift+Tab cycles permission posture (ask \u2192 accept-edits \u2192 plan) \xB7 Alt+T toggles reasoning \xB7 Alt+P switches model \xB7 Ctrl+O toggles verbose tool output \xB7 Ctrl+X Ctrl+E edits the buffer in $EDITOR \xB7 \u2192 or Tab accepts the dim history ghost-suggestion \xB7 Alt+S/Ctrl+S stash/unstash.
12256
12712
  REPL steer/stash: type while a turn is running \u2192 Enter STEERS it (the text reaches the model at its next step, alongside the next tool result). Alt+S (or Ctrl+S) with text stashes it for AFTER the turn instead; on an empty prompt pops the next stashed entry for editing.
12257
12713
  REPL editing (emacs/readline): Ctrl-A/E line start/end \xB7 Ctrl-B/F char \xB7 Alt-B/F or Alt/Ctrl-\u2190/\u2192 word \xB7 Ctrl-W kill word \xB7 Ctrl-U/K kill to start/end \xB7 Ctrl-Y yank \xB7 Ctrl-_ undo \xB7 Alt-D kill word fwd \xB7 Ctrl-L clear screen. Set editorMode:'vim' (or /config) for modal vim editing.
12258
- REPL paste: large/multi-line pastes collapse to a [Pasted text +N lines] preview (expands on send); a pasted image/file path attaches as [Image]/[File]; Ctrl-V or /paste grabs a clipboard image (macOS).`;
12714
+ REPL paste: large/multi-line pastes collapse to a [Pasted #N "\u2026" +N lines] preview (expands on send); a pasted image/file path attaches as [Image]/[File]; Ctrl-V or /paste grabs a clipboard image (macOS).`;
12259
12715
  function newestModel() {
12260
12716
  return listModels().slice().sort((a, b) => (getModelInfo(b)?.releaseDate ?? "").localeCompare(getModelInfo(a)?.releaseDate ?? ""))[0] ?? "";
12261
12717
  }
@@ -12290,7 +12746,7 @@ function resolveModelOrNewest(model) {
12290
12746
  var ENV_KEY_ALIASES = { google: ["GEMINI_API_KEY"] };
12291
12747
  function loadEnvFile(file) {
12292
12748
  if (!existsSync9(file)) return;
12293
- for (const line of readFileSync8(file, "utf8").split("\n")) {
12749
+ for (const line of readFileSync9(file, "utf8").split("\n")) {
12294
12750
  const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
12295
12751
  if (!m || m[1] in process.env) continue;
12296
12752
  let val = m[2].trim();
@@ -12745,7 +13201,7 @@ function costOf(pricing, promptTokens = 0, completionTokens = 0, cacheCreationTo
12745
13201
  function turnCost(model, usage) {
12746
13202
  return costOf(getModelInfo(model)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, model);
12747
13203
  }
12748
- async function evaluateGoal(ai, condition, transcript, log25) {
13204
+ async function evaluateGoal(ai, condition, transcript, log27) {
12749
13205
  const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
12750
13206
  const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
12751
13207
  return text.slice(0, 600);
@@ -12765,7 +13221,7 @@ ${recent}` }
12765
13221
  const match = r.content.match(/\{[\s\S]*\}/);
12766
13222
  if (match) return JSON.parse(match[0]);
12767
13223
  } catch (e) {
12768
- log25(dim(` (goal evaluator error: ${e?.message ?? e})
13224
+ log27(dim(` (goal evaluator error: ${e?.message ?? e})
12769
13225
  `));
12770
13226
  }
12771
13227
  return { met: false, reason: "evaluation unclear" };
@@ -12776,10 +13232,8 @@ function errInfo(e) {
12776
13232
  function fmtUsd(n) {
12777
13233
  return n >= 1 ? `$${n.toFixed(2)}` : `$${n.toFixed(4)}`;
12778
13234
  }
12779
- function estimateTranscriptTokens(messages) {
12780
- let chars = 0;
12781
- for (const m of messages) chars += (m.content?.length ?? 0) + (m.tool_calls ? JSON.stringify(m.tool_calls).length : 0);
12782
- return Math.ceil(chars / 4);
13235
+ function estimateTranscriptTokens(messages, keepRecentImages = 1) {
13236
+ return estimateTokens(messages, keepRecentImages);
12783
13237
  }
12784
13238
  function formatStatus(s) {
12785
13239
  const L = (k, v) => ` ${k.padEnd(12)} ${v}
@@ -12891,9 +13345,12 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
12891
13345
  const rules = [...flagRules, ...configRules, ...askRules];
12892
13346
  if (configRules.length) err(dim(` \u2295 ${configRules.length} permission rule(s) from config
12893
13347
  `));
13348
+ const model = resolveModelOrNewest(args.model ?? cfg.model);
12894
13349
  return {
12895
13350
  ai,
12896
- model: resolveModelOrNewest(args.model ?? cfg.model),
13351
+ model,
13352
+ // The real context window drives the context % and `/compact`'s budget (never `maxTokens` — that's spend).
13353
+ contextWindow: getModelInfo(model)?.contextWindow ?? 0,
12897
13354
  cwd: args.cwd,
12898
13355
  tools: cfg.tools,
12899
13356
  extraTools,
@@ -12909,7 +13366,7 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
12909
13366
  stream: args.stream,
12910
13367
  host: makeHost(args.outputFormat, { stream: true }),
12911
13368
  planMode: args.plan || cfg.planMode || false,
12912
- permissions: rules.length ? new PermissionPolicy({ rules, default: "allow", host: makeHost(), ask: makeAskResolver(resolve3(args.cwd ?? process.cwd())) }) : void 0,
13369
+ permissions: rules.length ? new PermissionPolicy({ rules, default: "allow", host: makeHost(), ask: makeAskResolver(resolve4(args.cwd ?? process.cwd())) }) : void 0,
12913
13370
  subagents: args.subagents || cfg.subagents || false,
12914
13371
  maxSteps: args.maxSteps ?? cfg.maxSteps,
12915
13372
  reasoning: args.reasoning ?? cfg.reasoning,
@@ -12985,7 +13442,7 @@ function mcpAgentTools(mounted, opts) {
12985
13442
  return tools;
12986
13443
  }
12987
13444
  async function closeMcp(mounted) {
12988
- await Promise.all(mounted.map((m) => m.client.close().catch((e) => log24.debug("mcp close failed", e))));
13445
+ await Promise.all(mounted.map((m) => m.client.close().catch((e) => log26.debug("mcp close failed", e))));
12989
13446
  }
12990
13447
  var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
12991
13448
  function mentionRefs(line) {
@@ -12998,11 +13455,13 @@ function readImageParts(cwd, line) {
12998
13455
  for (const ref of refs) {
12999
13456
  const mime = IMG_EXT[extname(ref).toLowerCase()];
13000
13457
  if (!mime) continue;
13001
- const abs = ref.startsWith("~/") ? untilde(ref) : resolve3(cwd, ref);
13458
+ const abs = ref.startsWith("~/") ? untilde(ref) : resolve4(cwd, ref);
13002
13459
  try {
13003
- parts.push(imagePart(`data:${mime};base64,${readFileSync8(abs).toString("base64")}`));
13460
+ statSync5(abs);
13004
13461
  } catch {
13462
+ continue;
13005
13463
  }
13464
+ parts.push(imageRefPart(abs, mime));
13006
13465
  }
13007
13466
  return parts;
13008
13467
  }
@@ -13012,9 +13471,9 @@ function pastePathClassifier(cwd) {
13012
13471
  if (!t || t.includes("\n")) return null;
13013
13472
  t = t.replace(/\\ /g, " ").replace(/^['"]|['"]$/g, "");
13014
13473
  if (!/^(\/|~\/|\.\/|\.\.\/)/.test(t)) return null;
13015
- const abs = t.startsWith("~/") ? untilde(t) : resolve3(cwd, t);
13474
+ const abs = t.startsWith("~/") ? untilde(t) : resolve4(cwd, t);
13016
13475
  try {
13017
- if (!statSync4(abs).isFile()) return null;
13476
+ if (!statSync5(abs).isFile()) return null;
13018
13477
  } catch {
13019
13478
  return null;
13020
13479
  }
@@ -13035,7 +13494,7 @@ async function expandMentions(fs, line) {
13035
13494
  if (loaded.includes(ref) || missing.includes(ref)) continue;
13036
13495
  if (ref.includes(":") && mcpMentionResolver) {
13037
13496
  const body = await mcpMentionResolver(ref).catch((e) => {
13038
- log24.debug("mcp mention resolve failed", e);
13497
+ log26.debug("mcp mention resolve failed", e);
13039
13498
  return null;
13040
13499
  });
13041
13500
  if (body != null) {
@@ -13121,7 +13580,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
13121
13580
  try {
13122
13581
  store.save(session);
13123
13582
  } catch (ex) {
13124
- log24.debug("mid-turn session flush failed", ex);
13583
+ log26.debug("mid-turn session flush failed", ex);
13125
13584
  }
13126
13585
  }
13127
13586
  return origNotify(e);
@@ -13190,10 +13649,12 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
13190
13649
  }
13191
13650
  return { ok, res };
13192
13651
  }
13652
+ var remintSessionImages = (data, store, cwd) => remintImageRefs(data.messages, [join14(store.dir, `${data.meta.id}.images`), cwd]);
13193
13653
  function startSession(args, store, agent, cwd) {
13194
13654
  if (args.resume || args.cont) {
13195
13655
  const data = args.resume ? store.load(args.resume) ?? globalSessionLoad(args.resume) : store.latestData();
13196
13656
  if (data) {
13657
+ data.messages = elideStaleImages(remintSessionImages(data, store, cwd), 1);
13197
13658
  agent.transcript = data.messages;
13198
13659
  if (args.fork) {
13199
13660
  const now5 = Date.now();
@@ -13249,7 +13710,7 @@ function initInstructions(cwd) {
13249
13710
  function persistSetting(cwd, key, value) {
13250
13711
  const path = join14(cwd, ".agent", "settings.json");
13251
13712
  try {
13252
- const obj = existsSync9(path) ? JSON.parse(readFileSync8(path, "utf8")) : {};
13713
+ const obj = existsSync9(path) ? JSON.parse(readFileSync9(path, "utf8")) : {};
13253
13714
  if (obj[key] === value) return;
13254
13715
  obj[key] = value;
13255
13716
  mkdirSync12(dirname5(path), { recursive: true });
@@ -13269,14 +13730,14 @@ var isCancelTeardown = (e) => {
13269
13730
  function installCancelGuards(mounted) {
13270
13731
  process.on("unhandledRejection", (e) => {
13271
13732
  if (isCancelTeardown(e)) {
13272
- log24.debug("suppressed unhandledRejection (cursor stream cancel)", e);
13733
+ log26.debug("suppressed unhandledRejection (cursor stream cancel)", e);
13273
13734
  return;
13274
13735
  }
13275
- log24.error("unhandledRejection", e);
13736
+ log26.error("unhandledRejection", e);
13276
13737
  });
13277
13738
  process.on("uncaughtException", (e) => {
13278
13739
  if (isCancelTeardown(e)) {
13279
- log24.debug("suppressed uncaughtException (cursor stream cancel)", e);
13740
+ log26.debug("suppressed uncaughtException (cursor stream cancel)", e);
13280
13741
  return;
13281
13742
  }
13282
13743
  console.error(e);
@@ -13308,7 +13769,7 @@ async function repl(args, ai, cfg, cwd) {
13308
13769
  },
13309
13770
  tickMs: 15e3
13310
13771
  });
13311
- const agentxInvocation = () => process.argv[1] ? `${process.execPath} ${resolve3(process.argv[1])}` : "agentx";
13772
+ const agentxInvocation = () => process.argv[1] ? `${process.execPath} ${resolve4(process.argv[1])}` : "agentx";
13312
13773
  const osSched = new OsScheduler({ agentx: agentxInvocation() });
13313
13774
  const osBackend = osSched.available() ? {
13314
13775
  get sessionId() {
@@ -13547,7 +14008,7 @@ async function repl(args, ai, cfg, cwd) {
13547
14008
  quickLook: {
13548
14009
  branch: () => {
13549
14010
  try {
13550
- const head = readFileSync8(join14(cwd, ".git", "HEAD"), "utf8").trim();
14011
+ const head = readFileSync9(join14(cwd, ".git", "HEAD"), "utf8").trim();
13551
14012
  return head.startsWith("ref: refs/heads/") ? `branch: ${head.slice("ref: refs/heads/".length)}` : `detached HEAD at ${head.slice(0, 12)}`;
13552
14013
  } catch {
13553
14014
  return "not a git repository";
@@ -13613,6 +14074,7 @@ async function repl(args, ai, cfg, cwd) {
13613
14074
  };
13614
14075
  const setModel = (m) => {
13615
14076
  work.model = m;
14077
+ work.contextWindow = getModelInfo(m)?.contextWindow ?? 0;
13616
14078
  work.providerOptions = providerOptionsFor(m, cwd, cfg.mcpServers);
13617
14079
  if (dx) dx.options.actModel = m;
13618
14080
  persistModel(cwd, m);
@@ -13676,10 +14138,11 @@ async function repl(args, ai, cfg, cwd) {
13676
14138
  const pendingImages = [];
13677
14139
  const bangContext = [];
13678
14140
  const grabClipboardAttachment = () => {
13679
- const dir = join14(tmpdir3(), "agentx-pasted");
14141
+ const dir = join14(store.dir, `${session.meta.id}.images`);
13680
14142
  try {
13681
14143
  mkdirSync12(dir, { recursive: true });
13682
- } catch {
14144
+ } catch (e) {
14145
+ log26.debug("paste image dir create failed", e);
13683
14146
  }
13684
14147
  const img = grabClipboardImage(dir, String(Date.now()));
13685
14148
  return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
@@ -13786,13 +14249,13 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
13786
14249
  </system-reminder>`;
13787
14250
  };
13788
14251
  const histPath = join14(cwd, ".agent", "history");
13789
- const history = existsSync9(histPath) ? readFileSync8(histPath, "utf8").split("\n").filter(Boolean).reverse().slice(0, 500) : [];
14252
+ const history = existsSync9(histPath) ? readFileSync9(histPath, "utf8").split("\n").filter(Boolean).reverse().slice(0, 500) : [];
13790
14253
  const remember = (line) => {
13791
14254
  try {
13792
14255
  mkdirSync12(join14(cwd, ".agent"), { recursive: true });
13793
14256
  appendFileSync(histPath, line + "\n");
13794
14257
  } catch (e) {
13795
- log24.debug("history write failed", e);
14258
+ log26.debug("history write failed", e);
13796
14259
  }
13797
14260
  };
13798
14261
  const ago = (t) => {
@@ -13800,6 +14263,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
13800
14263
  return s < 60 ? "just now" : s < 3600 ? `${Math.floor(s / 60)}m ago` : s < 86400 ? `${Math.floor(s / 3600)}h ago` : `${Math.floor(s / 86400)}d ago`;
13801
14264
  };
13802
14265
  const resumeInto = (data) => {
14266
+ data.messages = elideStaleImages(remintSessionImages(data, store, cwd), 1);
13803
14267
  face.transcript = data.messages;
13804
14268
  session = data;
13805
14269
  checkpoints.use?.(data.meta.id);
@@ -13863,7 +14327,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
13863
14327
  try {
13864
14328
  store.save(session);
13865
14329
  } catch (e) {
13866
- log24.debug("session save after rewind failed", e);
14330
+ log26.debug("session save after rewind failed", e);
13867
14331
  }
13868
14332
  err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
13869
14333
  `));
@@ -13897,7 +14361,7 @@ ${task}`;
13897
14361
  bangContext.length = 0;
13898
14362
  }
13899
14363
  const delta = await refreshCatalogs().catch((e) => {
13900
- log24.debug("catalog refresh failed", e);
14364
+ log26.debug("catalog refresh failed", e);
13901
14365
  return "";
13902
14366
  });
13903
14367
  if (delta) {
@@ -14075,8 +14539,8 @@ ${task}`;
14075
14539
  context: {
14076
14540
  desc: "context-window usage (messages + estimated tokens)",
14077
14541
  run: () => {
14078
- const est = estimateTranscriptTokens(face.transcript);
14079
- const cap = face.options.maxTokens || 2e5;
14542
+ const est = estimateTranscriptTokens(face.transcript, face.options.keepRecentImages);
14543
+ const cap = Agent.contextCap(face.options);
14080
14544
  err(dim(` ${face.transcript.length} message(s) \xB7 ~${(est / 1e3).toFixed(1)}k tokens (~${Math.round(est / cap * 100)}% of ${Math.round(cap / 1e3)}k budget)
14081
14545
  `));
14082
14546
  }
@@ -14161,7 +14625,7 @@ ${task}`;
14161
14625
  desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
14162
14626
  run: async () => {
14163
14627
  await refreshCatalogs().catch((e) => {
14164
- log24.debug("catalog refresh failed", e);
14628
+ log26.debug("catalog refresh failed", e);
14165
14629
  });
14166
14630
  face.reprepare();
14167
14631
  err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
@@ -14454,13 +14918,21 @@ ${task}`;
14454
14918
  desc: "summarize older context to free up the window \u2014 /compact [what to preserve]",
14455
14919
  run: (a) => {
14456
14920
  const focus = a.join(" ").trim() || void 0;
14457
- const n = face.compactNow(12, focus);
14921
+ const r = face.compactNow(12, focus);
14458
14922
  session.messages = face.transcript;
14459
14923
  try {
14460
14924
  store.save(session);
14461
14925
  } catch {
14462
14926
  }
14463
- err(dim(` compacted \u2014 folded ${n} message(s)${focus && n ? ` (preserving: ${focus})` : ""}
14927
+ const pct = (t) => `${Math.round(t / r.cap * 100)}%`;
14928
+ const k = (t) => `${(t / 1e3).toFixed(1)}k`;
14929
+ const freed = r.tokensBefore - r.tokensAfter;
14930
+ if (freed <= 0) {
14931
+ err(dim(` nothing to compact \u2014 ~${k(r.tokensAfter)} tok (${pct(r.tokensAfter)} of window) is already at the floor
14932
+ `));
14933
+ return;
14934
+ }
14935
+ err(dim(` compacted \u2014 ~${k(r.tokensBefore)} \u2192 ~${k(r.tokensAfter)} tok (${pct(r.tokensBefore)} \u2192 ${pct(r.tokensAfter)} of window), freed ~${k(freed)}; folded ${r.removed} message(s)${focus ? ` (preserving: ${focus})` : ""}
14464
14936
  `));
14465
14937
  }
14466
14938
  },
@@ -14655,7 +15127,7 @@ ${task}`;
14655
15127
  try {
14656
15128
  for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
14657
15129
  } catch (e) {
14658
- log24.debug(`loadAgents(${d}) failed`, e);
15130
+ log26.debug(`loadAgents(${d}) failed`, e);
14659
15131
  }
14660
15132
  }
14661
15133
  if (!seen.size) {
@@ -14743,7 +15215,7 @@ ${task}`;
14743
15215
  }
14744
15216
  if (idx >= 0) {
14745
15217
  const old = mounted.splice(idx, 1)[0];
14746
- await old.client.close().catch((e) => log24.debug("mcp close failed", e));
15218
+ await old.client.close().catch((e) => log26.debug("mcp close failed", e));
14747
15219
  }
14748
15220
  try {
14749
15221
  const m = await mountMcpServer(name, conf);
@@ -14771,7 +15243,7 @@ ${task}`;
14771
15243
  }
14772
15244
  const m = mounted.splice(idx, 1)[0];
14773
15245
  remountMcpTools();
14774
- await m.client.close().catch((e) => log24.debug("mcp close failed", e));
15246
+ await m.client.close().catch((e) => log26.debug("mcp close failed", e));
14775
15247
  err(dim(` removed "${name}"
14776
15248
  `));
14777
15249
  return;
@@ -14887,7 +15359,7 @@ ${task}`;
14887
15359
  }
14888
15360
  const md = exportMarkdown(session.meta, shown);
14889
15361
  const name = a[0] ? extname(a[0]) ? a[0] : a[0] + ".md" : join14(".agent", "exports", `${session.meta.id}.md`);
14890
- const path = resolve3(cwd, name);
15362
+ const path = resolve4(cwd, name);
14891
15363
  try {
14892
15364
  mkdirSync12(dirname5(path), { recursive: true });
14893
15365
  writeFileSync9(path, md);
@@ -14974,7 +15446,7 @@ ${task}`;
14974
15446
  try {
14975
15447
  return readdirSync4(join14(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
14976
15448
  } catch (e) {
14977
- log24.debug("completion readdir failed", absDir, e);
15449
+ log26.debug("completion readdir failed", absDir, e);
14978
15450
  return null;
14979
15451
  }
14980
15452
  };
@@ -15110,7 +15582,7 @@ ${out}
15110
15582
  return;
15111
15583
  }
15112
15584
  await refreshCatalogs().catch((e) => {
15113
- log24.debug("catalog refresh failed", e);
15585
+ log26.debug("catalog refresh failed", e);
15114
15586
  });
15115
15587
  const sk = skills.find((x) => x.name === name);
15116
15588
  if (sk) {
@@ -15326,8 +15798,8 @@ ${out}
15326
15798
  err("\n");
15327
15799
  const initial = prefill ?? (carry || void 0);
15328
15800
  prefill = void 0;
15329
- const ctxTok = estimateTranscriptTokens(face.transcript);
15330
- const ctxCap = face.options.maxTokens || 2e5;
15801
+ const ctxTok = estimateTranscriptTokens(face.transcript, face.options.keepRecentImages);
15802
+ const ctxCap = Agent.contextCap(face.options);
15331
15803
  {
15332
15804
  const pct = Math.round(ctxTok / ctxCap * 100);
15333
15805
  const step = pct >= 90 ? 90 : pct >= 80 ? 80 : 0;
@@ -15541,7 +16013,7 @@ async function main() {
15541
16013
  let worktreeCleanup;
15542
16014
  if (args.worktree) {
15543
16015
  const { enterWorktree: enterWorktree2, exitWorktree: exitWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
15544
- const session = enterWorktree2(args.worktree, resolve3(args.cwd ?? process.cwd()));
16016
+ const session = enterWorktree2(args.worktree, resolve4(args.cwd ?? process.cwd()));
15545
16017
  err(dim(` \u2295 worktree: ${session.worktreePath} (branch ${session.branch})
15546
16018
  `));
15547
16019
  worktreeCleanup = () => {
@@ -15551,7 +16023,7 @@ async function main() {
15551
16023
  `));
15552
16024
  };
15553
16025
  }
15554
- const cwd = resolve3(args.cwd ?? process.cwd());
16026
+ const cwd = resolve4(args.cwd ?? process.cwd());
15555
16027
  const cfg = await loadConfig(cwd);
15556
16028
  cfg.permissions = mergePerms(loadClaudeSettings(cwd), mergePerms(loadPersistedRules(cwd), cfg.permissions));
15557
16029
  if (canPrompt && !args.yes && !isTrusted(cwd)) {