autopilot-ai 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/sdk/index.js CHANGED
@@ -325,9 +325,9 @@ var require_node_gyp_build = __commonJS({
325
325
  var debug = getFirst(path.join(dir, "build/Debug"), matchBuild);
326
326
  if (debug) return debug;
327
327
  }
328
- var prebuild = resolve4(dir);
328
+ var prebuild = resolve6(dir);
329
329
  if (prebuild) return prebuild;
330
- var nearby = resolve4(path.dirname(process.execPath));
330
+ var nearby = resolve6(path.dirname(process.execPath));
331
331
  if (nearby) return nearby;
332
332
  var target = [
333
333
  "platform=" + platform3,
@@ -343,7 +343,7 @@ var require_node_gyp_build = __commonJS({
343
343
  // eslint-disable-line
344
344
  ].filter(Boolean).join(" ");
345
345
  throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
346
- function resolve4(dir2) {
346
+ function resolve6(dir2) {
347
347
  var tuples = readdirSync2(path.join(dir2, "prebuilds")).map(parseTuple);
348
348
  var tuple = tuples.filter(matchTuple(platform3, arch)).sort(compareTuples)[0];
349
349
  if (!tuple) return;
@@ -1169,7 +1169,7 @@ var init_types = __esm({
1169
1169
  // src/hooks/settings.ts
1170
1170
  import { existsSync, readFileSync as readFileSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
1171
1171
  import { homedir as homedir10 } from "os";
1172
- import { join as join17, dirname as dirname7 } from "path";
1172
+ import { join as join17, dirname as dirname8 } from "path";
1173
1173
  import { createHash } from "crypto";
1174
1174
  function globalSettingsPath() {
1175
1175
  const xdg = process.env.XDG_CONFIG_HOME || join17(homedir10(), ".config");
@@ -1196,12 +1196,12 @@ function normalizeHook(event, raw, source) {
1196
1196
  if (!raw || typeof raw !== "object") return null;
1197
1197
  const r = raw;
1198
1198
  if (typeof r.command !== "string" || !r.command.trim()) return null;
1199
- const enabled = typeof r.enabled === "boolean" ? r.enabled : true;
1199
+ const enabled2 = typeof r.enabled === "boolean" ? r.enabled : true;
1200
1200
  const matcher = typeof r.matcher === "string" ? r.matcher : void 0;
1201
1201
  const timeoutMs = typeof r.timeoutMs === "number" && r.timeoutMs > 0 ? r.timeoutMs : void 0;
1202
1202
  const description = typeof r.description === "string" ? r.description : void 0;
1203
1203
  const id = typeof r.id === "string" && r.id.length > 0 ? r.id : deriveHookId(event, r.command);
1204
- return { id, matcher, command: r.command, timeoutMs, enabled, description, source };
1204
+ return { id, matcher, command: r.command, timeoutMs, enabled: enabled2, description, source };
1205
1205
  }
1206
1206
  function mergeHookMaps(a, b) {
1207
1207
  const out = {};
@@ -1237,7 +1237,7 @@ var init_settings = __esm({
1237
1237
  });
1238
1238
 
1239
1239
  // src/hooks/runner.ts
1240
- import { spawn as spawn3 } from "child_process";
1240
+ import { spawn as spawn4 } from "child_process";
1241
1241
  function isVetoEvent(event) {
1242
1242
  return event === "PreToolUse" || event === "UserPromptSubmit";
1243
1243
  }
@@ -1354,8 +1354,8 @@ var init_runner = __esm({
1354
1354
  init_logger();
1355
1355
  DEFAULT_TIMEOUT_MS = 3e4;
1356
1356
  STREAM_CAP_BYTES = 4 * 1024;
1357
- defaultSpawn = (command, payloadJson, env, cwd, timeoutMs, signal) => new Promise((resolve4) => {
1358
- const child = spawn3(command, {
1357
+ defaultSpawn = (command, payloadJson, env, cwd, timeoutMs, signal) => new Promise((resolve6) => {
1358
+ const child = spawn4(command, {
1359
1359
  shell: true,
1360
1360
  cwd,
1361
1361
  env,
@@ -1369,7 +1369,7 @@ var init_runner = __esm({
1369
1369
  settled = true;
1370
1370
  clearTimeout(timer2);
1371
1371
  signal?.removeEventListener("abort", onAbort);
1372
- resolve4({ exitCode, stdout, stderr, timedOut });
1372
+ resolve6({ exitCode, stdout, stderr, timedOut });
1373
1373
  };
1374
1374
  const onAbort = () => {
1375
1375
  child.kill("SIGTERM");
@@ -1442,7 +1442,7 @@ var init_manager = __esm({
1442
1442
  });
1443
1443
 
1444
1444
  // src/sdk/session.ts
1445
- import { resolve as resolve3 } from "path";
1445
+ import { resolve as resolve5 } from "path";
1446
1446
  import { homedir as homedir11 } from "os";
1447
1447
  import { join as join18 } from "path";
1448
1448
  import { existsSync as existsSync2 } from "fs";
@@ -1490,10 +1490,10 @@ async function* readSSE(stream, signal, idleTimeoutMs, postFirstByteIdleTimeoutM
1490
1490
  gotFirstByte = true;
1491
1491
  buffer += decoder.decode(value, { stream: true });
1492
1492
  buffer = buffer.replace(/\r\n/g, "\n");
1493
- let sep2;
1494
- while ((sep2 = buffer.indexOf("\n\n")) !== -1) {
1495
- const event = buffer.slice(0, sep2);
1496
- buffer = buffer.slice(sep2 + 2);
1493
+ let sep3;
1494
+ while ((sep3 = buffer.indexOf("\n\n")) !== -1) {
1495
+ const event = buffer.slice(0, sep3);
1496
+ buffer = buffer.slice(sep3 + 2);
1497
1497
  const data = extractData(event);
1498
1498
  if (data !== null) yield data;
1499
1499
  }
@@ -2296,11 +2296,11 @@ function extractApiError(parsed, rawText) {
2296
2296
  return null;
2297
2297
  }
2298
2298
  function sleep(ms, signal) {
2299
- return new Promise((resolve4, reject) => {
2299
+ return new Promise((resolve6, reject) => {
2300
2300
  if (signal?.aborted) return reject(new DOMException("aborted", "AbortError"));
2301
2301
  const t = setTimeout(() => {
2302
2302
  signal?.removeEventListener("abort", onAbort);
2303
- resolve4();
2303
+ resolve6();
2304
2304
  }, ms);
2305
2305
  const onAbort = () => {
2306
2306
  clearTimeout(t);
@@ -2385,7 +2385,7 @@ var RETENTION = {
2385
2385
  memoryMaxEntries: 1e3
2386
2386
  };
2387
2387
  async function rotateJsonl(path, maxBytes, rotations) {
2388
- const { rename } = await import("fs/promises");
2388
+ const { rename: rename3 } = await import("fs/promises");
2389
2389
  let s;
2390
2390
  try {
2391
2391
  s = await stat(path);
@@ -2397,7 +2397,7 @@ async function rotateJsonl(path, maxBytes, rotations) {
2397
2397
  const src = i === 1 ? path : `${path}.${i - 1}`;
2398
2398
  const dst = `${path}.${i}`;
2399
2399
  try {
2400
- await rename(src, dst);
2400
+ await rename3(src, dst);
2401
2401
  } catch {
2402
2402
  }
2403
2403
  }
@@ -3148,7 +3148,7 @@ function truncateForEmbedding(text) {
3148
3148
  return text.slice(0, MAX_EMBED_CHARS);
3149
3149
  }
3150
3150
  async function sleep2(ms) {
3151
- return new Promise((resolve4) => setTimeout(resolve4, ms));
3151
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
3152
3152
  }
3153
3153
  async function fetchWithRetry(url, init, retries = 3) {
3154
3154
  let lastError;
@@ -3347,6 +3347,56 @@ init_mode();
3347
3347
  import { platform, release, homedir as homedir4 } from "os";
3348
3348
  import { basename, join as join7 } from "path";
3349
3349
  import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
3350
+
3351
+ // src/agent/user-shell.ts
3352
+ var enabled = false;
3353
+ function userShellCommandsEnabled() {
3354
+ return enabled;
3355
+ }
3356
+ var USER_SHELL_PROMPT_SECTION = `## Commands the user runs with \`!\`
3357
+
3358
+ The user can run a shell command directly in this session by typing \`!\` followed by the command in the prompt (for example \`! gh auth login\`). It runs in their real terminal with full keyboard input, and the output is added to this conversation for you to read.
3359
+
3360
+ Your \`bash\` tool has no terminal: stdin is closed and there is no TTY, so anything that waits for typed input fails or hangs. Instead of running the command yourself, suggest \`! <exact command>\` when:
3361
+ - it needs a login or interactive auth (\`gh auth login\`, \`npm login\`, \`docker login\`, \`gcloud auth login\`, \`aws sso login\`, \`az login\`, \`wrangler login\`, \`vercel login\`, \u2026)
3362
+ - it prompts for a password, passphrase, one-time code, or confirmation (\`sudo \u2026\`, \`ssh\` with a passphrase or unknown host key, \`git push\` over HTTPS asking for credentials)
3363
+ - it is an interactive program (REPLs, setup wizards without non-interactive flags, \`git rebase -i\`, editors, TUIs)
3364
+ - the user would otherwise have to paste a secret (token, password) into the chat
3365
+ - one of your bash calls failed with a "not logged in", "authentication required", "not a tty", or "terminal required" error, or timed out waiting for input
3366
+ - the user declined permission for a command but still wants it run
3367
+
3368
+ Prefer non-interactive flags when they exist (\`--yes\`, \`-m\`, \`--with-token\` reading an env var, \`CI=1\`) and run the command yourself. Don't suggest \`!\` for commands your bash tool can run. When you do suggest it, give the exact line to type (e.g. "run \`! gh auth login\` in the prompt, then tell me when you're done") and stop; the output will appear in the conversation.`;
3369
+ var AUTH_OR_TTY_OUTPUT = [
3370
+ /not logged in(?:to)?\b/i,
3371
+ /you are not logged in/i,
3372
+ /\b(?:authentication|login|sign[- ]?in) (?:is )?required\b/i,
3373
+ /\bplease (?:run|use) [`'"]?[\w.-]+ (?:auth )?log ?in\b/i,
3374
+ /\bto (?:authenticate|get started with [\w ]+CLI), (?:please )?run\b/i,
3375
+ /\bgh auth login\b/i,
3376
+ /\bnot a tty\b/i,
3377
+ /\binappropriate ioctl for device\b/i,
3378
+ /\b(?:stdin|input) is not a (?:tty|terminal)\b/i,
3379
+ /\b(?:a|no) (?:terminal|tty) (?:is )?(?:required|present)\b/i,
3380
+ /\bmust be run (?:from|in) an? (?:terminal|interactive)/i,
3381
+ /\brequires an interactive (?:terminal|shell|session)\b/i,
3382
+ /\bcannot prompt\b|\bunable to prompt\b|\bprompts? (?:are )?disabled\b/i,
3383
+ /\bcould not read (?:username|password) for\b/i,
3384
+ /\bterminal prompts disabled\b/i,
3385
+ /\bhost key verification failed\b/i
3386
+ ];
3387
+ var INTERACTIVE_COMMAND = /^(?:sudo\b|ssh\b|passwd\b|su\b|gh auth login\b|npm (?:login|adduser)\b|yarn (?:npm )?login\b|pnpm login\b|docker login\b|gcloud auth\b|aws (?:sso )?(?:login|configure)\b|az login\b|firebase login\b|vercel login\b|wrangler login\b|netlify login\b|heroku login\b|fly(?:ctl)? auth login\b|op signin\b|gpg\b)/i;
3388
+ var TRAILING_PROMPT = /(?:password|passphrase|username|token|code|otp)[^\n]{0,40}:\s*$|\[(?:y\/n|Y\/n|y\/N)\]\s*$|\(y(?:es)?\/n(?:o)?\)\s*$|\?\s*$/i;
3389
+ function interactiveCommandHint(o) {
3390
+ if (!o.timedOut && o.exitCode === 0) return null;
3391
+ const cmd = o.command.trim().replace(/^cd\s+[^\s&;]+\s*(?:&&|;)\s*/, "");
3392
+ const tail = o.output.slice(-4e3);
3393
+ const looksInteractive = AUTH_OR_TTY_OUTPUT.some((re) => re.test(tail)) || INTERACTIVE_COMMAND.test(cmd) || o.timedOut && TRAILING_PROMPT.test(tail.trimEnd());
3394
+ if (!looksInteractive) return null;
3395
+ const suggestion = cmd.length <= 200 && !cmd.includes("\n") ? `\`! ${cmd}\`` : "`! <command>`";
3396
+ return `[autopilot] This command appears to need a login, a password, or an interactive terminal, which your bash tool cannot provide. Don't retry it as-is. If a non-interactive alternative exists, use it; otherwise ask the user to run ${suggestion} in the prompt and wait for its output to appear in the conversation.`;
3397
+ }
3398
+
3399
+ // src/agent/system-prompt.ts
3350
3400
  var CONTEXT_FILENAMES = ["KIMI.md", "KIMIFLARE.md", "AGENT.md"];
3351
3401
  var MAX_CONTEXT_BYTES = 20 * 1024;
3352
3402
  function loadContextFile(cwd) {
@@ -3409,6 +3459,9 @@ If the user asks what model you are, answer with exactly: \`${opts.model}\`. Thi
3409
3459
  const lspBlock = hasLsp ? "\n\nLSP tools are available for semantic code intelligence. Prefer `lsp_definition` over `grep` when looking for the source of a symbol. Prefer `lsp_references` over `grep` when finding usages. Use `lsp_hover` to confirm types before refactoring." : "";
3410
3460
  const tools = `Tools available:
3411
3461
  ${toolsBlock}`;
3462
+ const userShellBlock = userShellCommandsEnabled() ? `
3463
+
3464
+ ${USER_SHELL_PROMPT_SECTION}` : "";
3412
3465
  const ctx = loadContextFile(opts.cwd);
3413
3466
  const contextBlock = ctx ? `
3414
3467
 
@@ -3445,13 +3498,19 @@ ${opts.lspContext}` : "";
3445
3498
  The following MCP server information was pre-computed by the coordinator.
3446
3499
 
3447
3500
  ${opts.mcpContext}` : "";
3448
- return identity + "\n\n" + env + "\n\n" + tools + lspBlock + contextBlock + modeBlock + skillsBlock + memoryBlock + lspContextBlock + mcpContextBlock;
3501
+ const delegationBlock = opts.delegationDirective ? `
3502
+
3503
+ ## Subagent policy for this turn
3504
+
3505
+ ${opts.delegationDirective}` : "";
3506
+ return identity + "\n\n" + env + "\n\n" + tools + lspBlock + userShellBlock + contextBlock + modeBlock + skillsBlock + memoryBlock + lspContextBlock + mcpContextBlock + delegationBlock;
3449
3507
  }
3450
3508
  function buildSystemPrompt(opts) {
3451
3509
  return buildStaticPrefix(opts) + "\n\n" + buildSessionPrefix(opts);
3452
3510
  }
3453
3511
 
3454
3512
  // src/agent/loop.ts
3513
+ var MAX_LOOP_RECOVERIES = 1;
3455
3514
  var BudgetExhaustedError = class extends Error {
3456
3515
  constructor(message = "Cumulative input token budget exhausted") {
3457
3516
  super(message);
@@ -3459,7 +3518,7 @@ var BudgetExhaustedError = class extends Error {
3459
3518
  }
3460
3519
  };
3461
3520
  var AgentLoopError = class extends Error {
3462
- constructor(message = "Agent got stuck repeating the same tool calls") {
3521
+ constructor(message = "Agent got stuck repeating the same tool calls and was stopped after a recovery attempt") {
3463
3522
  super(message);
3464
3523
  this.name = "AgentLoopError";
3465
3524
  }
@@ -3516,6 +3575,12 @@ async function runAgentTurn(opts) {
3516
3575
  const turnStart = performance.now();
3517
3576
  logger.info("turn:start", { sessionId: opts.sessionId, codeMode: opts.codeMode ?? false });
3518
3577
  const max = opts.maxToolIterations ?? 200;
3578
+ const limitBehavior = opts.toolLimitBehavior ?? (opts.continueOnLimit ? "continue" : "throw");
3579
+ const requestedCeiling = opts.maxTotalToolIterations;
3580
+ const hardCeiling = Math.max(
3581
+ max,
3582
+ requestedCeiling !== void 0 && Number.isFinite(requestedCeiling) && requestedCeiling > 0 ? Math.floor(requestedCeiling) : max * 5
3583
+ );
3519
3584
  const codeMode = opts.codeMode ?? false;
3520
3585
  const fireStopHook = async () => {
3521
3586
  if (opts.signal.aborted) return;
@@ -3582,7 +3647,8 @@ async function runAgentTurn(opts) {
3582
3647
  model: opts.model,
3583
3648
  mode: opts.mode,
3584
3649
  skillContext: skillResult.skillContext,
3585
- preferPullRequests: opts.preferPullRequests
3650
+ preferPullRequests: opts.preferPullRequests,
3651
+ delegationDirective: opts.delegationDirective
3586
3652
  })
3587
3653
  };
3588
3654
  } else {
@@ -3594,7 +3660,8 @@ async function runAgentTurn(opts) {
3594
3660
  model: opts.model,
3595
3661
  mode: opts.mode,
3596
3662
  skillContext: skillResult.skillContext,
3597
- preferPullRequests: opts.preferPullRequests
3663
+ preferPullRequests: opts.preferPullRequests,
3664
+ delegationDirective: opts.delegationDirective
3598
3665
  })
3599
3666
  };
3600
3667
  }
@@ -3665,8 +3732,11 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
3665
3732
  const WEB_FETCH_DOMAIN_THRESHOLD = 2;
3666
3733
  let cumulativePromptTokens = 0;
3667
3734
  let iter = 0;
3735
+ let totalIter = 0;
3668
3736
  let budgetExhausted = false;
3669
3737
  let loopExhausted = false;
3738
+ let loopRecoveries = 0;
3739
+ let finalizeReason = null;
3670
3740
  let currentTasks = [];
3671
3741
  let mutatingToolsSinceLastTasksSet = 0;
3672
3742
  const MUTATING_TOOLS2 = /* @__PURE__ */ new Set(["write", "edit", "bash"]);
@@ -3684,36 +3754,43 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
3684
3754
  content: "You have reached the cumulative input token budget for this session. Please synthesize your findings and provide a final summary of what was accomplished."
3685
3755
  });
3686
3756
  }
3687
- if (loopExhausted) {
3757
+ if (finalizeReason === null && totalIter >= hardCeiling) {
3758
+ finalizeReason = "limit_ceiling";
3688
3759
  opts.messages.push({
3689
3760
  role: "system",
3690
- content: "You have repeatedly called the same tools with identical arguments and are stuck in a loop. Please synthesize what you know from the conversation history and provide a final answer."
3761
+ content: `You have reached the hard safety ceiling of ${hardCeiling} tool iterations for this turn. Do not call any more tools. Summarize what you accomplished, what remains unfinished, and the concrete next steps needed to complete the task.`
3691
3762
  });
3692
- }
3693
- if (iter >= max) {
3694
- if (opts.callbacks.onToolLimitReached) {
3695
- const decision = await opts.callbacks.onToolLimitReached();
3696
- if (decision === "continue") {
3697
- opts.messages.push({
3698
- role: "system",
3699
- content: "You have reached the tool-call limit for this session. The counter has been reset so you can continue working. Please proceed with your task."
3700
- });
3701
- iter = 0;
3702
- } else {
3703
- await fireStopHook();
3704
- return;
3705
- }
3706
- } else if (opts.continueOnLimit) {
3763
+ opts.callbacks.onGuardrail?.({
3764
+ kind: "limit_ceiling",
3765
+ message: `Tool-iteration ceiling (${hardCeiling}) reached \u2014 wrapping up the turn with a summary.`,
3766
+ totalIterations: totalIter
3767
+ });
3768
+ } else if (finalizeReason === null && iter >= max) {
3769
+ if (limitBehavior === "continue") {
3707
3770
  opts.messages.push({
3708
3771
  role: "system",
3709
3772
  content: "You have reached the tool-call limit for this session. The counter has been reset so you can continue working. Please proceed with your task."
3710
3773
  });
3711
3774
  iter = 0;
3775
+ opts.callbacks.onGuardrail?.({
3776
+ kind: "limit_reset",
3777
+ message: `Tool-call limit (${max}) reached \u2014 counter reset automatically (${totalIter}/${hardCeiling} total).`,
3778
+ totalIterations: totalIter
3779
+ });
3780
+ } else if (limitBehavior === "stop") {
3781
+ opts.callbacks.onGuardrail?.({
3782
+ kind: "limit_stopped",
3783
+ message: `Tool-call limit (${max}) reached \u2014 turn stopped.`,
3784
+ totalIterations: totalIter
3785
+ });
3786
+ await fireStopHook();
3787
+ return;
3712
3788
  } else {
3713
3789
  throw new Error(`kimiflare: tool iteration limit reached (${max})`);
3714
3790
  }
3715
3791
  }
3716
3792
  iter++;
3793
+ totalIter++;
3717
3794
  turn++;
3718
3795
  const previousMessages = opts.messages.slice();
3719
3796
  const toolCalls = [];
@@ -3852,8 +3929,21 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
3852
3929
  }))
3853
3930
  } : {}
3854
3931
  };
3932
+ if (finalizeReason !== null) {
3933
+ delete assistantMsg.tool_calls;
3934
+ if (!assistantMsg.content) {
3935
+ assistantMsg.content = finalizeReason === "loop" ? "Stopped: I kept repeating blocked tool calls and could not make further progress on this task." : `Stopped: reached the ${hardCeiling}-iteration safety ceiling for this turn before finishing.`;
3936
+ }
3937
+ }
3855
3938
  opts.messages.push(assistantMsg);
3856
3939
  opts.callbacks.onAssistantFinal?.(assistantMsg);
3940
+ if (finalizeReason !== null) {
3941
+ logger.info("turn:finalized", { sessionId: opts.sessionId, reason: finalizeReason, totalIter });
3942
+ if (budgetExhausted) throw new BudgetExhaustedError();
3943
+ await fireStopHook();
3944
+ if (finalizeReason === "loop") throw new AgentLoopError();
3945
+ return;
3946
+ }
3857
3947
  if (toolCalls.length === 0) {
3858
3948
  if (opts.sessionId && lastUsage) {
3859
3949
  void logTurnDebug({
@@ -3881,9 +3971,11 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
3881
3971
  return;
3882
3972
  }
3883
3973
  let blockedCount = 0;
3974
+ const availableToolNames = new Set(opts.tools.map((tool) => tool.name));
3975
+ if (codeMode) availableToolNames.add("execute_code");
3884
3976
  const allReadOnly = toolCalls.length > 1 && toolCalls.every((tc) => {
3885
3977
  const tool = opts.executor.list().find((t) => t.name === tc.function.name);
3886
- return tool?.isReadOnly === true;
3978
+ return availableToolNames.has(tc.function.name) && tool?.isReadOnly === true;
3887
3979
  });
3888
3980
  if (allReadOnly) {
3889
3981
  opts.callbacks.onInfo?.(`${toolCalls.length} read-only tools running in parallel`);
@@ -4123,6 +4215,26 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
4123
4215
  for (const [i, tc] of toolCalls.entries()) {
4124
4216
  if (opts.signal.aborted) throw new DOMException("aborted", "AbortError");
4125
4217
  const loopSignature = `${tc.function.name}:${stableStringify(tc.function.arguments)}`;
4218
+ if (!availableToolNames.has(tc.function.name)) {
4219
+ const unavailableResult = {
4220
+ tool_call_id: tc.id,
4221
+ name: tc.function.name,
4222
+ content: `Tool ${tc.function.name} is not available under this turn's policy. Continue without it or ask the user to enable it.`,
4223
+ ok: false
4224
+ };
4225
+ toolResults.push(unavailableResult);
4226
+ opts.messages.push({
4227
+ role: "tool",
4228
+ tool_call_id: tc.id,
4229
+ content: sanitizeString(unavailableResult.content),
4230
+ name: tc.function.name
4231
+ });
4232
+ opts.callbacks.onToolResult?.(unavailableResult);
4233
+ recentToolCalls.push(loopSignature);
4234
+ if (recentToolCalls.length > LOOP_WINDOW) recentToolCalls.shift();
4235
+ blockedCount++;
4236
+ continue;
4237
+ }
4126
4238
  const loopCount = recentToolCalls.filter((s) => s === loopSignature).length;
4127
4239
  if (loopCount >= LOOP_THRESHOLD) {
4128
4240
  const warning = `Loop detected: you have called ${tc.function.name} with the same arguments multiple times in a row. Consider a different approach.`;
@@ -4485,30 +4597,30 @@ ${sandboxResult.output}` : sandboxResult.output;
4485
4597
  throw new BudgetExhaustedError();
4486
4598
  }
4487
4599
  if (loopExhausted) {
4488
- if (opts.callbacks.onLoopDetected) {
4489
- const decision = await opts.callbacks.onLoopDetected();
4490
- if (decision === "continue") {
4491
- opts.messages.push({
4492
- role: "system",
4493
- content: "You were stuck calling the same tools with identical arguments. The guardrail has been reset so you can continue. Try a different approach."
4494
- });
4495
- loopExhausted = false;
4496
- recentToolCalls.length = 0;
4497
- continue;
4498
- }
4499
- if (decision === "synthesize") {
4500
- opts.messages.push({
4501
- role: "system",
4502
- content: "You were stuck calling the same tools with identical arguments. Please synthesize and conclude your findings so far. Do not call any more tools."
4503
- });
4504
- loopExhausted = false;
4505
- recentToolCalls.length = 0;
4506
- continue;
4507
- }
4508
- await fireStopHook();
4509
- return;
4600
+ loopExhausted = false;
4601
+ recentToolCalls.length = 0;
4602
+ if (loopRecoveries < MAX_LOOP_RECOVERIES) {
4603
+ loopRecoveries++;
4604
+ opts.messages.push({
4605
+ role: "system",
4606
+ content: "Every tool call in your last step was blocked because you kept repeating the same calls with identical arguments. Do not repeat them. Either take a genuinely different approach (different tools, arguments, or files), or, if you already have enough information, stop calling tools and give your final answer."
4607
+ });
4608
+ opts.callbacks.onGuardrail?.({
4609
+ kind: "loop_recovery",
4610
+ message: "Loop guardrail: repeated tool calls were blocked \u2014 asked the agent to change approach."
4611
+ });
4612
+ continue;
4510
4613
  }
4511
- throw new AgentLoopError();
4614
+ finalizeReason = "loop";
4615
+ opts.messages.push({
4616
+ role: "system",
4617
+ content: "You are still repeating blocked tool calls after being asked to change approach, so this turn is ending. Do not call any more tools. Summarize what you accomplished, what is blocking you, and what the user could do next."
4618
+ });
4619
+ opts.callbacks.onGuardrail?.({
4620
+ kind: "loop_stopped",
4621
+ message: "Loop guardrail: the agent kept repeating blocked tool calls \u2014 ending the turn with a summary."
4622
+ });
4623
+ continue;
4512
4624
  }
4513
4625
  }
4514
4626
  }
@@ -4875,15 +4987,15 @@ function makeErrorOutput(message) {
4875
4987
  return { content: message, rawBytes: bytes, reducedBytes: bytes };
4876
4988
  }
4877
4989
  async function execOnce(command, cwd) {
4878
- return new Promise((resolve4) => {
4990
+ return new Promise((resolve6) => {
4879
4991
  const child = spawn("bash", ["-lc", command], { cwd });
4880
4992
  let out = "";
4881
4993
  child.stdout.on("data", (d) => {
4882
4994
  out += d.toString("utf8");
4883
4995
  });
4884
4996
  child.on("close", (code) => {
4885
- if (code !== 0) return resolve4(void 0);
4886
- resolve4(out.trim() || void 0);
4997
+ if (code !== 0) return resolve6(void 0);
4998
+ resolve6(out.trim() || void 0);
4887
4999
  });
4888
5000
  });
4889
5001
  }
@@ -5021,7 +5133,7 @@ async function runBash(args, ctx) {
5021
5133
  if (pushGuard) return pushGuard;
5022
5134
  const { shell, args: shellArgs, isPosix } = getShellCommand(ctx.shell);
5023
5135
  const command = isPosix ? injectCoauthor(args.command, ctx.coauthor) : args.command;
5024
- return new Promise((resolve4, reject) => {
5136
+ return new Promise((resolve6, reject) => {
5025
5137
  logger.debug("bash:spawn", { command: args.command.slice(0, 200), cwd: ctx.cwd, shell });
5026
5138
  const child = spawn(shell, [...shellArgs, command], {
5027
5139
  cwd: ctx.cwd,
@@ -5030,6 +5142,7 @@ async function runBash(args, ctx) {
5030
5142
  GIT_EDITOR: "true"
5031
5143
  }
5032
5144
  });
5145
+ child.stdin?.end();
5033
5146
  let stdout = "";
5034
5147
  let stderr = "";
5035
5148
  let killedByTimeout = false;
@@ -5073,8 +5186,18 @@ ${stdout.trimEnd()}`);
5073
5186
  if (stderr) parts.push(`--- stderr ---
5074
5187
  ${stderr.trimEnd()}`);
5075
5188
  if (!stdout && !stderr) parts.push("(no output)");
5189
+ if (userShellCommandsEnabled() && !killedByAbort) {
5190
+ const hint = interactiveCommandHint({
5191
+ command: args.command,
5192
+ exitCode: code,
5193
+ timedOut: killedByTimeout,
5194
+ output: `${stdout}
5195
+ ${stderr}`
5196
+ });
5197
+ if (hint) parts.push(hint);
5198
+ }
5076
5199
  const raw = parts.join("\n");
5077
- resolve4({
5200
+ resolve6({
5078
5201
  content: raw,
5079
5202
  rawBytes: Buffer.byteLength(raw, "utf8"),
5080
5203
  reducedBytes: Buffer.byteLength(raw, "utf8")
@@ -6419,7 +6542,7 @@ var browserFetchTool = {
6419
6542
  };
6420
6543
  async function autoScroll(page) {
6421
6544
  await page.evaluate(async () => {
6422
- await new Promise((resolve4) => {
6545
+ await new Promise((resolve6) => {
6423
6546
  let totalHeight = 0;
6424
6547
  const distance = 300;
6425
6548
  const timer2 = setInterval(() => {
@@ -6428,12 +6551,12 @@ async function autoScroll(page) {
6428
6551
  totalHeight += distance;
6429
6552
  if (totalHeight >= scrollHeight) {
6430
6553
  clearInterval(timer2);
6431
- resolve4();
6554
+ resolve6();
6432
6555
  }
6433
6556
  }, 100);
6434
6557
  setTimeout(() => {
6435
6558
  clearInterval(timer2);
6436
- resolve4();
6559
+ resolve6();
6437
6560
  }, 1e4);
6438
6561
  });
6439
6562
  });
@@ -6658,9 +6781,13 @@ function readReasoningEffortEnv() {
6658
6781
  const raw = process.env.KIMI_REASONING_EFFORT?.toLowerCase();
6659
6782
  return EFFORTS.includes(raw ?? "") ? raw : void 0;
6660
6783
  }
6784
+ function readSubagentPolicyEnv() {
6785
+ const raw = process.env.KIMIFLARE_SUBAGENT_POLICY?.toLowerCase();
6786
+ return raw === "off" || raw === "suggest" || raw === "auto" ? raw : void 0;
6787
+ }
6661
6788
  function readCoauthorEnv() {
6662
- const enabled = process.env.KIMIFLARE_COAUTHOR;
6663
- if (enabled === "0" || enabled === "false") return void 0;
6789
+ const enabled2 = process.env.KIMIFLARE_COAUTHOR;
6790
+ if (enabled2 === "0" || enabled2 === "false") return void 0;
6664
6791
  const name = process.env.KIMIFLARE_COAUTHOR_NAME || "kimiflare";
6665
6792
  const email = process.env.KIMIFLARE_COAUTHOR_EMAIL || "kimiflare@proton.me";
6666
6793
  return { enabled: true, name, email };
@@ -6694,6 +6821,7 @@ async function loadConfig() {
6694
6821
  if (!openrouterApiKey && !baseUrl) return null;
6695
6822
  const envModel = process.env.KIMI_MODEL || void 0;
6696
6823
  const envEffort = readReasoningEffortEnv();
6824
+ const envSubagentPolicy = readSubagentPolicyEnv();
6697
6825
  const envCoauthor = readCoauthorEnv();
6698
6826
  const envCacheStable = process.env.KIMIFLARE_CACHE_STABLE_PROMPTS;
6699
6827
  const cacheStablePrompts = envCacheStable === "0" || envCacheStable === "false" ? false : true;
@@ -6714,6 +6842,7 @@ async function loadConfig() {
6714
6842
  model: m(envModel ?? persisted.model) ?? DEFAULT_MODEL,
6715
6843
  openrouterProvider: persisted.openrouterProvider,
6716
6844
  reasoningEffort: envEffort ?? persisted.reasoningEffort,
6845
+ subagentPolicy: envSubagentPolicy ?? (persisted.subagentPolicy === "off" || persisted.subagentPolicy === "suggest" || persisted.subagentPolicy === "auto" ? persisted.subagentPolicy : "suggest"),
6717
6846
  modesEnabled: persisted.modesEnabled,
6718
6847
  coauthor: envCoauthor?.enabled ?? persisted.coauthor ?? true,
6719
6848
  coauthorName: envCoauthor?.name ?? persisted.coauthorName,
@@ -6722,6 +6851,7 @@ async function loadConfig() {
6722
6851
  cacheStablePrompts: persisted.cacheStablePrompts ?? cacheStablePrompts,
6723
6852
  compiledContext: persisted.compiledContext ?? compiledContext,
6724
6853
  imageHistoryTurns: imageHistoryTurns === void 0 || Number.isNaN(imageHistoryTurns) ? persisted.imageHistoryTurns : imageHistoryTurns,
6854
+ maxTotalToolIterations: readNumberEnv("KIMIFLARE_MAX_TOTAL_TOOL_ITERATIONS") ?? persisted.maxTotalToolIterations,
6725
6855
  memoryEnabled: readBooleanEnv("KIMIFLARE_MEMORY_ENABLED") ?? persisted.memoryEnabled ?? false,
6726
6856
  memoryDbPath: process.env.KIMIFLARE_MEMORY_DB_PATH ?? persisted.memoryDbPath,
6727
6857
  memoryMaxAgeDays: readNumberEnv("KIMIFLARE_MEMORY_MAX_AGE_DAYS") ?? persisted.memoryMaxAgeDays,
@@ -6746,6 +6876,7 @@ async function loadConfig() {
6746
6876
  githubTokenExpiry: persisted.githubTokenExpiry,
6747
6877
  githubRepo: persisted.githubRepo,
6748
6878
  workerEndpoint: process.env.KIMIFLARE_WORKER_ENDPOINT ?? persisted.workerEndpoint,
6879
+ workerBackend: process.env.KIMIFLARE_WORKER_BACKEND === "hotcell" ? "hotcell" : process.env.KIMIFLARE_WORKER_BACKEND === "remote" ? "remote" : persisted.workerBackend === "hotcell" ? "hotcell" : "remote",
6749
6880
  workerBudgetUsd: readNumberEnv("KIMIFLARE_WORKER_BUDGET_USD") ?? persisted.workerBudgetUsd,
6750
6881
  workerBudgetMaxUsd: readNumberEnv("KIMIFLARE_WORKER_BUDGET_MAX_USD") ?? persisted.workerBudgetMaxUsd,
6751
6882
  workerMaxParallel: readNumberEnv("KIMIFLARE_WORKER_MAX_PARALLEL") ?? persisted.workerMaxParallel,
@@ -6851,6 +6982,330 @@ async function saveConfig(cfg) {
6851
6982
  return p;
6852
6983
  }
6853
6984
 
6985
+ // src/tools/hotcell-worker.ts
6986
+ import { spawn as spawn2 } from "child_process";
6987
+ import { promisify as promisify2 } from "util";
6988
+ import { execFile as execFile2 } from "child_process";
6989
+ import { resolve as resolve3 } from "path";
6990
+ var execFileAsync = promisify2(execFile2);
6991
+ var MAX_OUTPUT_BYTES = 1e6;
6992
+ var MAX_PROMPT_CHARS = 4e4;
6993
+ var DEFAULT_CELL_TIMEOUT_MS = 3e5;
6994
+ var MAX_PARALLEL_WORKERS = 3;
6995
+ var runningWorkers = 0;
6996
+ var workerQueue = [];
6997
+ async function runHotcellWorker(options) {
6998
+ const workerId = `hotcell-${crypto.randomUUID().slice(0, 8)}`;
6999
+ const cwd = resolve3(options.cwd ?? process.cwd());
7000
+ const model = options.model.trim();
7001
+ if (!model) throw new Error("Hotcell workers require the coordinator's explicit model ID.");
7002
+ validateModelId(model);
7003
+ if (options.task.length + (options.context?.length ?? 0) > MAX_PROMPT_CHARS) {
7004
+ throw new Error(`Hotcell worker prompt exceeds the ${MAX_PROMPT_CHARS.toLocaleString()} character limit.`);
7005
+ }
7006
+ if (!Number.isFinite(options.budgetUsd) || options.budgetUsd <= 0) {
7007
+ throw new Error("Hotcell worker spend cap must be a positive number.");
7008
+ }
7009
+ if (options.signal?.aborted) return terminalResult(workerId, options.task, "cancelled", "Cancelled before worker setup.");
7010
+ await acquireSlot(options.maxParallel ?? MAX_PARALLEL_WORKERS, options.signal);
7011
+ let cellId;
7012
+ let result;
7013
+ const command = options.hotcellCommand ?? process.env.HOTCELL_BIN ?? "hotcell";
7014
+ const execute = options.processRunner ?? runProcess;
7015
+ const timeoutMs = options.timeoutMs ?? DEFAULT_CELL_TIMEOUT_MS;
7016
+ try {
7017
+ const repo = await getCleanRepository(cwd);
7018
+ const cellName = "autopilot-" + workerId;
7019
+ const created = await execute(command, [
7020
+ "create",
7021
+ "-n",
7022
+ "1",
7023
+ "--name",
7024
+ cellName,
7025
+ "--repo",
7026
+ repo.url,
7027
+ "--ref",
7028
+ repo.ref,
7029
+ "--egress",
7030
+ "--egress-spend-cap",
7031
+ String(options.budgetUsd),
7032
+ "--memory",
7033
+ "1024",
7034
+ "--cpus",
7035
+ "2"
7036
+ ], { cwd, signal: options.signal, timeoutMs: 3e5 });
7037
+ cellId = parseCellId(created.stdout);
7038
+ if (!cellId) {
7039
+ try {
7040
+ const cells = await execute(command, ["ls"], { cwd, timeoutMs: 1e4 });
7041
+ cellId = parseNamedCellId(cells.stdout, cellName);
7042
+ } catch {
7043
+ }
7044
+ }
7045
+ if (created.timedOut || created.aborted) {
7046
+ result = terminalResult(
7047
+ workerId,
7048
+ options.task,
7049
+ options.signal?.aborted ? "cancelled" : "timed_out",
7050
+ options.signal?.aborted ? "Cancelled during Hotcell setup." : "Hotcell setup timed out before the worker started.",
7051
+ model
7052
+ );
7053
+ return result;
7054
+ }
7055
+ if (created.code !== 0) throw new Error(formatSetupFailure(created));
7056
+ if (!cellId) throw new Error("Hotcell created a sandbox but did not return a recognizable cell ID.");
7057
+ if (/no providers configured|providers:\s*\(none\)/i.test(created.stdout)) {
7058
+ result = terminalResult(workerId, options.task, "failed", "Hotcell has no OpenRouter gateway route. Run `hotcell keys add openrouter` on the daemon host, then retry.", model);
7059
+ return result;
7060
+ }
7061
+ const prompt = [
7062
+ "You are an isolated, read-only research worker. You cannot edit files, run shell commands, push, publish, or call write-capable integrations.",
7063
+ "Use the exact requested model and return concise findings, recommendations, and relevant file paths. Do not claim to have changed files.",
7064
+ `Mission:
7065
+ ${options.task}`,
7066
+ options.context ? `Coordinator context:
7067
+ ${options.context}` : ""
7068
+ ].filter(Boolean).join("\n\n");
7069
+ const encodedPrompt = Buffer.from(prompt, "utf8").toString("base64");
7070
+ const shellCommand = [
7071
+ 'GIT_DIR="$(find /workspace -mindepth 2 -maxdepth 5 -name .git -print -quit)"',
7072
+ 'test -n "$GIT_DIR"',
7073
+ 'REPO_ROOT="${GIT_DIR%/.git}"',
7074
+ `git -C "$REPO_ROOT" fetch --quiet origin ${shellQuote(repo.commit)}`,
7075
+ `git -C "$REPO_ROOT" checkout --quiet --detach ${shellQuote(repo.commit)}`,
7076
+ 'cd "$REPO_ROOT"',
7077
+ "npm ci --no-audit --no-fund --loglevel=error 1>&2",
7078
+ 'export OPENROUTER_BASE_URL="${OPENROUTER_BASE_URL%/}/v1"',
7079
+ `PROMPT="$(printf '%s' '${encodedPrompt}' | base64 -d)"`,
7080
+ `node --import tsx "$REPO_ROOT/src/index.tsx" --format json --max-input-tokens 14000 --model ${shellQuote(model)} --worker-profile research -p "$PROMPT"`
7081
+ ].join(" && ");
7082
+ const execution = await execute(command, ["exec", cellId, shellCommand, "--cwd", "/workspace"], {
7083
+ cwd,
7084
+ signal: options.signal,
7085
+ timeoutMs
7086
+ });
7087
+ const stats = await execute(command, ["stats", cellId], { cwd, timeoutMs: 3e4 }).catch(() => void 0);
7088
+ const metrics = stats?.code === 0 ? parseHotcellStats(stats.stdout) : {};
7089
+ const json = parsePrintOutput(execution.stdout);
7090
+ const summary = typeof json?.text === "string" ? json.text.trim() : execution.stdout.trim();
7091
+ const tokensUsed = typeof json?.usage?.totalTokens === "number" ? json.usage.totalTokens : 0;
7092
+ const budgetExhausted = execution.code === 42;
7093
+ const diagnostic = `${execution.stdout}
7094
+ ${execution.stderr}`.toLowerCase();
7095
+ const executionDiagnostic = sanitizeHotcellDiagnostic(execution.stderr);
7096
+ const spendExhausted = /(?:http\s*)?402|spend cap|spending limit/.test(diagnostic);
7097
+ const status = execution.timedOut ? "timed_out" : execution.aborted ? options.signal?.aborted ? "cancelled" : "failed" : spendExhausted ? "spend_exhausted" : budgetExhausted ? "budget_exhausted" : execution.code === 0 ? "completed" : "failed";
7098
+ const findings = summary ? [{ topic: "Research findings", summary: summary.slice(0, 4e4), confidence: "medium", sources: [], relevance: "high" }] : [];
7099
+ result = {
7100
+ workerId,
7101
+ status,
7102
+ task: options.task,
7103
+ findings,
7104
+ recommendations: [],
7105
+ filesRead: [],
7106
+ webSources: [],
7107
+ costUsd: metrics.costUsd ?? 0,
7108
+ tokensUsed: metrics.tokensUsed ?? tokensUsed,
7109
+ reasoning: "",
7110
+ model,
7111
+ exitCode: execution.code,
7112
+ ...budgetExhausted || spendExhausted ? { budgetExceeded: true, partialResult: findings.length > 0 } : {},
7113
+ ...execution.code !== 0 && !budgetExhausted ? {
7114
+ error: status === "timed_out" ? "Worker timed out." : status === "cancelled" ? "Worker cancelled." : spendExhausted ? "Hotcell per-cell spend cap was reached." : execution.outputLimitExceeded ? "Worker output exceeded the 1 MB result limit." : /unknown model|model.{0,30}(?:not found|invalid|unavailable|unsupported)/i.test(diagnostic) ? "The requested model is unavailable in the Hotcell OpenRouter gateway." : /no openrouter api key|openrouter.{0,30}(?:key|gateway).{0,30}(?:missing|not configured)/i.test(diagnostic) ? "Hotcell OpenRouter gateway credentials are not configured. Run `hotcell keys add openrouter` on the daemon host." : `Worker process exited with code ${execution.code}${executionDiagnostic ? `: ${executionDiagnostic}` : "."}`
7115
+ } : {}
7116
+ };
7117
+ } catch (error) {
7118
+ const message = error instanceof Error ? error.message : "Unknown Hotcell worker error.";
7119
+ result = terminalResult(workerId, options.task, options.signal?.aborted ? "cancelled" : "failed", message, model);
7120
+ } finally {
7121
+ if (cellId) {
7122
+ try {
7123
+ const removed = await execute(command, ["rm", cellId], { cwd, timeoutMs: 3e4 });
7124
+ if (removed.code !== 0 && result) {
7125
+ result = {
7126
+ ...result,
7127
+ status: result.status === "completed" ? "failed" : result.status,
7128
+ error: [result.error, "Hotcell cleanup failed; remove the cell manually to revoke its gateway token."].filter(Boolean).join(" ")
7129
+ };
7130
+ }
7131
+ } catch {
7132
+ if (result) {
7133
+ result = {
7134
+ ...result,
7135
+ status: result.status === "completed" ? "failed" : result.status,
7136
+ error: [result.error, "Hotcell cleanup failed; remove the cell manually to revoke its gateway token."].filter(Boolean).join(" ")
7137
+ };
7138
+ }
7139
+ }
7140
+ }
7141
+ releaseSlot();
7142
+ }
7143
+ return result;
7144
+ }
7145
+ async function getCleanRepository(cwd) {
7146
+ const root = (await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd })).stdout.trim();
7147
+ const status = (await execFileAsync("git", ["status", "--porcelain"], { cwd: root })).stdout;
7148
+ if (status.trim()) throw new Error("Hotcell workers require a clean Git checkout; commit or stash local changes first.");
7149
+ const commit = (await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: root })).stdout.trim();
7150
+ const ref = (await execFileAsync("git", ["branch", "--show-current"], { cwd: root })).stdout.trim();
7151
+ if (!ref) throw new Error("Hotcell workers require a named branch so the daemon can clone before pinning the exact commit.");
7152
+ const url = (await execFileAsync("git", ["config", "--get", "remote.origin.url"], { cwd: root })).stdout.trim();
7153
+ if (!/^(https:\/\/|git@|ssh:\/\/)/i.test(url) || /:\/\/[^/]*@/.test(url)) {
7154
+ throw new Error("Hotcell workers require a credential-free HTTPS or SSH origin URL that the Hotcell daemon can clone.");
7155
+ }
7156
+ return { url, commit, ref };
7157
+ }
7158
+ function parseCellId(output) {
7159
+ const uuid = output.match(/\b[a-f0-9]{8}(?:-[a-f0-9]{4,}){0,4}\b/gi)?.at(-1);
7160
+ if (uuid) return uuid;
7161
+ const labeled = output.match(/(?:sandbox|cell)(?:\s+id)?\s*[:= ]\s*([a-z0-9][a-z0-9_-]{7,79})/i)?.[1];
7162
+ if (labeled) return labeled;
7163
+ return output.split(/\r?\n/).map((line) => line.trim()).reverse().find((line) => /^[a-z0-9][a-z0-9_-]{7,79}$/i.test(line));
7164
+ }
7165
+ function parseNamedCellId(output, name) {
7166
+ for (const line of output.split(/\r?\n/)) {
7167
+ const columns = line.trim().split(/\s+/);
7168
+ if (columns[1] === name && /^[a-z0-9][a-z0-9_-]{7,79}$/i.test(columns[0] ?? "")) return columns[0];
7169
+ }
7170
+ return void 0;
7171
+ }
7172
+ function formatSetupFailure(result) {
7173
+ const detail = sanitizeHotcellDiagnostic(`${result.stderr}
7174
+ ${result.stdout}`);
7175
+ return `Hotcell setup failed (exit ${result.code})${detail ? `: ${detail}` : ". Check that the daemon, repository access, and gateway route are configured."}`;
7176
+ }
7177
+ function sanitizeHotcellDiagnostic(text) {
7178
+ return text.replace(/\b(?:sk-or-v1-|sk-ant-|gh[pousr]_|github_pat_)[A-Za-z0-9_-]{8,}\b/gi, "[REDACTED]").replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]").replace(/\b[A-Z0-9_]*(?:API[_-]?KEY|ACCESS[_-]?TOKEN|SECRET|PASSWORD)\s*[:=]\s*[^\s,;]+/gi, "[REDACTED]").trim().slice(-1500);
7179
+ }
7180
+ function shellQuote(value) {
7181
+ return `'${value.replace(/'/g, `'\\''`)}'`;
7182
+ }
7183
+ function parseHotcellStats(output) {
7184
+ const llm = output.match(/LLM:\s*([\d,]+) calls,\s*([\d,]+) in \+ ([\d,]+) out tokens,\s*\$([\d.]+)/i);
7185
+ const totalCost = output.match(/Cost:\s*\$?([\d.]+)/i);
7186
+ const tokensUsed = llm ? Number(llm[2].replaceAll(",", "")) + Number(llm[3].replaceAll(",", "")) : void 0;
7187
+ const costUsd = totalCost ? Number(totalCost[1]) : llm ? Number(llm[4]) : void 0;
7188
+ return {
7189
+ ...Number.isFinite(tokensUsed) ? { tokensUsed } : {},
7190
+ ...Number.isFinite(costUsd) ? { costUsd } : {}
7191
+ };
7192
+ }
7193
+ function parsePrintOutput(stdout) {
7194
+ const candidate = stdout.trim();
7195
+ try {
7196
+ const parsed = JSON.parse(candidate);
7197
+ if (!parsed || typeof parsed !== "object") return void 0;
7198
+ return parsed;
7199
+ } catch {
7200
+ return void 0;
7201
+ }
7202
+ }
7203
+ async function runProcess(executable, args, options) {
7204
+ if (options.signal?.aborted) return { code: 130, stdout: "", stderr: "", aborted: true };
7205
+ return await new Promise((resolvePromise, reject) => {
7206
+ const child = spawn2(executable, args, { cwd: options.cwd, stdio: ["ignore", "pipe", "pipe"] });
7207
+ const stdout = [];
7208
+ const stderr = [];
7209
+ let bytes = 0;
7210
+ let aborted2 = false;
7211
+ let timedOut = false;
7212
+ let outputLimitExceeded = false;
7213
+ let settled = false;
7214
+ const terminate = () => {
7215
+ child.kill("SIGTERM");
7216
+ const forceTimer = setTimeout(() => child.kill("SIGKILL"), 2e3);
7217
+ forceTimer.unref();
7218
+ };
7219
+ const abort = () => {
7220
+ aborted2 = true;
7221
+ terminate();
7222
+ };
7223
+ const timer2 = setTimeout(() => {
7224
+ timedOut = true;
7225
+ terminate();
7226
+ }, options.timeoutMs);
7227
+ timer2.unref();
7228
+ options.signal?.addEventListener("abort", abort, { once: true });
7229
+ const collect = (target, chunk) => {
7230
+ bytes += chunk.length;
7231
+ if (bytes > MAX_OUTPUT_BYTES) {
7232
+ outputLimitExceeded = true;
7233
+ terminate();
7234
+ return;
7235
+ }
7236
+ target.push(chunk);
7237
+ };
7238
+ child.stdout.on("data", (chunk) => collect(stdout, chunk));
7239
+ child.stderr.on("data", (chunk) => collect(stderr, chunk));
7240
+ child.once("error", (error) => {
7241
+ if (settled) return;
7242
+ settled = true;
7243
+ clearTimeout(timer2);
7244
+ options.signal?.removeEventListener("abort", abort);
7245
+ reject(error);
7246
+ });
7247
+ child.once("close", (code) => {
7248
+ if (settled) return;
7249
+ settled = true;
7250
+ clearTimeout(timer2);
7251
+ options.signal?.removeEventListener("abort", abort);
7252
+ resolvePromise({
7253
+ code: code ?? (aborted2 ? 130 : 1),
7254
+ stdout: Buffer.concat(stdout).toString("utf8"),
7255
+ stderr: Buffer.concat(stderr).toString("utf8"),
7256
+ aborted: aborted2,
7257
+ timedOut,
7258
+ outputLimitExceeded
7259
+ });
7260
+ });
7261
+ });
7262
+ }
7263
+ function terminalResult(workerId, task, status, error, model) {
7264
+ return {
7265
+ workerId,
7266
+ status,
7267
+ task,
7268
+ findings: [],
7269
+ recommendations: [],
7270
+ filesRead: [],
7271
+ webSources: [],
7272
+ costUsd: 0,
7273
+ tokensUsed: 0,
7274
+ reasoning: "",
7275
+ ...model ? { model } : {},
7276
+ error
7277
+ };
7278
+ }
7279
+ async function acquireSlot(limit, signal) {
7280
+ const cap = Math.max(1, Math.min(MAX_PARALLEL_WORKERS, Math.floor(limit) || 1));
7281
+ if (runningWorkers < cap) {
7282
+ runningWorkers++;
7283
+ return;
7284
+ }
7285
+ await new Promise((resolvePromise, reject) => {
7286
+ const wake = () => {
7287
+ if (signal?.aborted) {
7288
+ const index = workerQueue.indexOf(wake);
7289
+ if (index >= 0) workerQueue.splice(index, 1);
7290
+ reject(new Error("Hotcell worker cancelled while waiting for a slot."));
7291
+ return;
7292
+ }
7293
+ if (runningWorkers < cap) {
7294
+ const index = workerQueue.indexOf(wake);
7295
+ if (index >= 0) workerQueue.splice(index, 1);
7296
+ runningWorkers++;
7297
+ resolvePromise();
7298
+ }
7299
+ };
7300
+ workerQueue.push(wake);
7301
+ signal?.addEventListener("abort", wake, { once: true });
7302
+ });
7303
+ }
7304
+ function releaseSlot() {
7305
+ runningWorkers = Math.max(0, runningWorkers - 1);
7306
+ for (const wake of workerQueue.slice()) wake();
7307
+ }
7308
+
6854
7309
  // src/tools/spawn-worker.ts
6855
7310
  var DEFAULT_WORKER_TIMEOUT_MS = 3e5;
6856
7311
  async function callWorkerEndpoint(endpoint, apiKey, payload, signal, timeoutMs = DEFAULT_WORKER_TIMEOUT_MS) {
@@ -6886,10 +7341,9 @@ async function callWorkerEndpoint(endpoint, apiKey, payload, signal, timeoutMs =
6886
7341
  var spawnWorkerTool = {
6887
7342
  name: "spawn_worker",
6888
7343
  description: [
6889
- "Spawn a standalone remote worker agent to perform research or execute a plan.",
6890
- "Workers run independently with their own full context window and tool access.",
6891
- "Mode 'plan': read-only research worker that returns structured findings.",
6892
- "Mode 'execute': write-enabled worker that creates a branch, implements changes, and opens a PR.",
7344
+ "Spawn a standalone worker using the configured remote endpoint or an opt-in local Hotcell sandbox.",
7345
+ "Mode 'plan': read-only research. Local Hotcell workers use a clean committed revision and cannot edit files.",
7346
+ "Mode 'execute': remote write + PR only; local Hotcell execute mode is intentionally unavailable.",
6893
7347
  "Use for heavy tasks that benefit from parallel research (e.g. 'research OAuth2, testing, and migration')."
6894
7348
  ].join(" "),
6895
7349
  parameters: {
@@ -6922,11 +7376,11 @@ var spawnWorkerTool = {
6922
7376
  tools: {
6923
7377
  type: "string",
6924
7378
  enum: ["all", "read-only"],
6925
- description: "Tool set available to the worker. Default 'all' for plan mode, ignored for execute."
7379
+ description: "Requested remote tool set. Local Hotcell research always uses a narrow read-only profile and rejects `all`."
6926
7380
  },
6927
7381
  model: {
6928
7382
  type: "string",
6929
- description: `Model to use for the worker. Defaults to the session's model (${DEFAULT_MODEL} if unset).`
7383
+ description: "Model to use for the worker. Defaults to the active session model."
6930
7384
  },
6931
7385
  branchName: {
6932
7386
  type: "string",
@@ -6954,25 +7408,71 @@ var spawnWorkerTool = {
6954
7408
  body: args.task.slice(0, 200)
6955
7409
  }),
6956
7410
  async run(args, ctx) {
6957
- const endpoint = process.env.KIMIFLARE_WORKER_ENDPOINT;
7411
+ const cfg = await loadConfig().catch(() => null);
7412
+ const timeoutMs = cfg?.workerTimeoutMs ?? readNumberEnv2("KIMIFLARE_WORKER_TIMEOUT_MS") ?? DEFAULT_WORKER_TIMEOUT_MS;
7413
+ const budgetCeiling = resolveWorkerBudgetUsd(cfg);
7414
+ if (cfg?.workerBackend === "hotcell") {
7415
+ if (cfg.baseUrl) {
7416
+ return textOutput("Hotcell workers currently require an OpenRouter-backed session; custom model endpoints cannot be routed through the Hotcell gateway.");
7417
+ }
7418
+ if (args.mode !== "plan") {
7419
+ return textOutput("Hotcell workers currently support read-only plan mode only. Execute mode is disabled until reviewed patch artifacts are supported.");
7420
+ }
7421
+ if (args.tools === "all") {
7422
+ return textOutput("Hotcell research workers are restricted to read-only tools; the requested broader toolset is not available.");
7423
+ }
7424
+ const model = args.model ?? ctx.model;
7425
+ if (!model) {
7426
+ return textOutput("Hotcell worker requires the coordinator's active model ID. No model was provided; refusing to fall back to a default.");
7427
+ }
7428
+ const requestedBudget = args.budget?.maxCostUsd ?? budgetCeiling;
7429
+ const budgetUsd = Math.min(requestedBudget, budgetCeiling);
7430
+ if (!Number.isFinite(budgetUsd) || budgetUsd <= 0) {
7431
+ return textOutput("Hotcell worker spend cap must be a positive number.");
7432
+ }
7433
+ try {
7434
+ const result = await runHotcellWorker({
7435
+ task: args.task,
7436
+ context: args.context,
7437
+ model,
7438
+ budgetUsd,
7439
+ timeoutMs,
7440
+ maxParallel: cfg?.workerMaxParallel,
7441
+ cwd: ctx.cwd,
7442
+ signal: ctx.signal
7443
+ });
7444
+ if (result.status !== "completed" && result.status !== "budget_exhausted") {
7445
+ return textOutput(`Hotcell worker ${result.status}: ${result.error ?? "unknown error"}`);
7446
+ }
7447
+ const lines = [
7448
+ `Hotcell worker ${result.status}${result.status === "budget_exhausted" ? " (partial result)" : ""}.`,
7449
+ `Model: ${result.model ?? model} \xB7 Tokens: ${result.tokensUsed.toLocaleString()} \xB7 Cost: ${result.costUsd > 0 ? `${result.costUsd.toFixed(4)} total Hotcell cost` : "unavailable from Hotcell stats"}.`,
7450
+ ...result.findings.map((finding) => `
7451
+ ## ${finding.topic}
7452
+ ${finding.summary}`)
7453
+ ];
7454
+ if (result.status === "budget_exhausted") {
7455
+ lines.push("\nWorker input-token budget was exhausted; review partial findings before relying on them.");
7456
+ }
7457
+ return textOutput(lines.join("\n"));
7458
+ } catch (error) {
7459
+ return textOutput(`Failed to spawn Hotcell worker: ${error instanceof Error ? error.message : String(error)}`);
7460
+ }
7461
+ }
7462
+ const endpoint = process.env.KIMIFLARE_WORKER_ENDPOINT ?? cfg?.workerEndpoint ?? cfg?.remoteWorkerUrl;
6958
7463
  if (!endpoint) {
6959
- const msg = "Worker endpoint not configured. Set KIMIFLARE_WORKER_ENDPOINT or workerEndpoint in config.";
6960
- const bytes = Buffer.byteLength(msg, "utf8");
6961
- return { content: msg, rawBytes: bytes, reducedBytes: bytes };
7464
+ return textOutput("Worker endpoint not configured. Set KIMIFLARE_WORKER_ENDPOINT or workerEndpoint in config, or set workerBackend to hotcell.");
6962
7465
  }
6963
7466
  const apiKey = process.env.KIMIFLARE_WORKER_API_KEY;
6964
- const timeoutMs = readNumberEnv2("KIMIFLARE_WORKER_TIMEOUT_MS") ?? DEFAULT_WORKER_TIMEOUT_MS;
6965
- const cfg = await loadConfig().catch(() => null);
6966
- const budgetUsd = resolveWorkerBudgetUsd(cfg);
6967
7467
  const defaultModel = cfg?.model ?? DEFAULT_MODEL;
6968
7468
  const payload = {
6969
7469
  mode: args.mode,
6970
7470
  task: args.task,
6971
7471
  context: args.context ?? "",
6972
- budget: { maxCostUsd: budgetUsd },
7472
+ budget: { maxCostUsd: budgetCeiling },
6973
7473
  outputFormat: args.outputFormat ?? "structured",
6974
7474
  tools: args.tools ?? (args.mode === "plan" ? "read-only" : "all"),
6975
- model: args.model ?? defaultModel,
7475
+ model: args.model ?? ctx.model ?? defaultModel,
6976
7476
  ...args.mode === "execute" ? {
6977
7477
  branchName: args.branchName,
6978
7478
  baseBranch: args.baseBranch ?? "main",
@@ -7026,6 +7526,10 @@ var spawnWorkerTool = {
7026
7526
  }
7027
7527
  }
7028
7528
  };
7529
+ function textOutput(content) {
7530
+ const bytes = Buffer.byteLength(content, "utf8");
7531
+ return { content, rawBytes: bytes, reducedBytes: bytes };
7532
+ }
7029
7533
  function readNumberEnv2(name) {
7030
7534
  const raw = process.env[name];
7031
7535
  if (!raw) return void 0;
@@ -7639,6 +8143,8 @@ function makeExpandArtifactTool(store) {
7639
8143
  }
7640
8144
 
7641
8145
  // src/tools/executor.ts
8146
+ import { realpath as realpath2 } from "fs/promises";
8147
+ import { isAbsolute as isAbsolute2, relative as relative3, resolve as resolve4, sep as sep2 } from "path";
7642
8148
  var ALL_TOOLS = [
7643
8149
  { ...readTool, isReadOnly: true },
7644
8150
  writeTool,
@@ -8522,7 +9028,7 @@ Context: This memory was explicitly provided by the user during a conversation.`
8522
9028
  };
8523
9029
 
8524
9030
  // src/lsp/connection.ts
8525
- import { spawn as spawn2 } from "child_process";
9031
+ import { spawn as spawn3 } from "child_process";
8526
9032
  import { EventEmitter } from "events";
8527
9033
  var LspConnection = class extends EventEmitter {
8528
9034
  child = null;
@@ -8539,7 +9045,7 @@ var LspConnection = class extends EventEmitter {
8539
9045
  if (this.child) {
8540
9046
  throw new Error("LSP connection already started");
8541
9047
  }
8542
- return new Promise((resolve4, reject) => {
9048
+ return new Promise((resolve6, reject) => {
8543
9049
  const abortController = new AbortController();
8544
9050
  const spawnTimer = setTimeout(() => {
8545
9051
  abortController.abort();
@@ -8547,7 +9053,7 @@ var LspConnection = class extends EventEmitter {
8547
9053
  reject(new Error(`LSP server spawn timed out after ${spawnTimeoutMs}ms`));
8548
9054
  }, spawnTimeoutMs);
8549
9055
  try {
8550
- const child = spawn2(command[0], command.slice(1), {
9056
+ const child = spawn3(command[0], command.slice(1), {
8551
9057
  env: { ...process.env, ...env },
8552
9058
  stdio: ["pipe", "pipe", "pipe"]
8553
9059
  });
@@ -8578,7 +9084,7 @@ var LspConnection = class extends EventEmitter {
8578
9084
  if (child.pid) {
8579
9085
  clearTimeout(spawnTimer);
8580
9086
  this.child = child;
8581
- resolve4();
9087
+ resolve6();
8582
9088
  }
8583
9089
  });
8584
9090
  } catch (err) {
@@ -8593,12 +9099,12 @@ var LspConnection = class extends EventEmitter {
8593
9099
  }
8594
9100
  const id = this.nextId++;
8595
9101
  const msg = { jsonrpc: "2.0", id, method, params };
8596
- return new Promise((resolve4, reject) => {
9102
+ return new Promise((resolve6, reject) => {
8597
9103
  const timer2 = setTimeout(() => {
8598
9104
  this.pending.delete(id);
8599
9105
  reject(toolTimeoutError(`LSP request '${method}'`, this.requestTimeoutMs));
8600
9106
  }, this.requestTimeoutMs);
8601
- const pending = { resolve: resolve4, reject, signal, timer: timer2 };
9107
+ const pending = { resolve: resolve6, reject, signal, timer: timer2 };
8602
9108
  this.pending.set(id, pending);
8603
9109
  if (signal) {
8604
9110
  const onAbort = () => {
@@ -9139,10 +9645,10 @@ var LspManager = class {
9139
9645
  };
9140
9646
 
9141
9647
  // src/lsp/adapter.ts
9142
- import { relative as relative3 } from "path";
9648
+ import { relative as relative4 } from "path";
9143
9649
  function formatLocation(loc, cwd) {
9144
9650
  const path = fromUri(loc.uri);
9145
- const rel = relative3(cwd, path) || path;
9651
+ const rel = relative4(cwd, path) || path;
9146
9652
  return `${rel}:${loc.range.start.line + 1}:${loc.range.start.character + 1}`;
9147
9653
  }
9148
9654
  function formatLocations(locs, cwd) {
@@ -9179,7 +9685,7 @@ function formatWorkspaceSymbols(symbols, cwd) {
9179
9685
  if (!symbols || symbols.length === 0) return "No symbols found.";
9180
9686
  return symbols.map((s) => {
9181
9687
  const path = fromUri(s.location.uri);
9182
- const rel = relative3(cwd, path) || path;
9688
+ const rel = relative4(cwd, path) || path;
9183
9689
  const container = s.containerName ? ` in ${s.containerName}` : "";
9184
9690
  return `${s.name} (${symbolKindName(s.kind)})${container} \u2014 ${rel}:${s.location.range.start.line + 1}:${s.location.range.start.character + 1}`;
9185
9691
  }).join("\n");
@@ -9199,7 +9705,7 @@ function formatWorkspaceEdit(edit, cwd) {
9199
9705
  if (edit.changes) {
9200
9706
  for (const [uri, edits] of Object.entries(edit.changes)) {
9201
9707
  const path = fromUri(uri);
9202
- const rel = relative3(cwd, path) || path;
9708
+ const rel = relative4(cwd, path) || path;
9203
9709
  lines.push(`File: ${rel}`);
9204
9710
  for (const e of edits) {
9205
9711
  lines.push(` ${e.range.start.line + 1}:${e.range.start.character + 1}-${e.range.end.line + 1}:${e.range.end.character + 1}: ${e.newText}`);
@@ -9218,7 +9724,7 @@ function formatCodeActions(actions) {
9218
9724
  }
9219
9725
 
9220
9726
  // src/tools/lsp.ts
9221
- import { relative as relative4 } from "path";
9727
+ import { relative as relative5 } from "path";
9222
9728
  function makeOutput2(content) {
9223
9729
  const bytes = Buffer.byteLength(content, "utf8");
9224
9730
  return { content, rawBytes: bytes, reducedBytes: bytes };
@@ -9226,7 +9732,7 @@ function makeOutput2(content) {
9226
9732
  function resolveLspPath(args, ctx) {
9227
9733
  const raw = typeof args.path === "string" ? args.path : "";
9228
9734
  const resolved = resolvePath(ctx.cwd, raw);
9229
- const rel = relative4(ctx.cwd, resolved);
9735
+ const rel = relative5(ctx.cwd, resolved);
9230
9736
  if (isPathOutside(rel)) {
9231
9737
  throw new Error(`Path outside workspace: ${raw}`);
9232
9738
  }
@@ -9505,11 +10011,11 @@ async function loadSession(filePath) {
9505
10011
  }
9506
10012
 
9507
10013
  // src/usage-tracker.ts
9508
- import { readFile as readFile8, writeFile as writeFile7, mkdir as mkdir6 } from "fs/promises";
10014
+ import { readFile as readFile8, rename as rename2 } from "fs/promises";
9509
10015
  import { homedir as homedir8 } from "os";
9510
10016
  import { join as join15 } from "path";
9511
10017
  import { EventEmitter as EventEmitter2 } from "events";
9512
- import { randomUUID } from "crypto";
10018
+ import { randomUUID as randomUUID2 } from "crypto";
9513
10019
 
9514
10020
  // src/pricing.ts
9515
10021
  var PRICE_IN_PER_M = 0.95;
@@ -9530,6 +10036,59 @@ function calculateCost(promptTokens, completionTokens, cachedTokens = 0, model)
9530
10036
  return { uncachedIn, cachedIn, out, total };
9531
10037
  }
9532
10038
 
10039
+ // src/util/atomic-file.ts
10040
+ import { mkdir as mkdir6, rename, rm, stat as stat4, writeFile as writeFile7 } from "fs/promises";
10041
+ import { dirname as dirname6 } from "path";
10042
+ import { randomUUID } from "crypto";
10043
+ async function writeFileAtomic(path, data) {
10044
+ await mkdir6(dirname6(path), { recursive: true });
10045
+ const tmp = `${path}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
10046
+ try {
10047
+ await writeFile7(tmp, data, "utf8");
10048
+ await rename(tmp, path);
10049
+ } catch (err) {
10050
+ await rm(tmp, { force: true }).catch(() => void 0);
10051
+ throw err;
10052
+ }
10053
+ }
10054
+ var LOCK_STALE_MS = 15e3;
10055
+ var LOCK_WAIT_MS = 5e3;
10056
+ var LOCK_RETRY_MS = 25;
10057
+ async function withFileLock(lockPath, fn) {
10058
+ await mkdir6(dirname6(lockPath), { recursive: true });
10059
+ const deadline = Date.now() + LOCK_WAIT_MS;
10060
+ let held = false;
10061
+ while (!held) {
10062
+ try {
10063
+ await mkdir6(lockPath);
10064
+ held = true;
10065
+ } catch (err) {
10066
+ if (err.code !== "EEXIST") break;
10067
+ const age = await stat4(lockPath).then((s) => Date.now() - s.mtimeMs, () => 0);
10068
+ if (age > LOCK_STALE_MS) {
10069
+ await rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
10070
+ continue;
10071
+ }
10072
+ if (Date.now() > deadline) break;
10073
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
10074
+ }
10075
+ }
10076
+ try {
10077
+ return await fn();
10078
+ } finally {
10079
+ if (held) await rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
10080
+ }
10081
+ }
10082
+ function recoverJsonPrefix(raw) {
10083
+ for (let i = raw.lastIndexOf("}"); i > 0; i = raw.lastIndexOf("}", i - 1)) {
10084
+ try {
10085
+ return JSON.parse(raw.slice(0, i + 1));
10086
+ } catch {
10087
+ }
10088
+ }
10089
+ return void 0;
10090
+ }
10091
+
9533
10092
  // src/usage-tracker.ts
9534
10093
  var LOG_VERSION2 = 1;
9535
10094
  var usageEvents = new EventEmitter2();
@@ -9553,21 +10112,29 @@ function cutoffDate(daysBack) {
9553
10112
  return d.toISOString().slice(0, 10);
9554
10113
  }
9555
10114
  async function loadLog() {
10115
+ let raw;
9556
10116
  try {
9557
- const raw = await readFile8(usagePath(), "utf8");
9558
- const parsed = JSON.parse(raw);
9559
- if (parsed.version === LOG_VERSION2) return parsed;
10117
+ raw = await readFile8(usagePath(), "utf8");
9560
10118
  } catch {
10119
+ return { version: LOG_VERSION2, days: [], sessions: [] };
9561
10120
  }
10121
+ let parsed;
10122
+ try {
10123
+ parsed = JSON.parse(raw);
10124
+ } catch {
10125
+ await rename2(usagePath(), `${usagePath()}.corrupt-${Date.now()}`).catch(() => void 0);
10126
+ parsed = recoverJsonPrefix(raw);
10127
+ }
10128
+ if (parsed && parsed.version === LOG_VERSION2) return parsed;
9562
10129
  return { version: LOG_VERSION2, days: [], sessions: [] };
9563
10130
  }
9564
10131
  async function saveLog(log2) {
9565
- await mkdir6(usageDir(), { recursive: true });
9566
- await writeFile7(usagePath(), JSON.stringify(log2, null, 2), "utf8");
10132
+ await writeFileAtomic(usagePath(), JSON.stringify(log2, null, 2));
9567
10133
  }
9568
10134
  var writeChain = Promise.resolve();
9569
10135
  function withLock(fn) {
9570
- const next = writeChain.then(fn, fn);
10136
+ const locked = () => withFileLock(`${usagePath()}.lock`, fn);
10137
+ const next = writeChain.then(locked, locked);
9571
10138
  writeChain = next.catch(() => void 0);
9572
10139
  return next;
9573
10140
  }
@@ -9597,8 +10164,7 @@ async function upsertHistoryDay(day) {
9597
10164
  entries.push(day);
9598
10165
  }
9599
10166
  const lines = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
9600
- await mkdir6(usageDir(), { recursive: true });
9601
- await writeFile7(historyPath(), lines, "utf8");
10167
+ await writeFileAtomic(historyPath(), lines);
9602
10168
  }
9603
10169
  function getOrCreateDay(log2, date) {
9604
10170
  let day = log2.days.find((d) => d.date === date);
@@ -9657,7 +10223,7 @@ async function recordUsage(sessionId, usage, lookup, model) {
9657
10223
  );
9658
10224
  const estimatedCost = cost.total;
9659
10225
  const cachedTokens = usage.prompt_tokens_details?.cached_tokens ?? 0;
9660
- const turnId = randomUUID();
10226
+ const turnId = randomUUID2();
9661
10227
  const generationId = lookup?.meta.generationId;
9662
10228
  const inlineCost = typeof usage.cost === "number" && Number.isFinite(usage.cost) ? usage.cost : void 0;
9663
10229
  await withLock(async () => {
@@ -9760,7 +10326,7 @@ async function reconcileTurnCost(sessionId, turnId, lookup, fetchImpl2 = fetch,
9760
10326
  // src/models/openrouter-catalog.ts
9761
10327
  import { readFile as readFile9, writeFile as writeFile8, mkdir as mkdir7 } from "fs/promises";
9762
10328
  import { homedir as homedir9 } from "os";
9763
- import { join as join16, dirname as dirname6 } from "path";
10329
+ import { join as join16, dirname as dirname7 } from "path";
9764
10330
  var CACHE_VERSION = 4;
9765
10331
  var DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
9766
10332
  function catalogCachePath() {
@@ -9826,7 +10392,7 @@ async function readCache() {
9826
10392
  async function writeCache(models) {
9827
10393
  const path = catalogCachePath();
9828
10394
  try {
9829
- await mkdir7(dirname6(path), { recursive: true });
10395
+ await mkdir7(dirname7(path), { recursive: true });
9830
10396
  const cache = { version: CACHE_VERSION, fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), models };
9831
10397
  await writeFile8(path, JSON.stringify(cache, null, 2), "utf8");
9832
10398
  } catch {
@@ -9899,7 +10465,7 @@ function createDefaultPermissionHandler(options) {
9899
10465
  async function createAgentSession(opts) {
9900
10466
  const config2 = await resolveSdkConfig(opts);
9901
10467
  if (!resolveCustomEndpoint(config2)) await ensureOpenRouterCatalog();
9902
- const cwd = resolve3(opts.cwd ?? process.cwd());
10468
+ const cwd = resolve5(opts.cwd ?? process.cwd());
9903
10469
  const tools = opts.tools ?? ALL_TOOLS;
9904
10470
  let hooks;
9905
10471
  if (opts.enableHooks) {
@@ -10261,6 +10827,10 @@ var InternalSession = class {
10261
10827
  onWarning: (msg) => {
10262
10828
  this.emit({ type: "warning", message: msg });
10263
10829
  },
10830
+ onGuardrail: (ev) => {
10831
+ this.emit({ type: "warning", message: ev.message });
10832
+ if (ev.kind === "limit_stopped") this.emit({ type: "status", status: "error" });
10833
+ },
10264
10834
  askPermission: async (req) => {
10265
10835
  if (mode === "auto") return "allow";
10266
10836
  if (mode === "plan") {
@@ -10280,12 +10850,12 @@ var InternalSession = class {
10280
10850
  toolName: req.tool.name,
10281
10851
  args: req.args
10282
10852
  });
10283
- const decision = await new Promise((resolve4) => {
10284
- this.permissionResolvers.set(requestId, resolve4);
10853
+ const decision = await new Promise((resolve6) => {
10854
+ this.permissionResolvers.set(requestId, resolve6);
10285
10855
  setTimeout(() => {
10286
10856
  if (this.permissionResolvers.has(requestId)) {
10287
10857
  this.permissionResolvers.delete(requestId);
10288
- resolve4("deny");
10858
+ resolve6("deny");
10289
10859
  }
10290
10860
  }, 3e5);
10291
10861
  });
@@ -10296,10 +10866,6 @@ var InternalSession = class {
10296
10866
  });
10297
10867
  return decision;
10298
10868
  },
10299
- onToolLimitReached: async () => {
10300
- this.emit({ type: "status", status: "error" });
10301
- return "stop";
10302
- },
10303
10869
  onKimiMdStale: () => {
10304
10870
  this.onKimiMdStale?.();
10305
10871
  }
@@ -10317,6 +10883,9 @@ var InternalSession = class {
10317
10883
  hooks: this.hooks,
10318
10884
  // M6.1: Stop fires at end-of-turn when enabled.
10319
10885
  maxToolIterations,
10886
+ // SDK callers bound turns with `maxToolIterations`: stop cleanly at the
10887
+ // limit rather than resetting (never waits on a prompt either way).
10888
+ toolLimitBehavior: "stop",
10320
10889
  reasoningEffort: this.reasoningEffort,
10321
10890
  coauthor,
10322
10891
  sessionId: this.sessionId,