@cryer/star-cli 0.3.12 → 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 (33) hide show
  1. package/README.md +17 -5
  2. package/README.zh-CN.md +15 -5
  3. package/dist/{chunk-LSYGHLRU.js → chunk-2ISBRSBA.js} +479 -296
  4. package/dist/chunk-2ISBRSBA.js.map +1 -0
  5. package/dist/{chunk-F5RWZMQY.js → chunk-4HANB3DJ.js} +458 -221
  6. package/dist/chunk-4HANB3DJ.js.map +1 -0
  7. package/dist/chunk-4IRY44TG.js +0 -0
  8. package/dist/{chunk-Q5FJN4LN.js → chunk-54KH4JSO.js} +213 -125
  9. package/dist/chunk-54KH4JSO.js.map +1 -0
  10. package/dist/{chunk-MYHD3SZB.js → chunk-CECMCDZG.js} +43 -30
  11. package/dist/chunk-CECMCDZG.js.map +1 -0
  12. package/dist/{chunk-MVAMPVTA.js → chunk-KILMI3S5.js} +13 -5
  13. package/dist/{chunk-MVAMPVTA.js.map → chunk-KILMI3S5.js.map} +1 -1
  14. package/dist/{chunk-2GAUAYCU.js → chunk-NRLGGA7L.js} +507 -175
  15. package/dist/chunk-NRLGGA7L.js.map +1 -0
  16. package/dist/loop-M64D5YII.js +17 -0
  17. package/dist/main.js +177 -131
  18. package/dist/main.js.map +1 -1
  19. package/dist/{provider-EWMUPBLH.js → provider-3Z4B7RMF.js} +2 -2
  20. package/dist/{repl-SSCURTAH.js → repl-N2H7JDSH.js} +617 -327
  21. package/dist/repl-N2H7JDSH.js.map +1 -0
  22. package/dist/{tools-BH55BVIS.js → tools-5RKM7ZI3.js} +3 -3
  23. package/package.json +16 -25
  24. package/dist/chunk-2GAUAYCU.js.map +0 -1
  25. package/dist/chunk-F5RWZMQY.js.map +0 -1
  26. package/dist/chunk-LSYGHLRU.js.map +0 -1
  27. package/dist/chunk-MYHD3SZB.js.map +0 -1
  28. package/dist/chunk-Q5FJN4LN.js.map +0 -1
  29. package/dist/loop-BYXBNFFY.js +0 -17
  30. package/dist/repl-SSCURTAH.js.map +0 -1
  31. /package/dist/{loop-BYXBNFFY.js.map → loop-M64D5YII.js.map} +0 -0
  32. /package/dist/{provider-EWMUPBLH.js.map → provider-3Z4B7RMF.js.map} +0 -0
  33. /package/dist/{tools-BH55BVIS.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-MYHD3SZB.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-Q5FJN4LN.js";
28
+ } from "./chunk-54KH4JSO.js";
28
29
  import {
29
30
  TodoStore,
30
31
  beginTurn,
@@ -32,6 +33,7 @@ import {
32
33
  defaultAgentTasks,
33
34
  dropTurnSnapshots,
34
35
  isOversizedImageError,
36
+ isSecretEnvName,
35
37
  isSensitivePath,
36
38
  isVisionUnsupportedError,
37
39
  pendingTodoTitles,
@@ -43,7 +45,7 @@ import {
43
45
  stripOversizedImages,
44
46
  undoTurnSnapshots,
45
47
  userMemoryPath
46
- } from "./chunk-LSYGHLRU.js";
48
+ } from "./chunk-2ISBRSBA.js";
47
49
 
48
50
  // src/agent/loop.ts
49
51
  import { tool as aiTool, generateText as generateText3 } from "ai";
@@ -146,6 +148,157 @@ function estimateToolSchemaTokens(tools) {
146
148
  return tokens;
147
149
  }
148
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
+
149
302
  // src/context/compaction.ts
150
303
  var MIN_KEPT_MESSAGES = 4;
151
304
  function placeholderMessage(droppedCount) {
@@ -226,20 +379,22 @@ function serializeSummaryInput(messages) {
226
379
  [... ${total - MAX_SUMMARY_INPUT_CHARS} characters omitted ...]
227
380
  ${tail}`;
228
381
  }
229
- async function summarizeMessages(messages, model, signal, temperature) {
382
+ async function summarizeMessages(messages, model, signal, temperature, onUsage) {
230
383
  const transcript = serializeSummaryInput(messages);
231
- const { text } = await generateText({
384
+ const { text, usage } = await generateText({
232
385
  model,
233
386
  system: SUMMARY_SYSTEM_PROMPT,
234
387
  prompt: `Summarize this conversation so far:
235
388
 
236
389
  ${transcript}`,
237
390
  temperature,
391
+ maxRetries: 0,
238
392
  // A hung relay must not stall the whole turn on the summary: cap it at
239
393
  // 60s and let the caller's abort (Esc) cut it short as well; the caller
240
394
  // falls back to the truncation placeholder on any failure.
241
395
  abortSignal: signal ? AbortSignal.any([signal, AbortSignal.timeout(6e4)]) : AbortSignal.timeout(6e4)
242
396
  });
397
+ onUsage?.(usage);
243
398
  return text.trim();
244
399
  }
245
400
  function compactMessages(messages, maxTokens, opts = {}) {
@@ -297,69 +452,6 @@ function compactMessages(messages, maxTokens, opts = {}) {
297
452
  };
298
453
  }
299
454
 
300
- // src/context/elision.ts
301
- var ACTIVATION_FRACTION = 0.5;
302
- var KEEP_RECENT_MESSAGES = 24;
303
- var MIN_ELISION_TOKENS = 250;
304
- 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]";
305
- var ELIDED_IMAGE_TEXT = "[image elided to free context \u2014 re-attach it if it is still needed]";
306
- 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";
307
- function elideStaleContent(messages, thresholdTokens, overheadTokens = 0) {
308
- let total = overheadTokens;
309
- for (const star of messages) total += estimateMessageTokens(star.message);
310
- if (total < thresholdTokens * ACTIVATION_FRACTION) return null;
311
- const cutoff = messages.length - KEEP_RECENT_MESSAGES;
312
- if (cutoff <= 0) return null;
313
- let elidedCount = 0;
314
- const out = messages.slice();
315
- for (let i = 0; i < cutoff; i++) {
316
- const star = out[i];
317
- if (!star || star.meta?.elided) continue;
318
- const message = star.message;
319
- if (message.role === "tool") {
320
- if (estimateMessageTokens(message) < MIN_ELISION_TOKENS) continue;
321
- out[i] = {
322
- message: {
323
- ...message,
324
- content: message.content.map((part) => ({
325
- ...part,
326
- result: ELIDED_TOOL_RESULT_TEXT
327
- }))
328
- },
329
- meta: { ...star.meta, elided: true }
330
- };
331
- elidedCount++;
332
- } else if (message.role === "assistant" && Array.isArray(message.content)) {
333
- if (!message.content.some((part) => part.type === "tool-call")) continue;
334
- if (estimateMessageTokens(message) < MIN_ELISION_TOKENS) continue;
335
- out[i] = {
336
- message: {
337
- ...message,
338
- content: message.content.map(
339
- (part) => part.type === "tool-call" ? { ...part, args: { elided: ELIDED_TOOL_CALL_ARGS_TEXT } } : part
340
- )
341
- },
342
- meta: { ...star.meta, elided: true }
343
- };
344
- elidedCount++;
345
- } else if (message.role === "user" && Array.isArray(message.content)) {
346
- if (!message.content.some((part) => part.type === "image" || part.type === "file")) continue;
347
- out[i] = {
348
- message: {
349
- ...message,
350
- content: message.content.map(
351
- (part) => part.type === "image" || part.type === "file" ? { type: "text", text: ELIDED_IMAGE_TEXT } : part
352
- )
353
- },
354
- meta: { ...star.meta, elided: true }
355
- };
356
- elidedCount++;
357
- }
358
- }
359
- if (elidedCount === 0) return null;
360
- return { messages: out, elidedCount };
361
- }
362
-
363
455
  // src/core/events.ts
364
456
  function usageTotalTokens(usage) {
365
457
  const summed = usage.promptTokens + usage.completionTokens;
@@ -374,6 +466,19 @@ function toStreamErrorInfo(error) {
374
466
  if (typeof raw.statusCode === "number") info2.statusCode = raw.statusCode;
375
467
  if (typeof raw.status === "number") info2.statusCode = raw.status;
376
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
+ }
377
482
  return info2;
378
483
  }
379
484
  const err = error instanceof Error ? error : new Error(String(error));
@@ -400,7 +505,7 @@ function toStreamErrorInfo(error) {
400
505
  }
401
506
 
402
507
  // src/core/git.ts
403
- import { execFileSync } from "child_process";
508
+ import { execFile, execFileSync } from "child_process";
404
509
  var GIT_TIMEOUT_MS = 2e3;
405
510
  var GIT_MAX_BUFFER = 8 * 1024 * 1024;
406
511
  var COMMIT_DIFF_MAX_LINES = 2e3;
@@ -440,6 +545,47 @@ function getGitSummaryCached(cwd) {
440
545
  gitSummaryCache.set(cwd, { at: now, summary });
441
546
  return summary;
442
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
+ }
443
589
  function formatGitSummary(summary) {
444
590
  const lines = [
445
591
  "Git context for the working directory (refreshed each turn):",
@@ -574,6 +720,10 @@ function runCommand(hook, event, ctx, timeoutSec) {
574
720
  child.stdin.on("error", () => {
575
721
  });
576
722
  child.stdin.end(inputJson);
723
+ } else {
724
+ child.stdin?.on("error", () => {
725
+ });
726
+ child.stdin?.end();
577
727
  }
578
728
  let stderr = "";
579
729
  const stderrDecoder = new StringDecoder("utf8");
@@ -674,7 +824,9 @@ var NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
674
824
  "EPIPE",
675
825
  "EAI_AGAIN",
676
826
  "ENOTFOUND",
677
- "ECONNABORTED"
827
+ "ECONNABORTED",
828
+ "EHOSTUNREACH",
829
+ "ENETUNREACH"
678
830
  ]);
679
831
  function isNetworkFailure(error) {
680
832
  for (const code of [error.code, error.causeCode]) {
@@ -1088,7 +1240,16 @@ function lexShell(command) {
1088
1240
  while (end < command.length && /[A-Za-z0-9_]/.test(command[end] ?? "")) {
1089
1241
  end += 1;
1090
1242
  }
1091
- word += command.slice(pos, end);
1243
+ let name = command.slice(pos + 1, end);
1244
+ if (name.toLowerCase() === "env" && command[end] === ":" && /[A-Za-z_]/.test(command[end + 1] ?? "")) {
1245
+ let varEnd = end + 1;
1246
+ while (varEnd < command.length && /[A-Za-z0-9_]/.test(command[varEnd] ?? "")) {
1247
+ varEnd += 1;
1248
+ }
1249
+ name = command.slice(end + 1, varEnd);
1250
+ end = varEnd;
1251
+ }
1252
+ word += `$${name}`;
1092
1253
  return end;
1093
1254
  }
1094
1255
  word += "$";
@@ -1101,8 +1262,9 @@ function lexShell(command) {
1101
1262
  doubleQuoted = false;
1102
1263
  i += 1;
1103
1264
  } else if (c === "\\") {
1104
- if (i + 1 < command.length) {
1105
- word += command[i + 1];
1265
+ const nextCh = command[i + 1];
1266
+ if (nextCh !== void 0 && (nextCh === "$" || nextCh === "`" || nextCh === '"' || nextCh === "\\")) {
1267
+ word += nextCh;
1106
1268
  i += 2;
1107
1269
  } else {
1108
1270
  word += "\\";
@@ -1213,6 +1375,7 @@ function splitShellSegments(tokens) {
1213
1375
  }
1214
1376
  return segments;
1215
1377
  }
1378
+ var DYNAMIC_SHELL_WORD = "\0star-dynamic-word\0";
1216
1379
  function segmentWords(segment) {
1217
1380
  const words = [];
1218
1381
  const redirectTargets = [];
@@ -1223,11 +1386,16 @@ function segmentWords(segment) {
1223
1386
  if (target?.kind === "word") {
1224
1387
  redirectTargets.push(target.text);
1225
1388
  k += 1;
1389
+ } else if (target?.kind === "subst") {
1390
+ redirectTargets.push(DYNAMIC_SHELL_WORD);
1391
+ k += 1;
1226
1392
  }
1227
1393
  continue;
1228
1394
  }
1229
1395
  if (token.kind === "word") {
1230
1396
  words.push(token.text);
1397
+ } else if (token.kind === "subst") {
1398
+ words.push(DYNAMIC_SHELL_WORD);
1231
1399
  }
1232
1400
  }
1233
1401
  return { words, redirectTargets };
@@ -1362,25 +1530,67 @@ var EGRESS_COMMANDS = /* @__PURE__ */ new Set([
1362
1530
  "scp",
1363
1531
  "sftp",
1364
1532
  "ftp",
1365
- "telnet"
1533
+ "telnet",
1534
+ "dig",
1535
+ "nslookup",
1536
+ "host",
1537
+ "certutil",
1538
+ "bitsadmin",
1539
+ "iwr",
1540
+ "irm",
1541
+ "invoke-webrequest",
1542
+ "invoke-restmethod"
1366
1543
  ]);
1367
1544
  var BLOCK_DEVICE = /^\/dev\/(?:sd[a-z]+\d*|xvd[a-z]+\d*|vd[a-z]+\d*|hd[a-z]+\d*|nvme\d+n\d+(?:p\d+)?|mmcblk\d+(?:p\d+)?)$/i;
1368
1545
  function baseName(word) {
1369
1546
  const base = word.replace(/\\/g, "/").split("/").pop() ?? word;
1370
1547
  return base.toLowerCase().replace(/\.exe$/, "");
1371
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
+ }
1372
1552
  function isDangerousDeleteTarget(target) {
1553
+ if (isDynamicTarget(target)) return true;
1373
1554
  if (/^\/+$/.test(target)) return true;
1374
1555
  const norm = target.replace(/\\/g, "/").replace(/\/+$/, "");
1375
1556
  if (norm === "/*" || norm === "*") return true;
1376
1557
  if (norm === "~" || norm.startsWith("~/") || /^~[^/]/.test(norm)) return true;
1377
1558
  if (norm === "." || norm === "./*") return true;
1378
1559
  if (/^[a-zA-Z]:$/.test(norm)) return true;
1379
- const homeVars = ["$HOME", "$USERPROFILE", "$HOMEPATH"];
1380
- if (homeVars.includes(norm)) return true;
1381
- if (homeVars.some((v) => norm.startsWith(`${v}/`))) return true;
1382
- if (/^%(USERPROFILE|HOMEDRIVE|HOMEPATH)%$/i.test(norm)) return true;
1383
- if (/^%(USERPROFILE|HOMEPATH)%\//i.test(norm)) return true;
1560
+ if (/^\/[a-zA-Z]$/.test(norm)) return true;
1561
+ const upper = norm.toUpperCase();
1562
+ const homeVars = [
1563
+ "$HOME",
1564
+ "$USERPROFILE",
1565
+ "$HOMEPATH",
1566
+ "$APPDATA",
1567
+ "$LOCALAPPDATA",
1568
+ "$PROGRAMDATA",
1569
+ "$PROGRAMFILES",
1570
+ "$SYSTEMROOT"
1571
+ ];
1572
+ if (homeVars.includes(upper)) return true;
1573
+ if (homeVars.some((v) => upper.startsWith(`${v}/`))) return true;
1574
+ if (/^%(USERPROFILE|HOMEDRIVE|HOMEPATH|APPDATA|LOCALAPPDATA|SYSTEMROOT|PROGRAMDATA|PROGRAMFILES|PROGRAMFILES\(X86\))%$/i.test(
1575
+ norm
1576
+ ))
1577
+ return true;
1578
+ if (/^%(USERPROFILE|HOMEPATH|APPDATA|LOCALAPPDATA|SYSTEMROOT|PROGRAMDATA|PROGRAMFILES|PROGRAMFILES\(X86\))%\//i.test(
1579
+ norm
1580
+ ))
1581
+ return true;
1582
+ const win = /^(?:[a-zA-Z]:|\/[a-zA-Z])\/(.+)$/.exec(norm);
1583
+ if (win) {
1584
+ const rest = (win[1] ?? "").toLowerCase();
1585
+ if (/^(?:windows|program files(?: \(x86\))?|programdata|boot|recovery|system volume information)(?:\/|$)/.test(
1586
+ rest
1587
+ )) {
1588
+ return true;
1589
+ }
1590
+ if (/^users(?:\/[^/]+)?$/.test(rest)) return true;
1591
+ }
1592
+ if (/^\/(?:etc|usr|bin|sbin|lib|lib64|boot|opt|var|srv|sys|proc|dev)$/.test(norm)) return true;
1593
+ if (/^\/(?:home|root)(?:\/[^/]+)?$/.test(norm)) return true;
1384
1594
  return false;
1385
1595
  }
1386
1596
  function rmIsDangerous(args) {
@@ -1614,7 +1824,7 @@ function decodePowerShellEncoded(encoded) {
1614
1824
  }
1615
1825
  function segmentIsDangerous(segment, depth, lex) {
1616
1826
  const { words, redirectTargets } = segmentWords(segment);
1617
- if (redirectTargets.some((target) => BLOCK_DEVICE.test(target))) {
1827
+ if (redirectTargets.some((target) => isDynamicTarget(target) || BLOCK_DEVICE.test(target))) {
1618
1828
  return true;
1619
1829
  }
1620
1830
  const invocation = commandInvocation(words);
@@ -1656,10 +1866,10 @@ function segmentIsDangerous(segment, depth, lex) {
1656
1866
  return recursive && targets.some(isDangerousDeleteTarget);
1657
1867
  }
1658
1868
  case "format":
1659
- return rest.some((arg) => /^[a-z]:$/i.test(arg));
1869
+ return rest.some((arg) => isDynamicTarget(arg) || /^[a-z]:$/i.test(arg));
1660
1870
  case "dd":
1661
1871
  return rest.some(
1662
- (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)))
1663
1873
  );
1664
1874
  case "find":
1665
1875
  return findIsDangerous(rest, depth, lex);
@@ -1740,6 +1950,10 @@ function tokenTouchesSensitivePath(word) {
1740
1950
  }
1741
1951
  return isSensitivePath(word);
1742
1952
  }
1953
+ function tokenReferencesSecretEnv(word) {
1954
+ const m = /\$([A-Za-z_][A-Za-z0-9_]*)/.exec(word);
1955
+ return m !== null && isSecretEnvName(m[1]);
1956
+ }
1743
1957
  function bashHygiene(command, lex) {
1744
1958
  let sensitive = false;
1745
1959
  let egress = false;
@@ -1751,7 +1965,7 @@ function bashHygiene(command, lex) {
1751
1965
  for (const segment of splitShellSegments(tokens)) {
1752
1966
  const { words, redirectTargets } = segmentWords(segment);
1753
1967
  for (const word of [...words, ...redirectTargets]) {
1754
- if (tokenTouchesSensitivePath(word)) sensitive = true;
1968
+ if (tokenTouchesSensitivePath(word) || tokenReferencesSecretEnv(word)) sensitive = true;
1755
1969
  }
1756
1970
  const invocation = commandInvocation(words);
1757
1971
  if (!invocation) continue;
@@ -1765,6 +1979,20 @@ function bashHygiene(command, lex) {
1765
1979
  script = idx >= 0 ? invocation.rest.slice(idx + 1).join(" ") : void 0;
1766
1980
  } else if (invocation.name === "eval" && invocation.rest.length > 0) {
1767
1981
  script = invocation.rest.join(" ");
1982
+ } else if (invocation.name === "powershell" || invocation.name === "pwsh") {
1983
+ for (let k = 0; k < invocation.rest.length; k += 1) {
1984
+ const arg = invocation.rest[k];
1985
+ if (/^-(c|command)$/i.test(arg)) {
1986
+ const joined = invocation.rest.slice(k + 1).join(" ");
1987
+ script = joined.length > 0 ? joined : void 0;
1988
+ break;
1989
+ }
1990
+ if (/^-(enc|encodedcommand)$/i.test(arg)) {
1991
+ const encoded = invocation.rest[k + 1];
1992
+ script = encoded === void 0 ? void 0 : decodePowerShellEncoded(encoded) ?? void 0;
1993
+ break;
1994
+ }
1995
+ }
1768
1996
  }
1769
1997
  if (script !== void 0) visit(lexCached(lex, script), depth + 1);
1770
1998
  }
@@ -1940,21 +2168,23 @@ function sanitizeTitle(raw) {
1940
2168
  } while (title !== previous);
1941
2169
  return title.slice(0, TITLE_MAX_CHARS).trim();
1942
2170
  }
1943
- async function generateSessionTitle(userText, model) {
1944
- const { text } = await generateText2({
2171
+ async function generateSessionTitle(userText, model, onUsage) {
2172
+ const { text, usage } = await generateText2({
1945
2173
  model,
1946
2174
  system: TITLE_SYSTEM_PROMPT,
1947
2175
  prompt: userText.slice(0, TITLE_INPUT_MAX_CHARS),
1948
2176
  maxTokens: TITLE_MAX_TOKENS,
2177
+ maxRetries: 0,
1949
2178
  abortSignal: AbortSignal.timeout(TITLE_TIMEOUT_MS)
1950
2179
  });
2180
+ onUsage?.(usage);
1951
2181
  return sanitizeTitle(text);
1952
2182
  }
1953
- function scheduleSessionTitle(store, userText, model) {
2183
+ function scheduleSessionTitle(store, userText, model, onUsage) {
1954
2184
  void (async () => {
1955
2185
  const meta = await store.meta();
1956
2186
  if (meta.title) return;
1957
- const title = await generateSessionTitle(userText, model);
2187
+ const title = await generateSessionTitle(userText, model, onUsage);
1958
2188
  if (!title) return;
1959
2189
  const current = await store.meta();
1960
2190
  if (current.title) return;
@@ -2123,44 +2353,8 @@ var MAX_SUBAGENT_DEPTH = 1;
2123
2353
  var MAX_REPORT_CHARS = 2e4;
2124
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.";
2125
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.";
2126
- function convertUsagePricing(usage, from, to, config, names) {
2127
- if (from === to) return usage;
2128
- const fromCfg = findModelPricing(config, names?.fromName, from.modelId);
2129
- const toCfg = findModelPricing(config, names?.toName, to.modelId);
2130
- if (fromCfg?.promptPrice === void 0 || fromCfg.completionPrice === void 0 || toCfg?.promptPrice === void 0 || toCfg.completionPrice === void 0 || toCfg.promptPrice <= 0 || toCfg.completionPrice <= 0) {
2131
- return usage;
2132
- }
2133
- const cached = Math.min(Math.max(usage.cachedPromptTokens ?? 0, 0), usage.promptTokens);
2134
- const promptUsd = (usage.promptTokens - cached) * fromCfg.promptPrice;
2135
- const cachedUsd = cached * (fromCfg.cacheReadPrice ?? fromCfg.promptPrice);
2136
- const cacheReadUsd = (usage.cacheReadInputTokens ?? 0) * (fromCfg.cacheReadPrice ?? 0);
2137
- const completionUsd = usage.completionTokens * fromCfg.completionPrice;
2138
- const scaledCached = cachedUsd / (toCfg.cacheReadPrice ?? toCfg.promptPrice);
2139
- const promptTokens = Math.round(promptUsd / toCfg.promptPrice + scaledCached);
2140
- const cachedPromptTokens = Math.round(scaledCached);
2141
- const toCacheReadPrice = toCfg.cacheReadPrice !== void 0 && toCfg.cacheReadPrice > 0 ? toCfg.cacheReadPrice : void 0;
2142
- const completionTokens = Math.round(
2143
- completionUsd / toCfg.completionPrice + (toCacheReadPrice === void 0 ? cacheReadUsd / toCfg.completionPrice : 0)
2144
- );
2145
- const cacheReadInputTokens = toCacheReadPrice === void 0 ? 0 : Math.round(cacheReadUsd / toCacheReadPrice);
2146
- const result = {
2147
- promptTokens,
2148
- completionTokens,
2149
- totalTokens: promptTokens + completionTokens
2150
- };
2151
- if (cachedPromptTokens > 0) result.cachedPromptTokens = cachedPromptTokens;
2152
- if (cacheReadInputTokens > 0) result.cacheReadInputTokens = cacheReadInputTokens;
2153
- return result;
2154
- }
2155
- function findModelPricing(config, name, modelId) {
2156
- if (name !== void 0) {
2157
- const byName = config.models.find((m) => m.name === name);
2158
- if (byName) return byName;
2159
- }
2160
- return config.models.find((m) => m.model === modelId);
2161
- }
2162
2356
  async function runSubagent(deps, args, cwd, signal, agentIdRef) {
2163
- const { AgentLoop: AgentLoop2 } = await import("./loop-BYXBNFFY.js");
2357
+ const { AgentLoop: AgentLoop2 } = await import("./loop-M64D5YII.js");
2164
2358
  const child = new AgentLoop2({
2165
2359
  model: deps.model,
2166
2360
  registry: createDefaultRegistry(),
@@ -2175,6 +2369,7 @@ ${READ_ONLY_PROMPT}` : ""}` : `${SUBAGENT_PROMPT}${args.read_only ? `
2175
2369
  ${READ_ONLY_PROMPT}` : ""}`,
2176
2370
  providerMetadata: deps.providerMetadata,
2177
2371
  temperature: deps.temperature,
2372
+ maxTokens: deps.maxTokens,
2178
2373
  streamIdleTimeoutSec: deps.streamIdleTimeoutSec,
2179
2374
  streamFirstChunkTimeoutSec: deps.streamFirstChunkTimeoutSec,
2180
2375
  vision: deps.vision,
@@ -2376,15 +2571,16 @@ ${context}
2376
2571
  }
2377
2572
  return messages;
2378
2573
  }
2379
- async function checkTaskComplete(model, request, finalReply, signal, temperature) {
2574
+ async function checkTaskComplete(model, request, finalReply, toolSummary, signal, temperature, onUsage) {
2380
2575
  const controller = new AbortController();
2381
2576
  const timer = setTimeout(() => controller.abort(), COMPLETION_CHECK_TIMEOUT_MS);
2382
2577
  const onAbort = () => controller.abort();
2383
2578
  signal.addEventListener("abort", onAbort, { once: true });
2384
2579
  try {
2385
- const { text } = await generateText3({
2580
+ const { text, usage } = await generateText3({
2386
2581
  model,
2387
2582
  maxTokens: 8,
2583
+ maxRetries: 0,
2388
2584
  temperature,
2389
2585
  abortSignal: controller.signal,
2390
2586
  prompt: [
@@ -2396,9 +2592,14 @@ ${request.slice(0, 2e3)}
2396
2592
  `<reply>
2397
2593
  ${finalReply.slice(0, 2e3)}
2398
2594
  </reply>`,
2399
- "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."
2400
2600
  ].join("\n")
2401
2601
  });
2602
+ onUsage(usage);
2402
2603
  const verdict = text.trim().toUpperCase();
2403
2604
  if (verdict.startsWith("NOT_DONE")) return false;
2404
2605
  if (verdict.startsWith("DONE")) return true;
@@ -2462,15 +2663,6 @@ var AgentLoop = class {
2462
2663
  // each new user turn restarts the streak.
2463
2664
  lastToolSignature = null;
2464
2665
  repeatedToolCalls = 0;
2465
- // Token usage subagents consumed, waiting to fold into this loop's next
2466
- // finish event with usage: sync children report when their tool call
2467
- // settles, background ones whenever they finish (drained or not).
2468
- // Consumers meter only root-loop finish events, so folding here bills
2469
- // subagent spend with zero consumer changes — /cost, /usage and
2470
- // sessionBudgetUsd all cover it. A background child still running when the
2471
- // session's last finish has passed never reports (best effort, noted in
2472
- // agent-tasks cleanup paths).
2473
- subagentUsage = null;
2474
2666
  // Turn context of the in-flight stream() turn, stamped onto every tool
2475
2667
  // execution so file snapshots attribute root changes to a turn and mark
2476
2668
  // subagent changes as excluded from turn-level retraction.
@@ -2489,6 +2681,7 @@ var AgentLoop = class {
2489
2681
  // turn context string. Saves a full toCoreMessages + attachTurnContext copy
2490
2682
  // per stream attempt, which retried steps used to pay every time.
2491
2683
  requestMessagesCache = null;
2684
+ historyTokenSnapshot = null;
2492
2685
  // Memoized per (registry, plan flag): tools register on the registry only
2493
2686
  // at loop construction, and the plan filter re-reads the live permission
2494
2687
  // mode on every call — so a built map stays valid until either input
@@ -2531,11 +2724,12 @@ var AgentLoop = class {
2531
2724
  // their stderr surfaces through this callback (REPL system message / stderr
2532
2725
  // in print mode).
2533
2726
  onHookWarning;
2727
+ onUsage;
2534
2728
  constructor(opts) {
2535
2729
  this.opts = opts;
2536
2730
  const depth = opts.subagentDepth ?? 0;
2537
2731
  if (depth > 0) {
2538
- opts.registry = new ToolRegistry(new TodoStore());
2732
+ opts.registry = new ToolRegistry(new TodoStore({ persistence: "memory" }));
2539
2733
  } else {
2540
2734
  setTodoPersistGuard(() => {
2541
2735
  const mode = this.opts.config.permissionMode;
@@ -2551,6 +2745,7 @@ var AgentLoop = class {
2551
2745
  opts.registry?.register(createSkillTool({ cwd: opts.cwd }));
2552
2746
  opts.registry?.register(createRememberTool());
2553
2747
  if (opts.registry && depth < MAX_SUBAGENT_DEPTH) {
2748
+ const subagentSessionStore = opts.sessionStore;
2554
2749
  opts.registry.register(
2555
2750
  createSubagentTool({
2556
2751
  model: opts.model,
@@ -2559,13 +2754,14 @@ var AgentLoop = class {
2559
2754
  system: opts.system,
2560
2755
  providerMetadata: opts.providerMetadata,
2561
2756
  temperature: opts.temperature,
2757
+ maxTokens: opts.maxTokens,
2562
2758
  streamIdleTimeoutSec: opts.streamIdleTimeoutSec,
2563
2759
  streamFirstChunkTimeoutSec: opts.streamFirstChunkTimeoutSec,
2564
2760
  vision: opts.vision,
2565
2761
  depth,
2566
2762
  modelName: opts.modelName,
2567
2763
  getConfirmHandler: () => this.confirmHandler,
2568
- onUsage: (usage, childModel, childModelName) => this.addSubagentUsage(usage, childModel, childModelName)
2764
+ onUsage: (usage, _childModel, childModelName) => this.recordUsage(usage, childModelName, subagentSessionStore)
2569
2765
  })
2570
2766
  );
2571
2767
  }
@@ -2631,8 +2827,25 @@ var AgentLoop = class {
2631
2827
  // budgets deliberately use only the first three (history + schemas +
2632
2828
  // calibration), since in-flight reasoning is never resent and must not
2633
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
+ }
2634
2847
  estimateContextTokens() {
2635
- 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);
2636
2849
  }
2637
2850
  // Auxiliary model for cheap side calls: config.smallModel, resolved once.
2638
2851
  // An unresolvable name/key degrades to the main model instead of breaking
@@ -2657,24 +2870,30 @@ var AgentLoop = class {
2657
2870
  this.resolveAuxModel();
2658
2871
  return this.auxModelValue ? this.auxTemperatureValue : this.opts.temperature;
2659
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
+ }
2660
2886
  // Rebinds the per-call provider metadata (e.g. after the reasoning effort
2661
2887
  // for the current model changed via /model) without recreating the loop.
2662
2888
  setProviderMetadata(metadata) {
2663
2889
  this.opts.providerMetadata = metadata;
2664
2890
  }
2665
2891
  // Swaps the persistence target (/new starts a fresh session mid-REPL).
2666
- // Title generation is re-armed so the new session gets one after its first
2667
- // turn, and snapshot checkpoints now flow to the new store. The redo stack
2668
- // dies with the old session context: its trees describe working-tree states
2669
- // that no longer line up with the new session's conversation. Subagent
2670
- // usage still waiting to fold belongs to the old conversation too — it
2671
- // must not bill the new session's first finish.
2672
2892
  setSessionStore(store) {
2673
2893
  this.opts.sessionStore = store;
2674
2894
  this.titleScheduled = false;
2675
2895
  this.titleInput = void 0;
2676
2896
  this.redoStack = [];
2677
- this.subagentUsage = null;
2678
2897
  if (store) this.bindSnapshotHooks(store);
2679
2898
  }
2680
2899
  bindSnapshotHooks(store) {
@@ -2696,12 +2915,17 @@ var AgentLoop = class {
2696
2915
  onRemove: (ids) => store.removeCheckpoints(ids)
2697
2916
  });
2698
2917
  }
2918
+ resetWholesaleHistoryState() {
2919
+ this.serverTokenBaseline = null;
2920
+ this.requestMessagesCache = null;
2921
+ this.historyTokenSnapshot = null;
2922
+ this.opts.registry?.resetVolatileState();
2923
+ }
2699
2924
  async loadMessages(messages) {
2700
2925
  this.messages = reconcileStarMessages(messages.map(toStarMessage));
2701
2926
  this.turnMarkers = [];
2702
2927
  this.redoStack = [];
2703
- this.subagentUsage = null;
2704
- this.opts.registry?.resetVolatileState();
2928
+ this.resetWholesaleHistoryState();
2705
2929
  }
2706
2930
  // Read-only counterpart of retractLastTurn: how many messages /undo would
2707
2931
  // drop, which snapshot turn's file changes it would revert (undefined =
@@ -2735,8 +2959,8 @@ var AgentLoop = class {
2735
2959
  REDO_LABEL_MAX
2736
2960
  );
2737
2961
  this.messages = result.messages;
2738
- await this.opts.sessionStore?.replaceMessages([...this.messages]);
2739
- this.opts.registry?.resetVolatileState();
2962
+ await this.replacePersisted([...this.messages]);
2963
+ this.resetWholesaleHistoryState();
2740
2964
  const last = this.turnMarkers[this.turnMarkers.length - 1];
2741
2965
  let turn;
2742
2966
  let tree;
@@ -2819,8 +3043,8 @@ var AgentLoop = class {
2819
3043
  const removed = this.messages.length - cut;
2820
3044
  if (removed === 0) return 0;
2821
3045
  this.messages = this.messages.slice(0, cut);
2822
- await this.opts.sessionStore?.replaceMessages([...this.messages]);
2823
- this.opts.registry?.resetVolatileState();
3046
+ await this.replacePersisted([...this.messages]);
3047
+ this.resetWholesaleHistoryState();
2824
3048
  this.turnMarkers = this.turnMarkers.filter((m) => m.userIndex < cut);
2825
3049
  this.redoStack = [];
2826
3050
  return removed;
@@ -2831,7 +3055,18 @@ var AgentLoop = class {
2831
3055
  await this.persist(message);
2832
3056
  }
2833
3057
  async persist(message, meta) {
2834
- 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
+ }
2835
3070
  }
2836
3071
  // Replaces oversized image parts in the in-memory history with text
2837
3072
  // placeholders and rewrites the persisted session, so the failed request
@@ -2843,7 +3078,8 @@ var AgentLoop = class {
2843
3078
  message: messages[i] ?? star.message,
2844
3079
  meta: star.meta
2845
3080
  }));
2846
- await this.opts.sessionStore?.replaceMessages(this.messages);
3081
+ await this.replacePersisted(this.messages);
3082
+ this.resetWholesaleHistoryState();
2847
3083
  return removed;
2848
3084
  }
2849
3085
  // Same rewrite as above, but for text-only endpoints that reject any image
@@ -2856,7 +3092,8 @@ var AgentLoop = class {
2856
3092
  message: messages[i] ?? star.message,
2857
3093
  meta: star.meta
2858
3094
  }));
2859
- await this.opts.sessionStore?.replaceMessages(this.messages);
3095
+ await this.replacePersisted(this.messages);
3096
+ this.resetWholesaleHistoryState();
2860
3097
  return removed;
2861
3098
  }
2862
3099
  // Background title generation runs once per loop, fed by the first real
@@ -2872,7 +3109,12 @@ var AgentLoop = class {
2872
3109
  const input = this.titleInput;
2873
3110
  if (!input) return;
2874
3111
  this.titleScheduled = true;
2875
- scheduleSessionTitle(store, input, this.getAuxModel());
3112
+ scheduleSessionTitle(
3113
+ store,
3114
+ input,
3115
+ this.getAuxModel(),
3116
+ (usage) => this.recordUsage(usage, this.getAuxModelName(), store)
3117
+ );
2876
3118
  }
2877
3119
  // A user interrupt (Esc) ends the turn mid-stream, before the normal
2878
3120
  // assistant-message persistence runs. Whatever text and tool calls already
@@ -2910,8 +3152,7 @@ var AgentLoop = class {
2910
3152
  ]
2911
3153
  };
2912
3154
  this.messages.push({ message: synthetic });
2913
- await this.persist(synthetic).catch(() => {
2914
- });
3155
+ await this.persist(synthetic);
2915
3156
  yield { type: "tool-result", id: call.id, name: call.name, content, isError: true };
2916
3157
  }
2917
3158
  }
@@ -2971,6 +3212,7 @@ var AgentLoop = class {
2971
3212
  let autoContinues = 0;
2972
3213
  let lastWasNudge = false;
2973
3214
  let usedToolsThisTurn = false;
3215
+ const toolOutcomes = [];
2974
3216
  let streamCutContinues = 0;
2975
3217
  let openTodos = [];
2976
3218
  let todoRevision = this.opts.registry.todos.revision;
@@ -3043,24 +3285,57 @@ ${finished.result}`
3043
3285
  }
3044
3286
  }
3045
3287
  const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
3046
- 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);
3047
3291
  const calibration = this.calibrationDelta();
3048
- 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
+ });
3049
3297
  if (elided) {
3050
3298
  this.messages = elided.messages;
3051
- await this.opts.sessionStore?.replaceMessages([...this.messages]);
3052
- this.opts.registry?.resetVolatileState();
3299
+ await this.replacePersisted([...this.messages]);
3300
+ this.resetWholesaleHistoryState();
3053
3301
  }
3054
3302
  const compacted = compactMessages(this.messages, threshold, {
3055
3303
  overheadTokens: toolSchemaTokens + calibration
3056
3304
  });
3057
3305
  if (compacted.compacted) {
3058
3306
  this.messages = await this.applyCompactionSummary(compacted, signal);
3059
- await this.opts.sessionStore?.replaceMessages([...this.messages]);
3307
+ await this.replacePersisted([...this.messages]);
3060
3308
  this.turnMarkers = [];
3061
- this.opts.registry?.resetVolatileState();
3309
+ this.resetWholesaleHistoryState();
3062
3310
  }
3063
- const requestLocalTokens = estimateTokens(this.getMessages()) + toolSchemaTokens;
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
+ }
3337
+ }
3338
+ const requestLocalTokens = this.historyTokens() + toolSchemaTokens;
3064
3339
  const maxRetries = Math.max(0, config.streamMaxRetries);
3065
3340
  const baseDelay = this.opts.retryDelayMs ?? STREAM_RETRY_BASE_DELAY_MS;
3066
3341
  let text = "";
@@ -3112,7 +3387,8 @@ ${finished.result}`
3112
3387
  serverTokens: event.usage.promptTokens
3113
3388
  };
3114
3389
  }
3115
- yield this.foldSubagentUsage(event);
3390
+ if (event.usage) this.recordUsage(event.usage);
3391
+ yield event;
3116
3392
  } else if (event.type === "error") {
3117
3393
  failure = event.error;
3118
3394
  } else {
@@ -3172,6 +3448,7 @@ ${finished.result}`
3172
3448
  }
3173
3449
  }
3174
3450
  if (failure) {
3451
+ yield* this.persistInterrupted(text, toolCalls);
3175
3452
  yield { type: "error", error: failure };
3176
3453
  await this.runStopHooks("error");
3177
3454
  return;
@@ -3270,8 +3547,10 @@ ${finished.result}`
3270
3547
  this.getAuxModel(),
3271
3548
  inputText,
3272
3549
  text,
3550
+ toolOutcomes.join("\n"),
3273
3551
  signal,
3274
- this.getAuxTemperature()
3552
+ this.getAuxTemperature(),
3553
+ (usage) => this.recordUsage(usage, this.getAuxModelName())
3275
3554
  );
3276
3555
  if (complete === false) {
3277
3556
  reason = "completion check reports the task unfinished";
@@ -3366,6 +3645,10 @@ Reissue ${call.name} with valid arguments.` : this.doomLoopRefusal(call);
3366
3645
  if (call.name === "todo_write" && !result.isError) {
3367
3646
  openTodos = pendingTodoTitles(call.args);
3368
3647
  }
3648
+ toolOutcomes.push(
3649
+ `${call.name} (${result.isError ? "failed" : "succeeded"}): ${result.content.slice(0, 500)}`
3650
+ );
3651
+ if (toolOutcomes.length > 20) toolOutcomes.shift();
3369
3652
  yield {
3370
3653
  type: "tool-result",
3371
3654
  id: call.id,
@@ -3390,8 +3673,7 @@ Reissue ${call.name} with valid arguments.` : this.doomLoopRefusal(call);
3390
3673
  ]
3391
3674
  };
3392
3675
  this.messages.push({ message: synthetic });
3393
- await this.persist(synthetic).catch(() => {
3394
- });
3676
+ await this.persist(synthetic);
3395
3677
  yield { type: "tool-result", id: call.id, name: call.name, content, isError: true };
3396
3678
  }
3397
3679
  for (const { message, meta } of pendingImages) {
@@ -3453,7 +3735,8 @@ Reissue ${call.name} with valid arguments.` : this.doomLoopRefusal(call);
3453
3735
  dropped,
3454
3736
  this.getAuxModel(),
3455
3737
  signal,
3456
- this.getAuxTemperature()
3738
+ this.getAuxTemperature(),
3739
+ (usage) => this.recordUsage(usage, this.getAuxModelName())
3457
3740
  );
3458
3741
  if (summary.trim().length === 0) return compacted.messages;
3459
3742
  const messages = compacted.messages.slice();
@@ -3478,7 +3761,8 @@ ${summary}`
3478
3761
  providerMetadata: this.opts.providerMetadata,
3479
3762
  idleTimeoutMs: (this.opts.streamIdleTimeoutSec ?? this.opts.config.streamIdleTimeoutSec) * 1e3 * timeoutScale,
3480
3763
  firstPartTimeoutMs: (this.opts.streamFirstChunkTimeoutSec ?? this.opts.config.streamFirstChunkTimeoutSec) * 1e3 * timeoutScale,
3481
- temperature: this.opts.temperature
3764
+ temperature: this.opts.temperature,
3765
+ maxTokens: this.opts.maxTokens
3482
3766
  })) {
3483
3767
  if (event.type === "finish" && event.truncated) {
3484
3768
  yield {
@@ -3564,53 +3848,6 @@ ${summary}`
3564
3848
  return empty;
3565
3849
  }
3566
3850
  }
3567
- // Accumulates a settled subagent's usage. When the child provably ran on a
3568
- // different, fully priced model, the counts are first converted into
3569
- // this-model-priced equivalents (convertUsagePricing): session cost is
3570
- // token totals × the active model's price, so the conversion keeps the $
3571
- // estimate accurate where raw sums would misprice. Price lookup goes by
3572
- // the [[models]] entry names both loops were resolved from, not model id.
3573
- addSubagentUsage(usage, childModel, childModelName) {
3574
- const converted = convertUsagePricing(usage, childModel, this.opts.model, this.opts.config, {
3575
- fromName: childModelName,
3576
- toName: this.opts.modelName
3577
- });
3578
- const pending = this.subagentUsage ?? {
3579
- promptTokens: 0,
3580
- completionTokens: 0,
3581
- totalTokens: 0
3582
- };
3583
- pending.promptTokens += converted.promptTokens;
3584
- pending.completionTokens += converted.completionTokens;
3585
- pending.totalTokens += usageTotalTokens(converted);
3586
- if (converted.cachedPromptTokens) {
3587
- pending.cachedPromptTokens = (pending.cachedPromptTokens ?? 0) + converted.cachedPromptTokens;
3588
- }
3589
- if (converted.cacheReadInputTokens) {
3590
- pending.cacheReadInputTokens = (pending.cacheReadInputTokens ?? 0) + converted.cacheReadInputTokens;
3591
- }
3592
- this.subagentUsage = pending;
3593
- }
3594
- // Folds pending subagent usage into an outgoing finish event (and clears
3595
- // it). A finish without usage reports nothing to consumers, so the pending
3596
- // amount is kept for the next finish that carries usage instead. Totals
3597
- // sum the billed classes (usageTotalTokens), not whatever extra classes a
3598
- // provider folded into its reported totalTokens.
3599
- foldSubagentUsage(event) {
3600
- const pending = this.subagentUsage;
3601
- if (!pending || !event.usage) return event;
3602
- this.subagentUsage = null;
3603
- const usage = {
3604
- promptTokens: event.usage.promptTokens + pending.promptTokens,
3605
- completionTokens: event.usage.completionTokens + pending.completionTokens,
3606
- totalTokens: usageTotalTokens(event.usage) + usageTotalTokens(pending)
3607
- };
3608
- const cachedPromptTokens = (event.usage.cachedPromptTokens ?? 0) + (pending.cachedPromptTokens ?? 0);
3609
- const cacheReadInputTokens = (event.usage.cacheReadInputTokens ?? 0) + (pending.cacheReadInputTokens ?? 0);
3610
- if (cachedPromptTokens > 0) usage.cachedPromptTokens = cachedPromptTokens;
3611
- if (cacheReadInputTokens > 0) usage.cacheReadInputTokens = cacheReadInputTokens;
3612
- return { ...event, usage };
3613
- }
3614
3851
  // opencode's doom_loop guard: returns a refusal once the identical tool
3615
3852
  // call (name + stable-serialized args) has repeated config.doomLoopThreshold
3616
3853
  // times in a row; 0 disables. Any different signature resets the streak.
@@ -3706,7 +3943,7 @@ export {
3706
3943
  compactMessages,
3707
3944
  WORKING_DIFF_MAX_LINES,
3708
3945
  isGitRepo,
3709
- getGitSummaryCached,
3946
+ getGitSummaryCachedAsync,
3710
3947
  collectCommitContext,
3711
3948
  buildCommitPrompt,
3712
3949
  collectWorkingDiff,
@@ -3719,4 +3956,4 @@ export {
3719
3956
  attachTurnContext,
3720
3957
  AgentLoop
3721
3958
  };
3722
- //# sourceMappingURL=chunk-F5RWZMQY.js.map
3959
+ //# sourceMappingURL=chunk-4HANB3DJ.js.map