@cryer/star-cli 0.3.10 → 0.3.12

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.
@@ -12,7 +12,7 @@ import {
12
12
  toCoreMessages,
13
13
  toStarMessage,
14
14
  trackTree
15
- } from "./chunk-IJGUW4PM.js";
15
+ } from "./chunk-MYHD3SZB.js";
16
16
  import {
17
17
  createModel,
18
18
  streamActivity
@@ -24,7 +24,7 @@ import {
24
24
  ToolRegistry,
25
25
  createDefaultRegistry,
26
26
  debugStreamLog
27
- } from "./chunk-URC7JXKX.js";
27
+ } from "./chunk-Q5FJN4LN.js";
28
28
  import {
29
29
  TodoStore,
30
30
  beginTurn,
@@ -43,7 +43,7 @@ import {
43
43
  stripOversizedImages,
44
44
  undoTurnSnapshots,
45
45
  userMemoryPath
46
- } from "./chunk-A5PXPWHB.js";
46
+ } from "./chunk-LSYGHLRU.js";
47
47
 
48
48
  // src/agent/loop.ts
49
49
  import { tool as aiTool, generateText as generateText3 } from "ai";
@@ -76,6 +76,9 @@ function textWeight(text) {
76
76
  }
77
77
  return text.length + cjk * (CHARS_PER_TOKEN - 1);
78
78
  }
79
+ function roughTextTokens(text) {
80
+ return Math.ceil(textWeight(text) / CHARS_PER_TOKEN);
81
+ }
79
82
  function contentTokenLength(message) {
80
83
  if (message.role === "tool") {
81
84
  let tokens2 = 0;
@@ -241,6 +244,7 @@ ${transcript}`,
241
244
  }
242
245
  function compactMessages(messages, maxTokens, opts = {}) {
243
246
  const limit = opts.force ? 0 : maxTokens;
247
+ const minKept = opts.force ? 1 : MIN_KEPT_MESSAGES;
244
248
  const overhead = opts.overheadTokens ?? 0;
245
249
  const tokens = messages.map((star) => estimateMessageTokens(star.message));
246
250
  let total = overhead;
@@ -269,7 +273,7 @@ function compactMessages(messages, maxTokens, opts = {}) {
269
273
  let droppedCount = 0;
270
274
  let droppedTokens = 0;
271
275
  for (const turn of turns) {
272
- if (restCount - droppedCount - turn.length < MIN_KEPT_MESSAGES) {
276
+ if (restCount - droppedCount - turn.length < minKept) {
273
277
  break;
274
278
  }
275
279
  droppedCount += turn.length;
@@ -299,6 +303,7 @@ var KEEP_RECENT_MESSAGES = 24;
299
303
  var MIN_ELISION_TOKENS = 250;
300
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]";
301
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";
302
307
  function elideStaleContent(messages, thresholdTokens, overheadTokens = 0) {
303
308
  let total = overheadTokens;
304
309
  for (const star of messages) total += estimateMessageTokens(star.message);
@@ -324,6 +329,19 @@ function elideStaleContent(messages, thresholdTokens, overheadTokens = 0) {
324
329
  meta: { ...star.meta, elided: true }
325
330
  };
326
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++;
327
345
  } else if (message.role === "user" && Array.isArray(message.content)) {
328
346
  if (!message.content.some((part) => part.type === "image" || part.type === "file")) continue;
329
347
  out[i] = {
@@ -348,6 +366,16 @@ function usageTotalTokens(usage) {
348
366
  return summed > 0 ? summed : usage.totalTokens;
349
367
  }
350
368
  function toStreamErrorInfo(error) {
369
+ if (error !== null && typeof error === "object" && !(error instanceof Error)) {
370
+ const raw = error;
371
+ const message = typeof raw.message === "string" && raw.message.length > 0 ? raw.message : JSON.stringify(raw);
372
+ const name = typeof raw.name === "string" ? raw.name : typeof raw.type === "string" ? raw.type : "Error";
373
+ const info2 = { name, message };
374
+ if (typeof raw.statusCode === "number") info2.statusCode = raw.statusCode;
375
+ if (typeof raw.status === "number") info2.statusCode = raw.status;
376
+ if (typeof raw.code === "string") info2.code = raw.code;
377
+ return info2;
378
+ }
351
379
  const err = error instanceof Error ? error : new Error(String(error));
352
380
  const info = { name: err.name, message: err.message };
353
381
  const statusCode = err.statusCode;
@@ -657,6 +685,10 @@ function isNetworkFailure(error) {
657
685
  function matchesRetryablePattern(text) {
658
686
  return typeof text === "string" && RETRYABLE_MESSAGE_PATTERNS.some((p) => p.test(text));
659
687
  }
688
+ var STATUSLESS_RETRYABLE_PATTERNS = [
689
+ /overloaded|rate.?limit|too many requests|resource.?exhausted|at capacity|service.?unavailable|try (?:your request )?again/i,
690
+ /\b(429|500|502|503|504|529)\b/
691
+ ];
660
692
  function isRetryableStreamError(error) {
661
693
  if (NON_RETRYABLE_ERROR_NAMES.has(error.name)) return false;
662
694
  if (error.isRetryable === true) return true;
@@ -666,7 +698,7 @@ function isRetryableStreamError(error) {
666
698
  if (NON_RETRYABLE_4XX.has(status)) return false;
667
699
  return matchesRetryablePattern(error.message) || matchesRetryablePattern(responseBodyOf(error));
668
700
  }
669
- return isNetworkFailure(error);
701
+ return isNetworkFailure(error) || STATUSLESS_RETRYABLE_PATTERNS.some((p) => p.test(error.message));
670
702
  }
671
703
  var RETRY_JITTER_FACTOR = 0.25;
672
704
  var MAX_RETRY_DELAY_MS = 6e4;
@@ -709,7 +741,7 @@ function summarizeStreamError(error) {
709
741
  }
710
742
 
711
743
  // src/llm/stream.ts
712
- import { streamText } from "ai";
744
+ import { InvalidToolArgumentsError, streamText } from "ai";
713
745
  var DEFAULT_IDLE_TIMEOUT_MS = 6e4;
714
746
  var DEFAULT_FIRST_PART_TIMEOUT_MS = 12e4;
715
747
  var CONTENT_PART_TYPES = /* @__PURE__ */ new Set(["text-delta", "tool-call", "tool-call-delta"]);
@@ -791,6 +823,7 @@ async function* streamChat(opts) {
791
823
  let deltas = 0;
792
824
  let hasContent = false;
793
825
  const argsProgress = /* @__PURE__ */ new Map();
826
+ let invalidArgsCounter = 0;
794
827
  let lastPartAt = Date.now();
795
828
  debugStreamLog("request", {
796
829
  messages: opts.messages.length,
@@ -863,6 +896,7 @@ async function* streamChat(opts) {
863
896
  break;
864
897
  case "tool-call":
865
898
  hasContent = true;
899
+ argsProgress.delete(part.toolCallId);
866
900
  yield {
867
901
  type: "tool-call",
868
902
  id: part.toolCallId,
@@ -902,6 +936,22 @@ async function* streamChat(opts) {
902
936
  usage: part.usage,
903
937
  deltas
904
938
  });
939
+ for (const [id, progress] of argsProgress) {
940
+ debugStreamLog("truncated-tool-call", {
941
+ toolCallId: id,
942
+ toolName: progress.name,
943
+ chars: progress.chars,
944
+ finishReason: part.finishReason
945
+ });
946
+ yield {
947
+ type: "tool-call",
948
+ id,
949
+ name: progress.name,
950
+ args: {},
951
+ invalidArgs: `the arguments were cut off mid-stream after ${progress.chars} characters (finish reason: ${part.finishReason}) \u2014 the reply hit the output or context limit before the JSON completed. Reissue ${progress.name} with complete arguments; for large content, split it into several smaller calls.`
952
+ };
953
+ }
954
+ argsProgress.clear();
905
955
  yield {
906
956
  type: "finish",
907
957
  finishReason: part.finishReason,
@@ -916,6 +966,29 @@ async function* streamChat(opts) {
916
966
  }
917
967
  case "error": {
918
968
  if (opts.abortSignal?.aborted) return;
969
+ if (InvalidToolArgumentsError.isInstance(part.error)) {
970
+ const invalid = part.error;
971
+ let args = invalid.toolArgs;
972
+ try {
973
+ args = JSON.parse(invalid.toolArgs);
974
+ } catch {
975
+ }
976
+ const started = [...argsProgress.entries()].reverse().find(([, progress]) => progress.name === invalid.toolName);
977
+ debugStreamLog("invalid-tool-args", {
978
+ toolName: invalid.toolName,
979
+ recoveredId: started?.[0] ?? null
980
+ });
981
+ yield {
982
+ type: "tool-call",
983
+ id: started?.[0] ?? `invalid-args-${++invalidArgsCounter}`,
984
+ name: invalid.toolName,
985
+ args,
986
+ // The zod detail tells the model exactly which field to fix;
987
+ // capped because the message embeds the full rejected value.
988
+ invalidArgs: invalid.message.length > 2e3 ? `${invalid.message.slice(0, 2e3)}\u2026` : invalid.message
989
+ };
990
+ return;
991
+ }
919
992
  const error = toStreamErrorInfo(part.error);
920
993
  debugStreamLog("error-part", { seenContent, deltas, error: summarizeStreamError(error) });
921
994
  yield { type: "error", error };
@@ -2087,7 +2160,7 @@ function findModelPricing(config, name, modelId) {
2087
2160
  return config.models.find((m) => m.model === modelId);
2088
2161
  }
2089
2162
  async function runSubagent(deps, args, cwd, signal, agentIdRef) {
2090
- const { AgentLoop: AgentLoop2 } = await import("./loop-TW2FHL7J.js");
2163
+ const { AgentLoop: AgentLoop2 } = await import("./loop-BYXBNFFY.js");
2091
2164
  const child = new AgentLoop2({
2092
2165
  model: deps.model,
2093
2166
  registry: createDefaultRegistry(),
@@ -2284,6 +2357,7 @@ function finalNudgeWarning(autoContinues, maxAutoContinues) {
2284
2357
  return autoContinues >= maxAutoContinues ? " This is the final automatic continuation \u2014 if your next reply again has no tool calls, nudging stops and the turn is handed back to the user. Finish the work now or summarize what remains." : "";
2285
2358
  }
2286
2359
  var COMPLETION_CHECK_TIMEOUT_MS = 3e4;
2360
+ var TODO_STALE_NUDGE_STEPS = 20;
2287
2361
  function attachTurnContext(messages, context) {
2288
2362
  if (!context) return messages;
2289
2363
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -2422,6 +2496,29 @@ var AgentLoop = class {
2422
2496
  // (the tool map's share of the request window) is computed alongside since
2423
2497
  // it depends on the same inputs.
2424
2498
  aiToolsCache = null;
2499
+ // Server-calibration baseline for the context estimate: the latest finish
2500
+ // event's provider-reported promptTokens paired with the local estimate of
2501
+ // the exact request that produced it. Non-OpenAI tokenizers can count the
2502
+ // same history far higher than the local cl100k estimate (a qwen endpoint
2503
+ // counted ~30% more on a Chinese-heavy code session), which let the ctx%
2504
+ // display and the auto-compaction trigger lag the real window fill until
2505
+ // the provider itself started truncating replies. The delta refreshes on
2506
+ // every step's finish, so drift is bounded to one step's new content.
2507
+ serverTokenBaseline = null;
2508
+ // Text streamed by the in-flight reply (visible text + reasoning +
2509
+ // completed tool-call args). estimateContextTokens folds a rough token
2510
+ // estimate of it into the status-bar ctx% so the display tracks the
2511
+ // server's real-time occupancy — prompt PLUS generated-so-far, which is
2512
+ // what llama.cpp-style monitors show — instead of lagging it by the whole
2513
+ // in-flight generation (10k reasoning tokens ≈ 8% of a 128k window).
2514
+ // Reset when each stream attempt ends: the persisted reply is then counted
2515
+ // through the history estimate, and reasoning — never resent — drops out.
2516
+ // User steering prompts submitted while a turn is running (the REPL's
2517
+ // "insert now" choice). Drained into the history as synthetic user
2518
+ // messages at the next step boundary — the in-flight model reply and its
2519
+ // tool calls always finish first, so a steer never interrupts mid-step.
2520
+ pendingSteers = [];
2521
+ inflightStreamText = "";
2425
2522
  // Lazily resolved auxiliary model (config.smallModel) for cheap side calls
2426
2523
  // — compaction summaries, session titles, completion checks. Undefined =
2427
2524
  // use the main model; a resolution failure is surfaced once as a notice.
@@ -2487,12 +2584,56 @@ var AgentLoop = class {
2487
2584
  getStarMessages() {
2488
2585
  return this.messages;
2489
2586
  }
2587
+ // Queue a user steering prompt for the running turn. Delivery happens at
2588
+ // the next step boundary (see drainSteers); if the turn ends first the
2589
+ // text stays queued and takeUndeliveredSteers hands it back to the caller.
2590
+ injectUserMessage(text) {
2591
+ const trimmed = text.trim();
2592
+ if (trimmed.length > 0) this.pendingSteers.push(trimmed);
2593
+ }
2594
+ // Steers that never reached a step boundary (turn ended, aborted or
2595
+ // errored first). The caller decides their fate — the REPL re-queues them
2596
+ // as ordinary prompts so user input is never dropped.
2597
+ takeUndeliveredSteers() {
2598
+ return this.pendingSteers.splice(0);
2599
+ }
2600
+ // Append queued steers to the history as synthetic user messages: they
2601
+ // belong to the turn in progress, so /undo, /rewind and compaction must
2602
+ // not treat them as turn boundaries. Returns the number delivered.
2603
+ async drainSteers() {
2604
+ const steers = this.pendingSteers.splice(0);
2605
+ for (const text of steers) {
2606
+ const message = { role: "user", content: text };
2607
+ const meta = { synthetic: "steer" };
2608
+ this.messages.push({ message, meta });
2609
+ await this.persist(message, meta);
2610
+ }
2611
+ return steers.length;
2612
+ }
2490
2613
  // Wire-schema token overhead of the active tool set (memoized by
2491
2614
  // buildAiTools). The auto-compact budget includes it; surfaced here so the
2492
2615
  // status bar ctx% can show the same total the compaction decision uses.
2493
2616
  getToolSchemaTokens() {
2494
2617
  return this.buildAiTools().schemaTokens;
2495
2618
  }
2619
+ // Additive correction from serverTokenBaseline, clamped ≥ 0: an
2620
+ // under-reading provider estimate must pull compaction earlier (the bug
2621
+ // this fixes), but an over-reading one must never push it later than the
2622
+ // local estimate alone would — compacting early is safe, late is fatal.
2623
+ calibrationDelta() {
2624
+ const baseline = this.serverTokenBaseline;
2625
+ if (!baseline) return 0;
2626
+ return Math.max(0, baseline.serverTokens - baseline.localTokens);
2627
+ }
2628
+ // Effective context-window usage in tokens: local BPE estimate + tool
2629
+ // schemas + the server-calibration delta + the in-flight reply's streamed
2630
+ // output. This is the status bar's live ctx% view; the elision/compaction
2631
+ // budgets deliberately use only the first three (history + schemas +
2632
+ // calibration), since in-flight reasoning is never resent and must not
2633
+ // trigger a compaction the next request won't need.
2634
+ estimateContextTokens() {
2635
+ return estimateTokens(this.getMessages()) + this.getToolSchemaTokens() + this.calibrationDelta() + (this.inflightStreamText.length > 0 ? roughTextTokens(this.inflightStreamText) : 0);
2636
+ }
2496
2637
  // Auxiliary model for cheap side calls: config.smallModel, resolved once.
2497
2638
  // An unresolvable name/key degrades to the main model instead of breaking
2498
2639
  // turns; the failure rides out once as a notice at the next turn start.
@@ -2832,6 +2973,9 @@ var AgentLoop = class {
2832
2973
  let usedToolsThisTurn = false;
2833
2974
  let streamCutContinues = 0;
2834
2975
  let openTodos = [];
2976
+ let todoRevision = this.opts.registry.todos.revision;
2977
+ let stepsSinceTodoChange = 0;
2978
+ const todoStaleNudgeSteps = this.opts.todoStaleNudgeSteps ?? TODO_STALE_NUDGE_STEPS;
2835
2979
  const maxSteps = config.maxSteps;
2836
2980
  let step = 0;
2837
2981
  let toolExecutedSinceCheckpoint = false;
@@ -2868,16 +3012,47 @@ ${finished.result}`
2868
3012
  await this.persist(note, meta);
2869
3013
  }
2870
3014
  }
3015
+ const steered = await this.drainSteers();
3016
+ if (steered > 0) {
3017
+ yield {
3018
+ type: "notice",
3019
+ message: `${steered} steering message(s) delivered to the model; taking effect from this step.`
3020
+ };
3021
+ }
3022
+ const currentTodoRevision = registry.todos.revision;
3023
+ if (currentTodoRevision !== todoRevision) {
3024
+ todoRevision = currentTodoRevision;
3025
+ stepsSinceTodoChange = 0;
3026
+ } else if (stepsSinceTodoChange >= todoStaleNudgeSteps) {
3027
+ const open = registry.todos.list().filter((t) => t.status !== "done");
3028
+ if (open.length > 0) {
3029
+ stepsSinceTodoChange = 0;
3030
+ yield {
3031
+ type: "notice",
3032
+ message: `todo list unchanged for ${todoStaleNudgeSteps} steps with ${open.length} open item(s); reminding the model to update it.`
3033
+ };
3034
+ const reminder = {
3035
+ role: "user",
3036
+ content: `[todo reminder] Your todo list still has ${open.length} open item(s), unchanged for ${todoStaleNudgeSteps} steps: ${open.slice(0, 5).map((t) => `"${t.title}"`).join(
3037
+ ", "
3038
+ )}${open.length > 5 ? ", \u2026" : ""}. Update the list with todo_write so it reflects what is done and what you are working on now, then continue working.`
3039
+ };
3040
+ const meta = { synthetic: "nudge" };
3041
+ this.messages.push({ message: reminder, meta });
3042
+ await this.persist(reminder, meta);
3043
+ }
3044
+ }
2871
3045
  const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
2872
3046
  const threshold = resolveCompactThreshold(config, maxTokens);
2873
- const elided = elideStaleContent(this.messages, threshold, toolSchemaTokens);
3047
+ const calibration = this.calibrationDelta();
3048
+ const elided = elideStaleContent(this.messages, threshold, toolSchemaTokens + calibration);
2874
3049
  if (elided) {
2875
3050
  this.messages = elided.messages;
2876
3051
  await this.opts.sessionStore?.replaceMessages([...this.messages]);
2877
3052
  this.opts.registry?.resetVolatileState();
2878
3053
  }
2879
3054
  const compacted = compactMessages(this.messages, threshold, {
2880
- overheadTokens: toolSchemaTokens
3055
+ overheadTokens: toolSchemaTokens + calibration
2881
3056
  });
2882
3057
  if (compacted.compacted) {
2883
3058
  this.messages = await this.applyCompactionSummary(compacted, signal);
@@ -2885,6 +3060,7 @@ ${finished.result}`
2885
3060
  this.turnMarkers = [];
2886
3061
  this.opts.registry?.resetVolatileState();
2887
3062
  }
3063
+ const requestLocalTokens = estimateTokens(this.getMessages()) + toolSchemaTokens;
2888
3064
  const maxRetries = Math.max(0, config.streamMaxRetries);
2889
3065
  const baseDelay = this.opts.retryDelayMs ?? STREAM_RETRY_BASE_DELAY_MS;
2890
3066
  let text = "";
@@ -2905,22 +3081,37 @@ ${finished.result}`
2905
3081
  lastUsage = void 0;
2906
3082
  sawReasoning = false;
2907
3083
  wasTruncated = false;
3084
+ this.inflightStreamText = "";
2908
3085
  const timeoutScale = Math.min(attempt + 1, 3);
2909
3086
  try {
2910
3087
  for await (const event of this.streamOnce(aiTools, signal, timeoutScale)) {
2911
3088
  if (event.type === "text-delta") {
2912
3089
  text += event.text;
3090
+ this.inflightStreamText += event.text;
2913
3091
  yield event;
2914
3092
  } else if (event.type === "reasoning") {
2915
3093
  sawReasoning = true;
3094
+ this.inflightStreamText += event.text;
2916
3095
  yield event;
2917
3096
  } else if (event.type === "tool-call") {
2918
- toolCalls.push({ id: event.id, name: event.name, args: event.args });
3097
+ this.inflightStreamText += JSON.stringify(event.args ?? null);
3098
+ toolCalls.push({
3099
+ id: event.id,
3100
+ name: event.name,
3101
+ args: event.args,
3102
+ ...event.invalidArgs !== void 0 ? { invalidArgs: event.invalidArgs } : {}
3103
+ });
2919
3104
  yield event;
2920
3105
  } else if (event.type === "finish") {
2921
3106
  lastFinishReason = event.finishReason;
2922
3107
  if (event.truncated) wasTruncated = true;
2923
3108
  lastUsage = event.usage;
3109
+ if (event.usage && event.usage.promptTokens > 0) {
3110
+ this.serverTokenBaseline = {
3111
+ localTokens: requestLocalTokens,
3112
+ serverTokens: event.usage.promptTokens
3113
+ };
3114
+ }
2924
3115
  yield this.foldSubagentUsage(event);
2925
3116
  } else if (event.type === "error") {
2926
3117
  failure = event.error;
@@ -2933,6 +3124,7 @@ ${finished.result}`
2933
3124
  failure = toStreamErrorInfo(error);
2934
3125
  }
2935
3126
  }
3127
+ this.inflightStreamText = "";
2936
3128
  if (signal.aborted) {
2937
3129
  yield* this.persistInterrupted(text, toolCalls);
2938
3130
  await this.runStopHooks("aborted");
@@ -2985,6 +3177,14 @@ ${finished.result}`
2985
3177
  return;
2986
3178
  }
2987
3179
  if (!hasOutput()) {
3180
+ const steered2 = await this.drainSteers();
3181
+ if (steered2 > 0) {
3182
+ yield {
3183
+ type: "notice",
3184
+ message: `${steered2} steering message(s) delivered to the model after an empty reply.`
3185
+ };
3186
+ continue;
3187
+ }
2988
3188
  if (isSteerableEmptyFinish(lastFinishReason) && config.permissionMode !== "plan" && autoContinues < config.maxAutoContinues) {
2989
3189
  autoContinues++;
2990
3190
  lastWasNudge = true;
@@ -3026,6 +3226,16 @@ ${finished.result}`
3026
3226
  this.messages.push({ message: assistantMessage });
3027
3227
  await this.persist(assistantMessage);
3028
3228
  if (toolCalls.length === 0) {
3229
+ const steered2 = await this.drainSteers();
3230
+ if (steered2 > 0) {
3231
+ autoContinues = 0;
3232
+ lastWasNudge = false;
3233
+ yield {
3234
+ type: "notice",
3235
+ message: `${steered2} steering message(s) delivered to the model; the turn continues.`
3236
+ };
3237
+ continue;
3238
+ }
3029
3239
  if (wasTruncated) {
3030
3240
  if (config.permissionMode !== "plan" && streamCutContinues < config.streamMaxRetries) {
3031
3241
  streamCutContinues++;
@@ -3114,7 +3324,8 @@ ${finished.result}`
3114
3324
  try {
3115
3325
  for (const call of toolCalls) {
3116
3326
  if (signal.aborted) break;
3117
- const refusal = this.doomLoopRefusal(call);
3327
+ const refusal = call.invalidArgs !== void 0 ? `Tool call not executed \u2014 ${call.invalidArgs}
3328
+ Reissue ${call.name} with valid arguments.` : this.doomLoopRefusal(call);
3118
3329
  if (refusal !== null) {
3119
3330
  yield { type: "notice", message: refusal };
3120
3331
  } else {
@@ -3194,6 +3405,7 @@ ${finished.result}`
3194
3405
  return;
3195
3406
  }
3196
3407
  step++;
3408
+ stepsSinceTodoChange++;
3197
3409
  }
3198
3410
  }
3199
3411
  // The permission mode can change at runtime, so the system message is
@@ -3507,4 +3719,4 @@ export {
3507
3719
  attachTurnContext,
3508
3720
  AgentLoop
3509
3721
  };
3510
- //# sourceMappingURL=chunk-MNVCCOI2.js.map
3722
+ //# sourceMappingURL=chunk-F5RWZMQY.js.map