@cryer/star-cli 0.3.13 → 0.3.14

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.
Files changed (32) hide show
  1. package/README.md +17 -5
  2. package/README.zh-CN.md +15 -5
  3. package/dist/{chunk-6PLZ3FLG.js → chunk-2ISBRSBA.js} +375 -235
  4. package/dist/chunk-2ISBRSBA.js.map +1 -0
  5. package/dist/{chunk-QELCMXQX.js → chunk-4HANB3DJ.js} +381 -212
  6. package/dist/chunk-4HANB3DJ.js.map +1 -0
  7. package/dist/{chunk-OF42OVM4.js → chunk-54KH4JSO.js} +204 -125
  8. package/dist/chunk-54KH4JSO.js.map +1 -0
  9. package/dist/{chunk-RD4S5LKM.js → chunk-CECMCDZG.js} +43 -30
  10. package/dist/chunk-CECMCDZG.js.map +1 -0
  11. package/dist/{chunk-MVAMPVTA.js → chunk-KILMI3S5.js} +13 -5
  12. package/dist/{chunk-MVAMPVTA.js.map → chunk-KILMI3S5.js.map} +1 -1
  13. package/dist/{chunk-NMB3TNQU.js → chunk-NRLGGA7L.js} +507 -175
  14. package/dist/chunk-NRLGGA7L.js.map +1 -0
  15. package/dist/loop-M64D5YII.js +17 -0
  16. package/dist/main.js +177 -131
  17. package/dist/main.js.map +1 -1
  18. package/dist/{provider-EWMUPBLH.js → provider-3Z4B7RMF.js} +2 -2
  19. package/dist/{repl-ZABBT33D.js → repl-N2H7JDSH.js} +617 -327
  20. package/dist/repl-N2H7JDSH.js.map +1 -0
  21. package/dist/{tools-4OLADVDR.js → tools-5RKM7ZI3.js} +3 -3
  22. package/package.json +1 -1
  23. package/dist/chunk-6PLZ3FLG.js.map +0 -1
  24. package/dist/chunk-NMB3TNQU.js.map +0 -1
  25. package/dist/chunk-OF42OVM4.js.map +0 -1
  26. package/dist/chunk-QELCMXQX.js.map +0 -1
  27. package/dist/chunk-RD4S5LKM.js.map +0 -1
  28. package/dist/loop-DWQ5WTPD.js +0 -17
  29. package/dist/repl-ZABBT33D.js.map +0 -1
  30. /package/dist/{loop-DWQ5WTPD.js.map → loop-M64D5YII.js.map} +0 -0
  31. /package/dist/{provider-EWMUPBLH.js.map → provider-3Z4B7RMF.js.map} +0 -0
  32. /package/dist/{tools-4OLADVDR.js.map → tools-5RKM7ZI3.js.map} +0 -0
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  INTERRUPTED_TOOL_RESULT_TEXT,
4
4
  UNFINISHED_TOOL_RESULT_TEXT,
5
+ computeCostUsd,
5
6
  coreMessageText,
6
7
  diffTreeNames,
7
8
  isSyntheticUserMessage,
@@ -12,11 +13,11 @@ import {
12
13
  toCoreMessages,
13
14
  toStarMessage,
14
15
  trackTree
15
- } from "./chunk-RD4S5LKM.js";
16
+ } from "./chunk-CECMCDZG.js";
16
17
  import {
17
18
  createModel,
18
19
  streamActivity
19
- } from "./chunk-MVAMPVTA.js";
20
+ } from "./chunk-KILMI3S5.js";
20
21
  import {
21
22
  resolveModelConfig
22
23
  } from "./chunk-4IRY44TG.js";
@@ -24,7 +25,7 @@ import {
24
25
  ToolRegistry,
25
26
  createDefaultRegistry,
26
27
  debugStreamLog
27
- } from "./chunk-OF42OVM4.js";
28
+ } from "./chunk-54KH4JSO.js";
28
29
  import {
29
30
  TodoStore,
30
31
  beginTurn,
@@ -44,7 +45,7 @@ import {
44
45
  stripOversizedImages,
45
46
  undoTurnSnapshots,
46
47
  userMemoryPath
47
- } from "./chunk-6PLZ3FLG.js";
48
+ } from "./chunk-2ISBRSBA.js";
48
49
 
49
50
  // src/agent/loop.ts
50
51
  import { tool as aiTool, generateText as generateText3 } from "ai";
@@ -147,6 +148,157 @@ function estimateToolSchemaTokens(tools) {
147
148
  return tokens;
148
149
  }
149
150
 
151
+ // src/context/elision.ts
152
+ var DEFAULT_ELISION_ACTIVATION_FRACTION = 0.5;
153
+ var DEFAULT_ELISION_KEEP_RECENT_MESSAGES = 24;
154
+ var DEFAULT_ELISION_MIN_TOKENS = 250;
155
+ var ELIDED_TOOL_RESULT_TEXT = "[stale tool output elided to free context \u2014 it was shown in full earlier; re-run the tool if you need it again]";
156
+ var ELIDED_IMAGE_TEXT = "[image elided to free context \u2014 re-attach it if it is still needed]";
157
+ var ELIDED_TOOL_CALL_ARGS_TEXT = "arguments elided to free context \u2014 the call already ran; this placeholder is not valid tool input, re-read the file if you need the old content";
158
+ function elideStaleContent(messages, thresholdTokens, overheadTokens = 0, options = {}) {
159
+ const activationFraction = options.activationFraction ?? DEFAULT_ELISION_ACTIVATION_FRACTION;
160
+ const keepRecentMessages = options.keepRecentMessages ?? DEFAULT_ELISION_KEEP_RECENT_MESSAGES;
161
+ const minTokens = options.minTokens ?? DEFAULT_ELISION_MIN_TOKENS;
162
+ let total = overheadTokens;
163
+ for (const star of messages) total += estimateMessageTokens(star.message);
164
+ if (total < thresholdTokens * activationFraction) return null;
165
+ const cutoff = messages.length - keepRecentMessages;
166
+ if (cutoff <= 0) return null;
167
+ let elidedCount = 0;
168
+ const out = messages.slice();
169
+ for (let i = 0; i < cutoff; i++) {
170
+ const star = out[i];
171
+ if (!star || star.meta?.elided) continue;
172
+ const message = star.message;
173
+ if (message.role === "tool") {
174
+ if (estimateMessageTokens(message) < minTokens) continue;
175
+ out[i] = {
176
+ message: {
177
+ ...message,
178
+ content: message.content.map((part) => ({
179
+ ...part,
180
+ result: ELIDED_TOOL_RESULT_TEXT
181
+ }))
182
+ },
183
+ meta: { ...star.meta, elided: true }
184
+ };
185
+ elidedCount++;
186
+ } else if (message.role === "assistant" && Array.isArray(message.content)) {
187
+ if (!message.content.some((part) => part.type === "tool-call")) continue;
188
+ if (estimateMessageTokens(message) < minTokens) continue;
189
+ out[i] = {
190
+ message: {
191
+ ...message,
192
+ content: message.content.map(
193
+ (part) => part.type === "tool-call" ? { ...part, args: { elided: ELIDED_TOOL_CALL_ARGS_TEXT } } : part
194
+ )
195
+ },
196
+ meta: { ...star.meta, elided: true }
197
+ };
198
+ elidedCount++;
199
+ } else if (message.role === "user" && Array.isArray(message.content)) {
200
+ if (!message.content.some((part) => part.type === "image" || part.type === "file")) continue;
201
+ out[i] = {
202
+ message: {
203
+ ...message,
204
+ content: message.content.map(
205
+ (part) => part.type === "image" || part.type === "file" ? { type: "text", text: ELIDED_IMAGE_TEXT } : part
206
+ )
207
+ },
208
+ meta: { ...star.meta, elided: true }
209
+ };
210
+ elidedCount++;
211
+ }
212
+ }
213
+ if (elidedCount === 0) return null;
214
+ return { messages: out, elidedCount };
215
+ }
216
+ var ELIDED_ASSISTANT_TEXT = "[assistant content elided by emergency context reduction]";
217
+ function isElidedToolCallArgs(args) {
218
+ return args !== null && typeof args === "object" && args.elided === ELIDED_TOOL_CALL_ARGS_TEXT;
219
+ }
220
+ function emergencyElideMessage(star) {
221
+ const message = star.message;
222
+ if (message.role === "system") return null;
223
+ if (message.role === "tool") {
224
+ if (message.content.every((part) => part.result === ELIDED_TOOL_RESULT_TEXT)) return null;
225
+ return {
226
+ message: {
227
+ ...message,
228
+ content: message.content.map((part) => ({
229
+ ...part,
230
+ result: ELIDED_TOOL_RESULT_TEXT
231
+ }))
232
+ },
233
+ meta: { ...star.meta, elided: true }
234
+ };
235
+ }
236
+ if (message.role === "user") {
237
+ if (!Array.isArray(message.content)) return null;
238
+ if (!message.content.some((part) => part.type === "image" || part.type === "file")) {
239
+ return null;
240
+ }
241
+ return {
242
+ message: {
243
+ ...message,
244
+ content: message.content.map(
245
+ (part) => part.type === "image" || part.type === "file" ? { type: "text", text: ELIDED_IMAGE_TEXT } : part
246
+ )
247
+ },
248
+ meta: { ...star.meta, elided: true }
249
+ };
250
+ }
251
+ if (typeof message.content === "string") {
252
+ if (message.content === ELIDED_ASSISTANT_TEXT) return null;
253
+ return {
254
+ message: { ...message, content: ELIDED_ASSISTANT_TEXT },
255
+ meta: { ...star.meta, elided: true }
256
+ };
257
+ }
258
+ let changed = false;
259
+ const content = message.content.map((part) => {
260
+ if (part.type === "tool-call" && !isElidedToolCallArgs(part.args)) {
261
+ changed = true;
262
+ return { ...part, args: { elided: ELIDED_TOOL_CALL_ARGS_TEXT } };
263
+ }
264
+ if ((part.type === "text" || part.type === "reasoning") && part.text !== ELIDED_ASSISTANT_TEXT) {
265
+ changed = true;
266
+ return { ...part, text: ELIDED_ASSISTANT_TEXT };
267
+ }
268
+ return part;
269
+ });
270
+ if (!changed) return null;
271
+ return { message: { ...message, content }, meta: { ...star.meta, elided: true } };
272
+ }
273
+ function emergencyElideContent(messages, maxTokens, overheadTokens = 0, options = {}) {
274
+ const minTokens = options.minTokens ?? 1;
275
+ const out = messages.slice();
276
+ const tokens = messages.map((star) => estimateMessageTokens(star.message));
277
+ let estimatedTokens = overheadTokens;
278
+ for (const count of tokens) estimatedTokens += count;
279
+ let elidedCount = 0;
280
+ const floors = minTokens < DEFAULT_ELISION_MIN_TOKENS ? [DEFAULT_ELISION_MIN_TOKENS, minTokens] : [minTokens];
281
+ for (const floor of floors) {
282
+ for (let i = 0; i < out.length && estimatedTokens > maxTokens; i++) {
283
+ const star = out[i];
284
+ if (!star || (tokens[i] ?? 0) < floor) continue;
285
+ const replacement = emergencyElideMessage(star);
286
+ if (!replacement) continue;
287
+ const replacementTokens = estimateMessageTokens(replacement.message);
288
+ if (replacementTokens >= (tokens[i] ?? 0)) continue;
289
+ out[i] = replacement;
290
+ estimatedTokens += replacementTokens - (tokens[i] ?? 0);
291
+ elidedCount++;
292
+ }
293
+ }
294
+ return {
295
+ messages: out,
296
+ elidedCount,
297
+ estimatedTokens,
298
+ withinBudget: estimatedTokens <= maxTokens
299
+ };
300
+ }
301
+
150
302
  // src/context/compaction.ts
151
303
  var MIN_KEPT_MESSAGES = 4;
152
304
  function placeholderMessage(droppedCount) {
@@ -227,20 +379,22 @@ function serializeSummaryInput(messages) {
227
379
  [... ${total - MAX_SUMMARY_INPUT_CHARS} characters omitted ...]
228
380
  ${tail}`;
229
381
  }
230
- async function summarizeMessages(messages, model, signal, temperature) {
382
+ async function summarizeMessages(messages, model, signal, temperature, onUsage) {
231
383
  const transcript = serializeSummaryInput(messages);
232
- const { text } = await generateText({
384
+ const { text, usage } = await generateText({
233
385
  model,
234
386
  system: SUMMARY_SYSTEM_PROMPT,
235
387
  prompt: `Summarize this conversation so far:
236
388
 
237
389
  ${transcript}`,
238
390
  temperature,
391
+ maxRetries: 0,
239
392
  // A hung relay must not stall the whole turn on the summary: cap it at
240
393
  // 60s and let the caller's abort (Esc) cut it short as well; the caller
241
394
  // falls back to the truncation placeholder on any failure.
242
395
  abortSignal: signal ? AbortSignal.any([signal, AbortSignal.timeout(6e4)]) : AbortSignal.timeout(6e4)
243
396
  });
397
+ onUsage?.(usage);
244
398
  return text.trim();
245
399
  }
246
400
  function compactMessages(messages, maxTokens, opts = {}) {
@@ -298,69 +452,6 @@ function compactMessages(messages, maxTokens, opts = {}) {
298
452
  };
299
453
  }
300
454
 
301
- // src/context/elision.ts
302
- var ACTIVATION_FRACTION = 0.5;
303
- var KEEP_RECENT_MESSAGES = 24;
304
- var MIN_ELISION_TOKENS = 250;
305
- var ELIDED_TOOL_RESULT_TEXT = "[stale tool output elided to free context \u2014 it was shown in full earlier; re-run the tool if you need it again]";
306
- var ELIDED_IMAGE_TEXT = "[image elided to free context \u2014 re-attach it if it is still needed]";
307
- var ELIDED_TOOL_CALL_ARGS_TEXT = "arguments elided to free context \u2014 the call already ran; this placeholder is not valid tool input, re-read the file if you need the old content";
308
- function elideStaleContent(messages, thresholdTokens, overheadTokens = 0) {
309
- let total = overheadTokens;
310
- for (const star of messages) total += estimateMessageTokens(star.message);
311
- if (total < thresholdTokens * ACTIVATION_FRACTION) return null;
312
- const cutoff = messages.length - KEEP_RECENT_MESSAGES;
313
- if (cutoff <= 0) return null;
314
- let elidedCount = 0;
315
- const out = messages.slice();
316
- for (let i = 0; i < cutoff; i++) {
317
- const star = out[i];
318
- if (!star || star.meta?.elided) continue;
319
- const message = star.message;
320
- if (message.role === "tool") {
321
- if (estimateMessageTokens(message) < MIN_ELISION_TOKENS) continue;
322
- out[i] = {
323
- message: {
324
- ...message,
325
- content: message.content.map((part) => ({
326
- ...part,
327
- result: ELIDED_TOOL_RESULT_TEXT
328
- }))
329
- },
330
- meta: { ...star.meta, elided: true }
331
- };
332
- elidedCount++;
333
- } else if (message.role === "assistant" && Array.isArray(message.content)) {
334
- if (!message.content.some((part) => part.type === "tool-call")) continue;
335
- if (estimateMessageTokens(message) < MIN_ELISION_TOKENS) continue;
336
- out[i] = {
337
- message: {
338
- ...message,
339
- content: message.content.map(
340
- (part) => part.type === "tool-call" ? { ...part, args: { elided: ELIDED_TOOL_CALL_ARGS_TEXT } } : part
341
- )
342
- },
343
- meta: { ...star.meta, elided: true }
344
- };
345
- elidedCount++;
346
- } else if (message.role === "user" && Array.isArray(message.content)) {
347
- if (!message.content.some((part) => part.type === "image" || part.type === "file")) continue;
348
- out[i] = {
349
- message: {
350
- ...message,
351
- content: message.content.map(
352
- (part) => part.type === "image" || part.type === "file" ? { type: "text", text: ELIDED_IMAGE_TEXT } : part
353
- )
354
- },
355
- meta: { ...star.meta, elided: true }
356
- };
357
- elidedCount++;
358
- }
359
- }
360
- if (elidedCount === 0) return null;
361
- return { messages: out, elidedCount };
362
- }
363
-
364
455
  // src/core/events.ts
365
456
  function usageTotalTokens(usage) {
366
457
  const summed = usage.promptTokens + usage.completionTokens;
@@ -375,6 +466,19 @@ function toStreamErrorInfo(error) {
375
466
  if (typeof raw.statusCode === "number") info2.statusCode = raw.statusCode;
376
467
  if (typeof raw.status === "number") info2.statusCode = raw.status;
377
468
  if (typeof raw.code === "string") info2.code = raw.code;
469
+ if (raw.isRetryable === true) info2.isRetryable = true;
470
+ const causeCode2 = raw.cause && typeof raw.cause === "object" ? raw.cause.code : void 0;
471
+ if (typeof causeCode2 === "string") info2.causeCode = causeCode2;
472
+ if (raw.responseHeaders && typeof raw.responseHeaders === "object") {
473
+ const strings = {};
474
+ for (const [key, value] of Object.entries(raw.responseHeaders)) {
475
+ if (typeof value === "string") strings[key] = value;
476
+ }
477
+ if (Object.keys(strings).length > 0) info2.responseHeaders = strings;
478
+ }
479
+ if (typeof raw.responseBody === "string" && raw.responseBody.length > 0) {
480
+ info2.responseBody = raw.responseBody;
481
+ }
378
482
  return info2;
379
483
  }
380
484
  const err = error instanceof Error ? error : new Error(String(error));
@@ -401,7 +505,7 @@ function toStreamErrorInfo(error) {
401
505
  }
402
506
 
403
507
  // src/core/git.ts
404
- import { execFileSync } from "child_process";
508
+ import { execFile, execFileSync } from "child_process";
405
509
  var GIT_TIMEOUT_MS = 2e3;
406
510
  var GIT_MAX_BUFFER = 8 * 1024 * 1024;
407
511
  var COMMIT_DIFF_MAX_LINES = 2e3;
@@ -441,6 +545,47 @@ function getGitSummaryCached(cwd) {
441
545
  gitSummaryCache.set(cwd, { at: now, summary });
442
546
  return summary;
443
547
  }
548
+ function runGitAsync(args, cwd) {
549
+ return new Promise((resolve) => {
550
+ execFile(
551
+ "git",
552
+ args,
553
+ {
554
+ cwd,
555
+ timeout: GIT_TIMEOUT_MS,
556
+ encoding: "utf8",
557
+ maxBuffer: GIT_MAX_BUFFER
558
+ },
559
+ (error, stdout) => resolve(error ? null : stdout.replace(/\n$/, ""))
560
+ );
561
+ });
562
+ }
563
+ var gitSummaryPending = /* @__PURE__ */ new Map();
564
+ function getGitSummaryCachedAsync(cwd) {
565
+ const now = Date.now();
566
+ const hit = gitSummaryCache.get(cwd);
567
+ if (hit && now - hit.at < GIT_SUMMARY_CACHE_TTL_MS) return Promise.resolve(hit.summary);
568
+ const pending = gitSummaryPending.get(cwd);
569
+ if (pending) return pending;
570
+ const task = (async () => {
571
+ const branch = await runGitAsync(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
572
+ if (branch === null) {
573
+ gitSummaryCache.set(cwd, { at: Date.now(), summary: null });
574
+ return null;
575
+ }
576
+ const status = await runGitAsync(["status", "--porcelain"], cwd) ?? "";
577
+ const log = await runGitAsync(["log", "--oneline", "-3"], cwd) ?? "";
578
+ const summary = {
579
+ branch,
580
+ dirtyCount: status === "" ? 0 : status.split("\n").length,
581
+ recentCommits: log === "" ? [] : log.split("\n")
582
+ };
583
+ gitSummaryCache.set(cwd, { at: Date.now(), summary });
584
+ return summary;
585
+ })().finally(() => gitSummaryPending.delete(cwd));
586
+ gitSummaryPending.set(cwd, task);
587
+ return task;
588
+ }
444
589
  function formatGitSummary(summary) {
445
590
  const lines = [
446
591
  "Git context for the working directory (refreshed each turn):",
@@ -575,6 +720,10 @@ function runCommand(hook, event, ctx, timeoutSec) {
575
720
  child.stdin.on("error", () => {
576
721
  });
577
722
  child.stdin.end(inputJson);
723
+ } else {
724
+ child.stdin?.on("error", () => {
725
+ });
726
+ child.stdin?.end();
578
727
  }
579
728
  let stderr = "";
580
729
  const stderrDecoder = new StringDecoder("utf8");
@@ -675,7 +824,9 @@ var NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
675
824
  "EPIPE",
676
825
  "EAI_AGAIN",
677
826
  "ENOTFOUND",
678
- "ECONNABORTED"
827
+ "ECONNABORTED",
828
+ "EHOSTUNREACH",
829
+ "ENETUNREACH"
679
830
  ]);
680
831
  function isNetworkFailure(error) {
681
832
  for (const code of [error.code, error.causeCode]) {
@@ -1224,6 +1375,7 @@ function splitShellSegments(tokens) {
1224
1375
  }
1225
1376
  return segments;
1226
1377
  }
1378
+ var DYNAMIC_SHELL_WORD = "\0star-dynamic-word\0";
1227
1379
  function segmentWords(segment) {
1228
1380
  const words = [];
1229
1381
  const redirectTargets = [];
@@ -1234,11 +1386,16 @@ function segmentWords(segment) {
1234
1386
  if (target?.kind === "word") {
1235
1387
  redirectTargets.push(target.text);
1236
1388
  k += 1;
1389
+ } else if (target?.kind === "subst") {
1390
+ redirectTargets.push(DYNAMIC_SHELL_WORD);
1391
+ k += 1;
1237
1392
  }
1238
1393
  continue;
1239
1394
  }
1240
1395
  if (token.kind === "word") {
1241
1396
  words.push(token.text);
1397
+ } else if (token.kind === "subst") {
1398
+ words.push(DYNAMIC_SHELL_WORD);
1242
1399
  }
1243
1400
  }
1244
1401
  return { words, redirectTargets };
@@ -1389,7 +1546,11 @@ function baseName(word) {
1389
1546
  const base = word.replace(/\\/g, "/").split("/").pop() ?? word;
1390
1547
  return base.toLowerCase().replace(/\.exe$/, "");
1391
1548
  }
1549
+ function isDynamicTarget(target) {
1550
+ return target.includes(DYNAMIC_SHELL_WORD) || /\$[A-Za-z_]/.test(target) || /%[A-Za-z_][A-Za-z0-9_]*%/.test(target);
1551
+ }
1392
1552
  function isDangerousDeleteTarget(target) {
1553
+ if (isDynamicTarget(target)) return true;
1393
1554
  if (/^\/+$/.test(target)) return true;
1394
1555
  const norm = target.replace(/\\/g, "/").replace(/\/+$/, "");
1395
1556
  if (norm === "/*" || norm === "*") return true;
@@ -1663,7 +1824,7 @@ function decodePowerShellEncoded(encoded) {
1663
1824
  }
1664
1825
  function segmentIsDangerous(segment, depth, lex) {
1665
1826
  const { words, redirectTargets } = segmentWords(segment);
1666
- if (redirectTargets.some((target) => BLOCK_DEVICE.test(target))) {
1827
+ if (redirectTargets.some((target) => isDynamicTarget(target) || BLOCK_DEVICE.test(target))) {
1667
1828
  return true;
1668
1829
  }
1669
1830
  const invocation = commandInvocation(words);
@@ -1705,10 +1866,10 @@ function segmentIsDangerous(segment, depth, lex) {
1705
1866
  return recursive && targets.some(isDangerousDeleteTarget);
1706
1867
  }
1707
1868
  case "format":
1708
- return rest.some((arg) => /^[a-z]:$/i.test(arg));
1869
+ return rest.some((arg) => isDynamicTarget(arg) || /^[a-z]:$/i.test(arg));
1709
1870
  case "dd":
1710
1871
  return rest.some(
1711
- (arg) => arg.startsWith("if=") || arg.startsWith("of=") && BLOCK_DEVICE.test(arg.slice(3))
1872
+ (arg) => arg.startsWith("if=") || arg.startsWith("of=") && (isDynamicTarget(arg.slice(3)) || BLOCK_DEVICE.test(arg.slice(3)))
1712
1873
  );
1713
1874
  case "find":
1714
1875
  return findIsDangerous(rest, depth, lex);
@@ -2007,21 +2168,23 @@ function sanitizeTitle(raw) {
2007
2168
  } while (title !== previous);
2008
2169
  return title.slice(0, TITLE_MAX_CHARS).trim();
2009
2170
  }
2010
- async function generateSessionTitle(userText, model) {
2011
- const { text } = await generateText2({
2171
+ async function generateSessionTitle(userText, model, onUsage) {
2172
+ const { text, usage } = await generateText2({
2012
2173
  model,
2013
2174
  system: TITLE_SYSTEM_PROMPT,
2014
2175
  prompt: userText.slice(0, TITLE_INPUT_MAX_CHARS),
2015
2176
  maxTokens: TITLE_MAX_TOKENS,
2177
+ maxRetries: 0,
2016
2178
  abortSignal: AbortSignal.timeout(TITLE_TIMEOUT_MS)
2017
2179
  });
2180
+ onUsage?.(usage);
2018
2181
  return sanitizeTitle(text);
2019
2182
  }
2020
- function scheduleSessionTitle(store, userText, model) {
2183
+ function scheduleSessionTitle(store, userText, model, onUsage) {
2021
2184
  void (async () => {
2022
2185
  const meta = await store.meta();
2023
2186
  if (meta.title) return;
2024
- const title = await generateSessionTitle(userText, model);
2187
+ const title = await generateSessionTitle(userText, model, onUsage);
2025
2188
  if (!title) return;
2026
2189
  const current = await store.meta();
2027
2190
  if (current.title) return;
@@ -2190,44 +2353,8 @@ var MAX_SUBAGENT_DEPTH = 1;
2190
2353
  var MAX_REPORT_CHARS = 2e4;
2191
2354
  var SUBAGENT_PROMPT = "You are a subagent spawned to handle a focused subtask on behalf of the main agent. Work autonomously with the tools available to you. Your final text is returned to the main agent as the tool result \u2014 make it a self-contained report: state what you found or did, cite exact file paths with line numbers for anything the main agent may need to open, and note explicitly anything left undone or unverified. Report outcomes, not process.";
2192
2355
  var READ_ONLY_PROMPT = "This subtask is read-only: your tools cannot modify files, run shell commands, or kill tasks. Investigate and report; do not attempt changes.";
2193
- function convertUsagePricing(usage, from, to, config, names) {
2194
- if (from === to) return usage;
2195
- const fromCfg = findModelPricing(config, names?.fromName, from.modelId);
2196
- const toCfg = findModelPricing(config, names?.toName, to.modelId);
2197
- if (fromCfg?.promptPrice === void 0 || fromCfg.completionPrice === void 0 || toCfg?.promptPrice === void 0 || toCfg.completionPrice === void 0 || toCfg.promptPrice <= 0 || toCfg.completionPrice <= 0) {
2198
- return usage;
2199
- }
2200
- const cached = Math.min(Math.max(usage.cachedPromptTokens ?? 0, 0), usage.promptTokens);
2201
- const promptUsd = (usage.promptTokens - cached) * fromCfg.promptPrice;
2202
- const cachedUsd = cached * (fromCfg.cacheReadPrice ?? fromCfg.promptPrice);
2203
- const cacheReadUsd = (usage.cacheReadInputTokens ?? 0) * (fromCfg.cacheReadPrice ?? 0);
2204
- const completionUsd = usage.completionTokens * fromCfg.completionPrice;
2205
- const scaledCached = cachedUsd / (toCfg.cacheReadPrice ?? toCfg.promptPrice);
2206
- const promptTokens = Math.round(promptUsd / toCfg.promptPrice + scaledCached);
2207
- const cachedPromptTokens = Math.round(scaledCached);
2208
- const toCacheReadPrice = toCfg.cacheReadPrice !== void 0 && toCfg.cacheReadPrice > 0 ? toCfg.cacheReadPrice : void 0;
2209
- const completionTokens = Math.round(
2210
- completionUsd / toCfg.completionPrice + (toCacheReadPrice === void 0 ? cacheReadUsd / toCfg.completionPrice : 0)
2211
- );
2212
- const cacheReadInputTokens = toCacheReadPrice === void 0 ? 0 : Math.round(cacheReadUsd / toCacheReadPrice);
2213
- const result = {
2214
- promptTokens,
2215
- completionTokens,
2216
- totalTokens: promptTokens + completionTokens
2217
- };
2218
- if (cachedPromptTokens > 0) result.cachedPromptTokens = cachedPromptTokens;
2219
- if (cacheReadInputTokens > 0) result.cacheReadInputTokens = cacheReadInputTokens;
2220
- return result;
2221
- }
2222
- function findModelPricing(config, name, modelId) {
2223
- if (name !== void 0) {
2224
- const byName = config.models.find((m) => m.name === name);
2225
- if (byName) return byName;
2226
- }
2227
- return config.models.find((m) => m.model === modelId);
2228
- }
2229
2356
  async function runSubagent(deps, args, cwd, signal, agentIdRef) {
2230
- const { AgentLoop: AgentLoop2 } = await import("./loop-DWQ5WTPD.js");
2357
+ const { AgentLoop: AgentLoop2 } = await import("./loop-M64D5YII.js");
2231
2358
  const child = new AgentLoop2({
2232
2359
  model: deps.model,
2233
2360
  registry: createDefaultRegistry(),
@@ -2242,6 +2369,7 @@ ${READ_ONLY_PROMPT}` : ""}` : `${SUBAGENT_PROMPT}${args.read_only ? `
2242
2369
  ${READ_ONLY_PROMPT}` : ""}`,
2243
2370
  providerMetadata: deps.providerMetadata,
2244
2371
  temperature: deps.temperature,
2372
+ maxTokens: deps.maxTokens,
2245
2373
  streamIdleTimeoutSec: deps.streamIdleTimeoutSec,
2246
2374
  streamFirstChunkTimeoutSec: deps.streamFirstChunkTimeoutSec,
2247
2375
  vision: deps.vision,
@@ -2443,15 +2571,16 @@ ${context}
2443
2571
  }
2444
2572
  return messages;
2445
2573
  }
2446
- async function checkTaskComplete(model, request, finalReply, signal, temperature) {
2574
+ async function checkTaskComplete(model, request, finalReply, toolSummary, signal, temperature, onUsage) {
2447
2575
  const controller = new AbortController();
2448
2576
  const timer = setTimeout(() => controller.abort(), COMPLETION_CHECK_TIMEOUT_MS);
2449
2577
  const onAbort = () => controller.abort();
2450
2578
  signal.addEventListener("abort", onAbort, { once: true });
2451
2579
  try {
2452
- const { text } = await generateText3({
2580
+ const { text, usage } = await generateText3({
2453
2581
  model,
2454
2582
  maxTokens: 8,
2583
+ maxRetries: 0,
2455
2584
  temperature,
2456
2585
  abortSignal: controller.signal,
2457
2586
  prompt: [
@@ -2463,9 +2592,14 @@ ${request.slice(0, 2e3)}
2463
2592
  `<reply>
2464
2593
  ${finalReply.slice(0, 2e3)}
2465
2594
  </reply>`,
2466
- "Has the agent fully completed the task \u2014 every requested action actually performed, and the result verified by running a check (tests, build, or inspecting the produced output) rather than assumed? For a task that changed code, completion also requires tests covering the change when the project has a test setup, and a test run that actually passed \u2014 a reply that claims success without an observed green check is not complete. Or is it only describing, planning, or reporting progress with work still left? Answer with exactly one word: DONE or NOT_DONE."
2595
+ "These are the actual tool outcomes, including failures:",
2596
+ `<tool-outcomes>
2597
+ ${toolSummary.slice(0, 6e3)}
2598
+ </tool-outcomes>`,
2599
+ "Has the agent fully completed the task \u2014 every requested action actually performed, and the result verified by running a check (tests, build, or inspecting the produced output) rather than assumed? Failed tools and missing verification mean the work is not complete even when the reply claims success. For a task that changed code, completion also requires tests covering the change when the project has a test setup, and a test run that actually passed. Or is it only describing, planning, or reporting progress with work still left? Answer with exactly one word: DONE or NOT_DONE."
2467
2600
  ].join("\n")
2468
2601
  });
2602
+ onUsage(usage);
2469
2603
  const verdict = text.trim().toUpperCase();
2470
2604
  if (verdict.startsWith("NOT_DONE")) return false;
2471
2605
  if (verdict.startsWith("DONE")) return true;
@@ -2529,15 +2663,6 @@ var AgentLoop = class {
2529
2663
  // each new user turn restarts the streak.
2530
2664
  lastToolSignature = null;
2531
2665
  repeatedToolCalls = 0;
2532
- // Token usage subagents consumed, waiting to fold into this loop's next
2533
- // finish event with usage: sync children report when their tool call
2534
- // settles, background ones whenever they finish (drained or not).
2535
- // Consumers meter only root-loop finish events, so folding here bills
2536
- // subagent spend with zero consumer changes — /cost, /usage and
2537
- // sessionBudgetUsd all cover it. A background child still running when the
2538
- // session's last finish has passed never reports (best effort, noted in
2539
- // agent-tasks cleanup paths).
2540
- subagentUsage = null;
2541
2666
  // Turn context of the in-flight stream() turn, stamped onto every tool
2542
2667
  // execution so file snapshots attribute root changes to a turn and mark
2543
2668
  // subagent changes as excluded from turn-level retraction.
@@ -2556,6 +2681,7 @@ var AgentLoop = class {
2556
2681
  // turn context string. Saves a full toCoreMessages + attachTurnContext copy
2557
2682
  // per stream attempt, which retried steps used to pay every time.
2558
2683
  requestMessagesCache = null;
2684
+ historyTokenSnapshot = null;
2559
2685
  // Memoized per (registry, plan flag): tools register on the registry only
2560
2686
  // at loop construction, and the plan filter re-reads the live permission
2561
2687
  // mode on every call — so a built map stays valid until either input
@@ -2598,11 +2724,12 @@ var AgentLoop = class {
2598
2724
  // their stderr surfaces through this callback (REPL system message / stderr
2599
2725
  // in print mode).
2600
2726
  onHookWarning;
2727
+ onUsage;
2601
2728
  constructor(opts) {
2602
2729
  this.opts = opts;
2603
2730
  const depth = opts.subagentDepth ?? 0;
2604
2731
  if (depth > 0) {
2605
- opts.registry = new ToolRegistry(new TodoStore());
2732
+ opts.registry = new ToolRegistry(new TodoStore({ persistence: "memory" }));
2606
2733
  } else {
2607
2734
  setTodoPersistGuard(() => {
2608
2735
  const mode = this.opts.config.permissionMode;
@@ -2618,6 +2745,7 @@ var AgentLoop = class {
2618
2745
  opts.registry?.register(createSkillTool({ cwd: opts.cwd }));
2619
2746
  opts.registry?.register(createRememberTool());
2620
2747
  if (opts.registry && depth < MAX_SUBAGENT_DEPTH) {
2748
+ const subagentSessionStore = opts.sessionStore;
2621
2749
  opts.registry.register(
2622
2750
  createSubagentTool({
2623
2751
  model: opts.model,
@@ -2626,13 +2754,14 @@ var AgentLoop = class {
2626
2754
  system: opts.system,
2627
2755
  providerMetadata: opts.providerMetadata,
2628
2756
  temperature: opts.temperature,
2757
+ maxTokens: opts.maxTokens,
2629
2758
  streamIdleTimeoutSec: opts.streamIdleTimeoutSec,
2630
2759
  streamFirstChunkTimeoutSec: opts.streamFirstChunkTimeoutSec,
2631
2760
  vision: opts.vision,
2632
2761
  depth,
2633
2762
  modelName: opts.modelName,
2634
2763
  getConfirmHandler: () => this.confirmHandler,
2635
- onUsage: (usage, childModel, childModelName) => this.addSubagentUsage(usage, childModel, childModelName)
2764
+ onUsage: (usage, _childModel, childModelName) => this.recordUsage(usage, childModelName, subagentSessionStore)
2636
2765
  })
2637
2766
  );
2638
2767
  }
@@ -2698,8 +2827,25 @@ var AgentLoop = class {
2698
2827
  // budgets deliberately use only the first three (history + schemas +
2699
2828
  // calibration), since in-flight reasoning is never resent and must not
2700
2829
  // trigger a compaction the next request won't need.
2830
+ historyTokens() {
2831
+ const cached = this.historyTokenSnapshot;
2832
+ if (cached?.source === this.messages && cached.length === this.messages.length) {
2833
+ return cached.total;
2834
+ }
2835
+ let total = 0;
2836
+ let start = 0;
2837
+ if (cached?.source === this.messages && cached.length < this.messages.length) {
2838
+ total = cached.total;
2839
+ start = cached.length;
2840
+ }
2841
+ for (let i = start; i < this.messages.length; i++) {
2842
+ total += estimateMessageTokens(this.messages[i]?.message ?? { role: "user", content: "" });
2843
+ }
2844
+ this.historyTokenSnapshot = { source: this.messages, length: this.messages.length, total };
2845
+ return total;
2846
+ }
2701
2847
  estimateContextTokens() {
2702
- return estimateTokens(this.getMessages()) + this.getToolSchemaTokens() + this.calibrationDelta() + (this.inflightStreamText.length > 0 ? roughTextTokens(this.inflightStreamText) : 0);
2848
+ return this.historyTokens() + this.getToolSchemaTokens() + this.calibrationDelta() + (this.inflightStreamText.length > 0 ? roughTextTokens(this.inflightStreamText) : 0);
2703
2849
  }
2704
2850
  // Auxiliary model for cheap side calls: config.smallModel, resolved once.
2705
2851
  // An unresolvable name/key degrades to the main model instead of breaking
@@ -2724,24 +2870,30 @@ var AgentLoop = class {
2724
2870
  this.resolveAuxModel();
2725
2871
  return this.auxModelValue ? this.auxTemperatureValue : this.opts.temperature;
2726
2872
  }
2873
+ getAuxModelName() {
2874
+ this.resolveAuxModel();
2875
+ return this.auxModelValue && this.opts.config.smallModel ? this.opts.config.smallModel : this.opts.modelName ?? this.opts.model.modelId;
2876
+ }
2877
+ recordUsage(usage, modelName = this.opts.modelName ?? this.opts.model.modelId, store = this.opts.sessionStore) {
2878
+ const normalized = { ...usage, totalTokens: usageTotalTokens(usage) };
2879
+ const model = this.opts.config.models.find((entry) => entry.name === modelName);
2880
+ const costUsd = computeCostUsd(normalized, model);
2881
+ this.onUsage?.(normalized, modelName, costUsd);
2882
+ void store?.addUsage(normalized, { modelName, costUsd }).catch(
2883
+ (error) => this.onHookWarning?.(`failed to record token usage: ${String(error)}`)
2884
+ );
2885
+ }
2727
2886
  // Rebinds the per-call provider metadata (e.g. after the reasoning effort
2728
2887
  // for the current model changed via /model) without recreating the loop.
2729
2888
  setProviderMetadata(metadata) {
2730
2889
  this.opts.providerMetadata = metadata;
2731
2890
  }
2732
2891
  // Swaps the persistence target (/new starts a fresh session mid-REPL).
2733
- // Title generation is re-armed so the new session gets one after its first
2734
- // turn, and snapshot checkpoints now flow to the new store. The redo stack
2735
- // dies with the old session context: its trees describe working-tree states
2736
- // that no longer line up with the new session's conversation. Subagent
2737
- // usage still waiting to fold belongs to the old conversation too — it
2738
- // must not bill the new session's first finish.
2739
2892
  setSessionStore(store) {
2740
2893
  this.opts.sessionStore = store;
2741
2894
  this.titleScheduled = false;
2742
2895
  this.titleInput = void 0;
2743
2896
  this.redoStack = [];
2744
- this.subagentUsage = null;
2745
2897
  if (store) this.bindSnapshotHooks(store);
2746
2898
  }
2747
2899
  bindSnapshotHooks(store) {
@@ -2763,13 +2915,17 @@ var AgentLoop = class {
2763
2915
  onRemove: (ids) => store.removeCheckpoints(ids)
2764
2916
  });
2765
2917
  }
2918
+ resetWholesaleHistoryState() {
2919
+ this.serverTokenBaseline = null;
2920
+ this.requestMessagesCache = null;
2921
+ this.historyTokenSnapshot = null;
2922
+ this.opts.registry?.resetVolatileState();
2923
+ }
2766
2924
  async loadMessages(messages) {
2767
2925
  this.messages = reconcileStarMessages(messages.map(toStarMessage));
2768
2926
  this.turnMarkers = [];
2769
2927
  this.redoStack = [];
2770
- this.subagentUsage = null;
2771
- this.serverTokenBaseline = null;
2772
- this.opts.registry?.resetVolatileState();
2928
+ this.resetWholesaleHistoryState();
2773
2929
  }
2774
2930
  // Read-only counterpart of retractLastTurn: how many messages /undo would
2775
2931
  // drop, which snapshot turn's file changes it would revert (undefined =
@@ -2803,8 +2959,8 @@ var AgentLoop = class {
2803
2959
  REDO_LABEL_MAX
2804
2960
  );
2805
2961
  this.messages = result.messages;
2806
- await this.opts.sessionStore?.replaceMessages([...this.messages]);
2807
- this.opts.registry?.resetVolatileState();
2962
+ await this.replacePersisted([...this.messages]);
2963
+ this.resetWholesaleHistoryState();
2808
2964
  const last = this.turnMarkers[this.turnMarkers.length - 1];
2809
2965
  let turn;
2810
2966
  let tree;
@@ -2887,8 +3043,8 @@ var AgentLoop = class {
2887
3043
  const removed = this.messages.length - cut;
2888
3044
  if (removed === 0) return 0;
2889
3045
  this.messages = this.messages.slice(0, cut);
2890
- await this.opts.sessionStore?.replaceMessages([...this.messages]);
2891
- this.opts.registry?.resetVolatileState();
3046
+ await this.replacePersisted([...this.messages]);
3047
+ this.resetWholesaleHistoryState();
2892
3048
  this.turnMarkers = this.turnMarkers.filter((m) => m.userIndex < cut);
2893
3049
  this.redoStack = [];
2894
3050
  return removed;
@@ -2899,7 +3055,18 @@ var AgentLoop = class {
2899
3055
  await this.persist(message);
2900
3056
  }
2901
3057
  async persist(message, meta) {
2902
- await this.opts.sessionStore?.append(message, meta);
3058
+ const store = this.opts.sessionStore;
3059
+ if (!store) return;
3060
+ if (!await store.append(message, meta) && !await store.recoverMessages()) {
3061
+ throw new Error(`Session ${store.id} could not persist the message history`);
3062
+ }
3063
+ }
3064
+ async replacePersisted(messages) {
3065
+ const store = this.opts.sessionStore;
3066
+ if (!store) return;
3067
+ if (!await store.replaceMessages(messages) && !await store.recoverMessages()) {
3068
+ throw new Error(`Session ${store.id} could not persist the rewritten history`);
3069
+ }
2903
3070
  }
2904
3071
  // Replaces oversized image parts in the in-memory history with text
2905
3072
  // placeholders and rewrites the persisted session, so the failed request
@@ -2911,7 +3078,8 @@ var AgentLoop = class {
2911
3078
  message: messages[i] ?? star.message,
2912
3079
  meta: star.meta
2913
3080
  }));
2914
- await this.opts.sessionStore?.replaceMessages(this.messages);
3081
+ await this.replacePersisted(this.messages);
3082
+ this.resetWholesaleHistoryState();
2915
3083
  return removed;
2916
3084
  }
2917
3085
  // Same rewrite as above, but for text-only endpoints that reject any image
@@ -2924,7 +3092,8 @@ var AgentLoop = class {
2924
3092
  message: messages[i] ?? star.message,
2925
3093
  meta: star.meta
2926
3094
  }));
2927
- await this.opts.sessionStore?.replaceMessages(this.messages);
3095
+ await this.replacePersisted(this.messages);
3096
+ this.resetWholesaleHistoryState();
2928
3097
  return removed;
2929
3098
  }
2930
3099
  // Background title generation runs once per loop, fed by the first real
@@ -2940,7 +3109,12 @@ var AgentLoop = class {
2940
3109
  const input = this.titleInput;
2941
3110
  if (!input) return;
2942
3111
  this.titleScheduled = true;
2943
- scheduleSessionTitle(store, input, this.getAuxModel());
3112
+ scheduleSessionTitle(
3113
+ store,
3114
+ input,
3115
+ this.getAuxModel(),
3116
+ (usage) => this.recordUsage(usage, this.getAuxModelName(), store)
3117
+ );
2944
3118
  }
2945
3119
  // A user interrupt (Esc) ends the turn mid-stream, before the normal
2946
3120
  // assistant-message persistence runs. Whatever text and tool calls already
@@ -2978,8 +3152,7 @@ var AgentLoop = class {
2978
3152
  ]
2979
3153
  };
2980
3154
  this.messages.push({ message: synthetic });
2981
- await this.persist(synthetic).catch(() => {
2982
- });
3155
+ await this.persist(synthetic);
2983
3156
  yield { type: "tool-result", id: call.id, name: call.name, content, isError: true };
2984
3157
  }
2985
3158
  }
@@ -3039,6 +3212,7 @@ var AgentLoop = class {
3039
3212
  let autoContinues = 0;
3040
3213
  let lastWasNudge = false;
3041
3214
  let usedToolsThisTurn = false;
3215
+ const toolOutcomes = [];
3042
3216
  let streamCutContinues = 0;
3043
3217
  let openTodos = [];
3044
3218
  let todoRevision = this.opts.registry.todos.revision;
@@ -3111,24 +3285,57 @@ ${finished.result}`
3111
3285
  }
3112
3286
  }
3113
3287
  const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
3114
- const threshold = resolveCompactThreshold(config, maxTokens);
3288
+ const outputReserve = Math.min(this.opts.maxTokens ?? 4096, Math.floor(maxTokens / 20));
3289
+ const requestBudget = Math.max(1, maxTokens - outputReserve);
3290
+ const threshold = Math.min(resolveCompactThreshold(config, maxTokens), requestBudget);
3115
3291
  const calibration = this.calibrationDelta();
3116
- const elided = elideStaleContent(this.messages, threshold, toolSchemaTokens + calibration);
3292
+ const elided = elideStaleContent(this.messages, threshold, toolSchemaTokens + calibration, {
3293
+ activationFraction: config.elisionThresholdFraction,
3294
+ keepRecentMessages: config.elisionKeepRecentMessages,
3295
+ minTokens: config.elisionMinTokens
3296
+ });
3117
3297
  if (elided) {
3118
3298
  this.messages = elided.messages;
3119
- await this.opts.sessionStore?.replaceMessages([...this.messages]);
3120
- this.opts.registry?.resetVolatileState();
3299
+ await this.replacePersisted([...this.messages]);
3300
+ this.resetWholesaleHistoryState();
3121
3301
  }
3122
3302
  const compacted = compactMessages(this.messages, threshold, {
3123
3303
  overheadTokens: toolSchemaTokens + calibration
3124
3304
  });
3125
3305
  if (compacted.compacted) {
3126
3306
  this.messages = await this.applyCompactionSummary(compacted, signal);
3127
- await this.opts.sessionStore?.replaceMessages([...this.messages]);
3307
+ await this.replacePersisted([...this.messages]);
3128
3308
  this.turnMarkers = [];
3129
- this.opts.registry?.resetVolatileState();
3309
+ this.resetWholesaleHistoryState();
3310
+ }
3311
+ if (this.historyTokens() + toolSchemaTokens + calibration > requestBudget) {
3312
+ const emergency = emergencyElideContent(
3313
+ this.messages,
3314
+ requestBudget,
3315
+ toolSchemaTokens + calibration
3316
+ );
3317
+ if (emergency.elidedCount > 0) {
3318
+ this.messages = emergency.messages;
3319
+ await this.replacePersisted([...this.messages]);
3320
+ this.resetWholesaleHistoryState();
3321
+ yield {
3322
+ type: "notice",
3323
+ message: `Emergency context reduction elided ${emergency.elidedCount} oversized message(s).`
3324
+ };
3325
+ }
3326
+ if (!emergency.withinBudget) {
3327
+ yield {
3328
+ type: "error",
3329
+ error: {
3330
+ name: "ContextLimitError",
3331
+ message: `The request still exceeds the context window after compaction and emergency elision (${emergency.estimatedTokens}/${requestBudget} estimated tokens). Start a new session or use a model with a larger context window.`
3332
+ }
3333
+ };
3334
+ await this.runStopHooks("error");
3335
+ return;
3336
+ }
3130
3337
  }
3131
- const requestLocalTokens = estimateTokens(this.getMessages()) + toolSchemaTokens;
3338
+ const requestLocalTokens = this.historyTokens() + toolSchemaTokens;
3132
3339
  const maxRetries = Math.max(0, config.streamMaxRetries);
3133
3340
  const baseDelay = this.opts.retryDelayMs ?? STREAM_RETRY_BASE_DELAY_MS;
3134
3341
  let text = "";
@@ -3180,7 +3387,8 @@ ${finished.result}`
3180
3387
  serverTokens: event.usage.promptTokens
3181
3388
  };
3182
3389
  }
3183
- yield this.foldSubagentUsage(event);
3390
+ if (event.usage) this.recordUsage(event.usage);
3391
+ yield event;
3184
3392
  } else if (event.type === "error") {
3185
3393
  failure = event.error;
3186
3394
  } else {
@@ -3240,6 +3448,7 @@ ${finished.result}`
3240
3448
  }
3241
3449
  }
3242
3450
  if (failure) {
3451
+ yield* this.persistInterrupted(text, toolCalls);
3243
3452
  yield { type: "error", error: failure };
3244
3453
  await this.runStopHooks("error");
3245
3454
  return;
@@ -3338,8 +3547,10 @@ ${finished.result}`
3338
3547
  this.getAuxModel(),
3339
3548
  inputText,
3340
3549
  text,
3550
+ toolOutcomes.join("\n"),
3341
3551
  signal,
3342
- this.getAuxTemperature()
3552
+ this.getAuxTemperature(),
3553
+ (usage) => this.recordUsage(usage, this.getAuxModelName())
3343
3554
  );
3344
3555
  if (complete === false) {
3345
3556
  reason = "completion check reports the task unfinished";
@@ -3434,6 +3645,10 @@ Reissue ${call.name} with valid arguments.` : this.doomLoopRefusal(call);
3434
3645
  if (call.name === "todo_write" && !result.isError) {
3435
3646
  openTodos = pendingTodoTitles(call.args);
3436
3647
  }
3648
+ toolOutcomes.push(
3649
+ `${call.name} (${result.isError ? "failed" : "succeeded"}): ${result.content.slice(0, 500)}`
3650
+ );
3651
+ if (toolOutcomes.length > 20) toolOutcomes.shift();
3437
3652
  yield {
3438
3653
  type: "tool-result",
3439
3654
  id: call.id,
@@ -3458,8 +3673,7 @@ Reissue ${call.name} with valid arguments.` : this.doomLoopRefusal(call);
3458
3673
  ]
3459
3674
  };
3460
3675
  this.messages.push({ message: synthetic });
3461
- await this.persist(synthetic).catch(() => {
3462
- });
3676
+ await this.persist(synthetic);
3463
3677
  yield { type: "tool-result", id: call.id, name: call.name, content, isError: true };
3464
3678
  }
3465
3679
  for (const { message, meta } of pendingImages) {
@@ -3521,7 +3735,8 @@ Reissue ${call.name} with valid arguments.` : this.doomLoopRefusal(call);
3521
3735
  dropped,
3522
3736
  this.getAuxModel(),
3523
3737
  signal,
3524
- this.getAuxTemperature()
3738
+ this.getAuxTemperature(),
3739
+ (usage) => this.recordUsage(usage, this.getAuxModelName())
3525
3740
  );
3526
3741
  if (summary.trim().length === 0) return compacted.messages;
3527
3742
  const messages = compacted.messages.slice();
@@ -3546,7 +3761,8 @@ ${summary}`
3546
3761
  providerMetadata: this.opts.providerMetadata,
3547
3762
  idleTimeoutMs: (this.opts.streamIdleTimeoutSec ?? this.opts.config.streamIdleTimeoutSec) * 1e3 * timeoutScale,
3548
3763
  firstPartTimeoutMs: (this.opts.streamFirstChunkTimeoutSec ?? this.opts.config.streamFirstChunkTimeoutSec) * 1e3 * timeoutScale,
3549
- temperature: this.opts.temperature
3764
+ temperature: this.opts.temperature,
3765
+ maxTokens: this.opts.maxTokens
3550
3766
  })) {
3551
3767
  if (event.type === "finish" && event.truncated) {
3552
3768
  yield {
@@ -3632,53 +3848,6 @@ ${summary}`
3632
3848
  return empty;
3633
3849
  }
3634
3850
  }
3635
- // Accumulates a settled subagent's usage. When the child provably ran on a
3636
- // different, fully priced model, the counts are first converted into
3637
- // this-model-priced equivalents (convertUsagePricing): session cost is
3638
- // token totals × the active model's price, so the conversion keeps the $
3639
- // estimate accurate where raw sums would misprice. Price lookup goes by
3640
- // the [[models]] entry names both loops were resolved from, not model id.
3641
- addSubagentUsage(usage, childModel, childModelName) {
3642
- const converted = convertUsagePricing(usage, childModel, this.opts.model, this.opts.config, {
3643
- fromName: childModelName,
3644
- toName: this.opts.modelName
3645
- });
3646
- const pending = this.subagentUsage ?? {
3647
- promptTokens: 0,
3648
- completionTokens: 0,
3649
- totalTokens: 0
3650
- };
3651
- pending.promptTokens += converted.promptTokens;
3652
- pending.completionTokens += converted.completionTokens;
3653
- pending.totalTokens += usageTotalTokens(converted);
3654
- if (converted.cachedPromptTokens) {
3655
- pending.cachedPromptTokens = (pending.cachedPromptTokens ?? 0) + converted.cachedPromptTokens;
3656
- }
3657
- if (converted.cacheReadInputTokens) {
3658
- pending.cacheReadInputTokens = (pending.cacheReadInputTokens ?? 0) + converted.cacheReadInputTokens;
3659
- }
3660
- this.subagentUsage = pending;
3661
- }
3662
- // Folds pending subagent usage into an outgoing finish event (and clears
3663
- // it). A finish without usage reports nothing to consumers, so the pending
3664
- // amount is kept for the next finish that carries usage instead. Totals
3665
- // sum the billed classes (usageTotalTokens), not whatever extra classes a
3666
- // provider folded into its reported totalTokens.
3667
- foldSubagentUsage(event) {
3668
- const pending = this.subagentUsage;
3669
- if (!pending || !event.usage) return event;
3670
- this.subagentUsage = null;
3671
- const usage = {
3672
- promptTokens: event.usage.promptTokens + pending.promptTokens,
3673
- completionTokens: event.usage.completionTokens + pending.completionTokens,
3674
- totalTokens: usageTotalTokens(event.usage) + usageTotalTokens(pending)
3675
- };
3676
- const cachedPromptTokens = (event.usage.cachedPromptTokens ?? 0) + (pending.cachedPromptTokens ?? 0);
3677
- const cacheReadInputTokens = (event.usage.cacheReadInputTokens ?? 0) + (pending.cacheReadInputTokens ?? 0);
3678
- if (cachedPromptTokens > 0) usage.cachedPromptTokens = cachedPromptTokens;
3679
- if (cacheReadInputTokens > 0) usage.cacheReadInputTokens = cacheReadInputTokens;
3680
- return { ...event, usage };
3681
- }
3682
3851
  // opencode's doom_loop guard: returns a refusal once the identical tool
3683
3852
  // call (name + stable-serialized args) has repeated config.doomLoopThreshold
3684
3853
  // times in a row; 0 disables. Any different signature resets the streak.
@@ -3774,7 +3943,7 @@ export {
3774
3943
  compactMessages,
3775
3944
  WORKING_DIFF_MAX_LINES,
3776
3945
  isGitRepo,
3777
- getGitSummaryCached,
3946
+ getGitSummaryCachedAsync,
3778
3947
  collectCommitContext,
3779
3948
  buildCommitPrompt,
3780
3949
  collectWorkingDiff,
@@ -3787,4 +3956,4 @@ export {
3787
3956
  attachTurnContext,
3788
3957
  AgentLoop
3789
3958
  };
3790
- //# sourceMappingURL=chunk-QELCMXQX.js.map
3959
+ //# sourceMappingURL=chunk-4HANB3DJ.js.map