autopilot-ai 1.13.0 → 1.14.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/index.js CHANGED
@@ -1832,9 +1832,14 @@ function shouldCompact(opts2) {
1832
1832
  const { turns } = groupIntoTurns(opts2.messages);
1833
1833
  return tokens > tokenThreshold || turns.length > turnThreshold;
1834
1834
  }
1835
+ function isCompiledStateMessage(m) {
1836
+ return m.role === "system" && typeof m.content === "string" && m.content.startsWith("[compiled session state]");
1837
+ }
1835
1838
  function compactMessagesViaArtifacts(opts2) {
1836
1839
  const keepLastTurns = opts2.keepLastTurns ?? 4;
1837
- const { prefix, turns } = groupIntoTurns(opts2.messages);
1840
+ const grouped = groupIntoTurns(opts2.messages);
1841
+ const turns = grouped.turns;
1842
+ const prefix = grouped.prefix.filter((m) => !isCompiledStateMessage(m));
1838
1843
  const tokensBefore = estimatePromptTokens(opts2.messages);
1839
1844
  if (turns.length <= keepLastTurns) {
1840
1845
  return {
@@ -4530,12 +4535,7 @@ ${opts2.lspContext}` : "";
4530
4535
  The following MCP server information was pre-computed by the coordinator.
4531
4536
 
4532
4537
  ${opts2.mcpContext}` : "";
4533
- const delegationBlock = opts2.delegationDirective ? `
4534
-
4535
- ## Subagent policy for this turn
4536
-
4537
- ${opts2.delegationDirective}` : "";
4538
- return identity + "\n\n" + env2 + "\n\n" + tools + lspBlock + userShellBlock + contextBlock + modeBlock + skillsBlock + memoryBlock + lspContextBlock + mcpContextBlock + delegationBlock;
4538
+ return identity + "\n\n" + env2 + "\n\n" + tools + lspBlock + userShellBlock + contextBlock + modeBlock + skillsBlock + memoryBlock + lspContextBlock + mcpContextBlock;
4539
4539
  }
4540
4540
  function buildSystemPrompt(opts2) {
4541
4541
  return buildStaticPrefix(opts2) + "\n\n" + buildSessionPrefix(opts2);
@@ -4557,6 +4557,139 @@ var init_system_prompt = __esm({
4557
4557
  }
4558
4558
  });
4559
4559
 
4560
+ // src/agent/permission-gate.ts
4561
+ function createPermissionGate(ask, calls) {
4562
+ const decisions = /* @__PURE__ */ new Map();
4563
+ let tail = Promise.resolve();
4564
+ return (req) => {
4565
+ const existing = decisions.get(req.sessionKey);
4566
+ if (existing) return existing;
4567
+ const sameTool = calls.filter((call) => call.name === req.tool.name);
4568
+ const request = sameTool.length > 1 ? { ...req, args: { ...req.args, batch: sameTool.map((call) => call.args) } } : req;
4569
+ const decision = tail.then(() => ask(request));
4570
+ tail = decision.catch(() => void 0);
4571
+ decisions.set(req.sessionKey, decision);
4572
+ return decision;
4573
+ };
4574
+ }
4575
+ var init_permission_gate = __esm({
4576
+ "src/agent/permission-gate.ts"() {
4577
+ "use strict";
4578
+ }
4579
+ });
4580
+
4581
+ // src/agent/context-budget.ts
4582
+ function effectiveInputBudget(model, maxCompletionTokens) {
4583
+ const contextWindow = getModelOrInfer(model).contextWindow;
4584
+ const completion = maxCompletionTokens ?? DEFAULT_MAX_COMPLETION_TOKENS;
4585
+ return Math.max(1, contextWindow - completion - BUDGET_SAFETY_MARGIN_TOKENS);
4586
+ }
4587
+ function proactiveCompactionThreshold(model, maxCompletionTokens) {
4588
+ return Math.min(
4589
+ PROACTIVE_COMPACTION_CAP_TOKENS,
4590
+ Math.floor(effectiveInputBudget(model, maxCompletionTokens) * PROACTIVE_SHARE_OF_BUDGET)
4591
+ );
4592
+ }
4593
+ function compactToFit(messages, budgetTokens, target) {
4594
+ const tokensBefore = estimatePromptTokens(messages);
4595
+ for (const keepLastTurns of KEEP_TURNS_LADDER) {
4596
+ const scratch = new ArtifactStore();
4597
+ const attempt = compactMessagesViaArtifacts({ messages, state: target.getState(), store: scratch, keepLastTurns });
4598
+ if (attempt.metrics.rawTurnsRemoved === 0) continue;
4599
+ const isLast = keepLastTurns === KEEP_TURNS_LADDER[KEEP_TURNS_LADDER.length - 1];
4600
+ if (attempt.metrics.estimatedTokensAfter > budgetTokens && !isLast) continue;
4601
+ const store = target.getStore();
4602
+ for (const artifact of scratch.list()) store.add(artifact);
4603
+ target.setState(attempt.newState);
4604
+ target.onChange?.();
4605
+ return {
4606
+ messages: attempt.newMessages,
4607
+ tokensBefore,
4608
+ tokensAfter: attempt.metrics.estimatedTokensAfter,
4609
+ turnsRemoved: attempt.metrics.rawTurnsRemoved,
4610
+ artifactsArchived: attempt.metrics.archivedArtifacts
4611
+ };
4612
+ }
4613
+ return null;
4614
+ }
4615
+ function sessionFileCompactionTarget(file) {
4616
+ let store = null;
4617
+ return {
4618
+ getState: () => file.sessionState ?? emptySessionState(),
4619
+ setState: (state) => {
4620
+ file.sessionState = state;
4621
+ },
4622
+ getStore: () => store ??= file.artifactStore ? deserializeArtifactStore(file.artifactStore) : new ArtifactStore(),
4623
+ onChange: () => {
4624
+ if (store) file.artifactStore = serializeArtifactStore(store);
4625
+ }
4626
+ };
4627
+ }
4628
+ var DEFAULT_MAX_COMPLETION_TOKENS, BUDGET_SAFETY_MARGIN_TOKENS, PROACTIVE_COMPACTION_CAP_TOKENS, PROACTIVE_SHARE_OF_BUDGET, KEEP_TURNS_LADDER, ContextBudgetError;
4629
+ var init_context_budget = __esm({
4630
+ "src/agent/context-budget.ts"() {
4631
+ "use strict";
4632
+ init_registry();
4633
+ init_artifact_compaction();
4634
+ init_session_state();
4635
+ DEFAULT_MAX_COMPLETION_TOKENS = 16384;
4636
+ BUDGET_SAFETY_MARGIN_TOKENS = 8192;
4637
+ PROACTIVE_COMPACTION_CAP_TOKENS = 8e4;
4638
+ PROACTIVE_SHARE_OF_BUDGET = 0.75;
4639
+ KEEP_TURNS_LADDER = [4, 2, 1];
4640
+ ContextBudgetError = class extends Error {
4641
+ constructor(estimatedTokens, budgetTokens, contextWindow) {
4642
+ super(
4643
+ `kimiflare: context window exceeded (~${estimatedTokens.toLocaleString()} tokens needed, ${budgetTokens.toLocaleString()} available of ${contextWindow.toLocaleString()}). Older turns were already compacted; the current turn alone is too large. Run /clear to start fresh, pick a model with a larger context window, or reduce the size of the request (e.g. fewer or smaller attachments).`
4644
+ );
4645
+ this.estimatedTokens = estimatedTokens;
4646
+ this.budgetTokens = budgetTokens;
4647
+ this.contextWindow = contextWindow;
4648
+ this.name = "ContextBudgetError";
4649
+ }
4650
+ estimatedTokens;
4651
+ budgetTokens;
4652
+ contextWindow;
4653
+ };
4654
+ }
4655
+ });
4656
+
4657
+ // src/agent/turn-directive.ts
4658
+ function withTurnDirective(messages, directive, turnUserMessage) {
4659
+ let out = messages;
4660
+ if (messages.some(hasPersistedBlock)) {
4661
+ out = messages.map(
4662
+ (m) => hasPersistedBlock(m) ? { ...m, content: m.content.replace(PERSISTED_BLOCK, "") } : m
4663
+ );
4664
+ }
4665
+ if (!directive) return out;
4666
+ let anchor = turnUserMessage ? out.indexOf(turnUserMessage) : -1;
4667
+ if (anchor === -1) {
4668
+ for (let i = out.length - 1; i >= 0; i--) {
4669
+ if (out[i].role === "user") {
4670
+ anchor = i;
4671
+ break;
4672
+ }
4673
+ }
4674
+ }
4675
+ const directiveMessage = { role: "system", content: `${HEADING}
4676
+
4677
+ ${directive}` };
4678
+ const insertAt = anchor === -1 ? out.length : anchor + 1;
4679
+ return [...out.slice(0, insertAt), directiveMessage, ...out.slice(insertAt)];
4680
+ }
4681
+ function hasPersistedBlock(m) {
4682
+ return m.role === "system" && typeof m.content === "string" && m.content.includes(HEADING);
4683
+ }
4684
+ var HEADING, PERSISTED_BLOCK;
4685
+ var init_turn_directive = __esm({
4686
+ "src/agent/turn-directive.ts"() {
4687
+ "use strict";
4688
+ HEADING = "## Subagent policy for this turn";
4689
+ PERSISTED_BLOCK = /\n*## Subagent policy for this turn\n[\s\S]*?(?=\n## |$)/;
4690
+ }
4691
+ });
4692
+
4560
4693
  // src/agent/loop.ts
4561
4694
  function getSessionWebFetchHistory(sessionId) {
4562
4695
  const key = sessionId ?? "default";
@@ -4621,6 +4754,7 @@ async function runAgentTurn(opts2) {
4621
4754
  let memoryRecalledCount = 0;
4622
4755
  let skillResult;
4623
4756
  const lastUserPrompt = extractLastUserText(opts2.messages);
4757
+ const turnUserMessage = [...opts2.messages].reverse().find((m) => m.role === "user");
4624
4758
  const userPromptPreview = lastUserPrompt.slice(0, 200);
4625
4759
  const skipSkillRouting = opts2.intentClassification?.tier === "light" && lastUserPrompt.length < 40;
4626
4760
  const recallPromise = opts2.sessionStartRecall && opts2.memoryManager && !hasRecalledMemory(opts2.messages) ? (async () => {
@@ -4670,8 +4804,7 @@ async function runAgentTurn(opts2) {
4670
4804
  model: opts2.model,
4671
4805
  mode: opts2.mode,
4672
4806
  skillContext: skillResult.skillContext,
4673
- preferPullRequests: opts2.preferPullRequests,
4674
- delegationDirective: opts2.delegationDirective
4807
+ preferPullRequests: opts2.preferPullRequests
4675
4808
  })
4676
4809
  };
4677
4810
  } else {
@@ -4683,8 +4816,7 @@ async function runAgentTurn(opts2) {
4683
4816
  model: opts2.model,
4684
4817
  mode: opts2.mode,
4685
4818
  skillContext: skillResult.skillContext,
4686
- preferPullRequests: opts2.preferPullRequests,
4687
- delegationDirective: opts2.delegationDirective
4819
+ preferPullRequests: opts2.preferPullRequests
4688
4820
  })
4689
4821
  };
4690
4822
  }
@@ -4752,7 +4884,6 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
4752
4884
  let webFetchesThisTurn = 0;
4753
4885
  let codeModeRedirects = 0;
4754
4886
  const MAX_WEB_FETCH_PER_TURN = 5;
4755
- const WEB_FETCH_DOMAIN_THRESHOLD = 2;
4756
4887
  let cumulativePromptTokens = 0;
4757
4888
  let iter = 0;
4758
4889
  let totalIter = 0;
@@ -4770,6 +4901,11 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
4770
4901
  mutatingToolsSinceLastTasksSet = 0;
4771
4902
  originalOnTasks?.(tasks);
4772
4903
  };
4904
+ const appendPendingInput = () => {
4905
+ const pending = opts2.drainPendingInput?.() ?? [];
4906
+ for (const msg of pending) opts2.messages.push(msg);
4907
+ return pending.length;
4908
+ };
4773
4909
  while (true) {
4774
4910
  if (budgetExhausted) {
4775
4911
  opts2.messages.push({
@@ -4827,46 +4963,53 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
4827
4963
  const shadowStrip = process.env.KIMIFLARE_SHADOW_STRIP === "1";
4828
4964
  const keepLastRaw = process.env.KIMIFLARE_REASONING_KEEP_LAST;
4829
4965
  const keepLast = keepLastRaw ? parseInt(keepLastRaw, 10) : 1;
4830
- let apiMessages = opts2.messages;
4831
4966
  let shadowStripMetrics;
4832
- if (stripReasoning || shadowStrip) {
4833
- const stripped = stripHistoricalReasoning(opts2.messages, {
4834
- keepLast: Number.isNaN(keepLast) ? 1 : keepLast
4835
- });
4836
- if (shadowStrip) {
4837
- const originalSections = analyzePrompt(opts2.messages);
4838
- const strippedSections = analyzePrompt(stripped);
4839
- const originalApproxTokens = originalSections.reduce(
4840
- (sum, s) => sum + s.approxTokens,
4841
- 0
4842
- );
4843
- const strippedApproxTokens = strippedSections.reduce(
4844
- (sum, s) => sum + s.approxTokens,
4845
- 0
4846
- );
4847
- shadowStripMetrics = {
4848
- originalApproxTokens,
4849
- strippedApproxTokens,
4850
- savingsPct: originalApproxTokens > 0 ? Math.round(
4851
- (originalApproxTokens - strippedApproxTokens) / originalApproxTokens * 100
4852
- ) : 0
4853
- };
4967
+ const buildApiMessages = () => {
4968
+ let out = opts2.messages;
4969
+ if (stripReasoning || shadowStrip) {
4970
+ const stripped = stripHistoricalReasoning(opts2.messages, {
4971
+ keepLast: Number.isNaN(keepLast) ? 1 : keepLast
4972
+ });
4973
+ if (shadowStrip) {
4974
+ const originalApproxTokens = analyzePrompt(opts2.messages).reduce((sum, sec) => sum + sec.approxTokens, 0);
4975
+ const strippedApproxTokens = analyzePrompt(stripped).reduce((sum, sec) => sum + sec.approxTokens, 0);
4976
+ shadowStripMetrics = {
4977
+ originalApproxTokens,
4978
+ strippedApproxTokens,
4979
+ savingsPct: originalApproxTokens > 0 ? Math.round((originalApproxTokens - strippedApproxTokens) / originalApproxTokens * 100) : 0
4980
+ };
4981
+ }
4982
+ if (stripReasoning) out = stripped;
4854
4983
  }
4855
- if (stripReasoning) {
4856
- apiMessages = stripped;
4984
+ if (opts2.keepLastImageTurns !== void 0) {
4985
+ out = stripOldImages(out, opts2.keepLastImageTurns);
4986
+ }
4987
+ return withTurnDirective(out, opts2.delegationDirective, turnUserMessage);
4988
+ };
4989
+ let apiMessages = buildApiMessages();
4990
+ let promptTokens = estimatePromptTokens(apiMessages);
4991
+ const maxPromptTokens = effectiveInputBudget(opts2.model, opts2.maxCompletionTokens);
4992
+ if (promptTokens > maxPromptTokens && opts2.compaction) {
4993
+ const requestOverhead = Math.max(0, promptTokens - estimatePromptTokens(opts2.messages));
4994
+ const outcome = compactToFit(opts2.messages, maxPromptTokens - requestOverhead, opts2.compaction);
4995
+ if (outcome) {
4996
+ opts2.messages.splice(0, opts2.messages.length, ...outcome.messages);
4997
+ apiMessages = buildApiMessages();
4998
+ promptTokens = estimatePromptTokens(apiMessages);
4999
+ const info = {
5000
+ tokensBefore: outcome.tokensBefore,
5001
+ tokensAfter: outcome.tokensAfter,
5002
+ turnsRemoved: outcome.turnsRemoved,
5003
+ artifactsArchived: outcome.artifactsArchived,
5004
+ budgetTokens: maxPromptTokens
5005
+ };
5006
+ logger.info("context:compacted", { sessionId: opts2.sessionId, ...info });
5007
+ opts2.callbacks.onCompacted?.(info);
4857
5008
  }
4858
5009
  }
4859
- if (opts2.keepLastImageTurns !== void 0) {
4860
- apiMessages = stripOldImages(apiMessages, opts2.keepLastImageTurns);
4861
- }
4862
- const promptTokens = estimatePromptTokens(apiMessages);
4863
- const ctxWindow = getModelOrInfer(opts2.model).contextWindow;
4864
- const completionBudget = opts2.maxCompletionTokens ?? DEFAULT_MAX_COMPLETION_TOKENS;
4865
- const maxPromptTokens = ctxWindow - completionBudget - BUDGET_SAFETY_MARGIN_TOKENS;
4866
5010
  if (promptTokens > maxPromptTokens) {
4867
- throw new Error(
4868
- `kimiflare: context window exceeded (~${promptTokens.toLocaleString()} / ${ctxWindow.toLocaleString()} tokens). Run /compact to summarize older turns, or /clear to start fresh.`
4869
- );
5011
+ logger.warn("context:irreducible", { sessionId: opts2.sessionId, promptTokens, budgetTokens: maxPromptTokens });
5012
+ throw new ContextBudgetError(promptTokens, maxPromptTokens, getModelOrInfer(opts2.model).contextWindow);
4870
5013
  }
4871
5014
  logger.debug("turn:api_request", { sessionId: opts2.sessionId, messageCount: apiMessages.length });
4872
5015
  const events = runKimi({
@@ -4989,6 +5132,7 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
4989
5132
  if (budgetExhausted) {
4990
5133
  throw new BudgetExhaustedError();
4991
5134
  }
5135
+ if (appendPendingInput() > 0) continue;
4992
5136
  logger.info("turn:complete", { sessionId: opts2.sessionId, durationMs: Math.round(performance.now() - turnStart) });
4993
5137
  await fireStopHook();
4994
5138
  return;
@@ -4996,12 +5140,20 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
4996
5140
  let blockedCount = 0;
4997
5141
  const availableToolNames = new Set(opts2.tools.map((tool) => tool.name));
4998
5142
  if (codeMode) availableToolNames.add("execute_code");
4999
- const allReadOnly = toolCalls.length > 1 && toolCalls.every((tc) => {
5000
- const tool = opts2.executor.list().find((t) => t.name === tc.function.name);
5001
- return availableToolNames.has(tc.function.name) && tool?.isReadOnly === true;
5143
+ const executorTools = opts2.executor.list();
5144
+ const allConcurrent = toolCalls.length > 1 && toolCalls.every((tc) => {
5145
+ const tool = executorTools.find((t) => t.name === tc.function.name);
5146
+ return availableToolNames.has(tc.function.name) && (tool?.isReadOnly === true || tool?.concurrent === true);
5002
5147
  });
5003
- if (allReadOnly) {
5004
- opts2.callbacks.onInfo?.(`${toolCalls.length} read-only tools running in parallel`);
5148
+ if (allConcurrent) {
5149
+ const workerCount = toolCalls.filter((tc) => executorTools.find((t) => t.name === tc.function.name)?.concurrent === true).length;
5150
+ opts2.callbacks.onInfo?.(
5151
+ workerCount > 0 ? `${toolCalls.length} tool calls running in parallel (${workerCount} isolated worker${workerCount === 1 ? "" : "s"})` : `${toolCalls.length} read-only tools running in parallel`
5152
+ );
5153
+ const batchAskPermission = createPermissionGate(
5154
+ opts2.callbacks.askPermission,
5155
+ toolCalls.map((tc) => ({ name: tc.function.name, args: parseArgsObject(tc.function.arguments) }))
5156
+ );
5005
5157
  const items = [];
5006
5158
  for (const tc of toolCalls) {
5007
5159
  if (opts2.signal.aborted) throw new DOMException("aborted", "AbortError");
@@ -5016,7 +5168,8 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
5016
5168
  tool_call_id: tc.id,
5017
5169
  name: tc.function.name,
5018
5170
  content: `Loop detected: you have called ${tc.function.name} with the same arguments multiple times in a row. Consider a different approach.`,
5019
- ok: false
5171
+ ok: false,
5172
+ guardrail: true
5020
5173
  }
5021
5174
  });
5022
5175
  continue;
@@ -5035,27 +5188,19 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
5035
5188
  const args = JSON.parse(tc.function.arguments || "{}");
5036
5189
  const url = args.url || "";
5037
5190
  try {
5038
- const domain = new URL(url).hostname;
5039
- const domainCount = webFetchHistory.filter((h) => h.domain === domain).length;
5191
+ new URL(url);
5040
5192
  const totalSessionFetches = webFetchHistory.length;
5041
- if (webFetchesThisTurn >= MAX_WEB_FETCH_PER_TURN || totalSessionFetches >= SESSION_WEB_FETCH_CAP || domainCount >= WEB_FETCH_DOMAIN_THRESHOLD) {
5042
- let warning;
5043
- if (webFetchesThisTurn >= MAX_WEB_FETCH_PER_TURN) {
5044
- warning = `Research budget exceeded: you have already made ${MAX_WEB_FETCH_PER_TURN} web requests this turn. Synthesize what you have learned instead of fetching more pages.`;
5045
- } else if (totalSessionFetches >= SESSION_WEB_FETCH_CAP) {
5046
- warning = `Session research budget exceeded: ${totalSessionFetches} web fetches across this session. Synthesize what you have learned from prior fetches instead of starting another page.`;
5047
- } else {
5048
- warning = `Loop detected: you have fetched from ${domain} multiple times. Consider a different approach or synthesize existing findings.`;
5049
- }
5193
+ if (webFetchesThisTurn >= MAX_WEB_FETCH_PER_TURN || totalSessionFetches >= SESSION_WEB_FETCH_CAP) {
5194
+ const warning = webFetchesThisTurn >= MAX_WEB_FETCH_PER_TURN ? `Research budget exceeded: you have already made ${MAX_WEB_FETCH_PER_TURN} web requests this turn. Synthesize what you have learned instead of fetching more pages.` : `Session research budget exceeded: ${totalSessionFetches} web fetches across this session. Synthesize what you have learned from prior fetches instead of starting another page.`;
5050
5195
  items.push({
5051
5196
  kind: "blocked",
5052
5197
  tc,
5053
5198
  loopSignature,
5054
- result: { tool_call_id: tc.id, name: "web_fetch", content: warning, ok: false }
5199
+ result: { tool_call_id: tc.id, name: "web_fetch", content: warning, ok: false, guardrail: true }
5055
5200
  });
5056
5201
  continue;
5057
5202
  }
5058
- webFetchHistory.push({ url, domain });
5203
+ webFetchHistory.push(url);
5059
5204
  webFetchesThisTurn++;
5060
5205
  } catch {
5061
5206
  }
@@ -5069,7 +5214,7 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
5069
5214
  logger.debug("turn:tool_start", { sessionId: opts2.sessionId, tool: it.tc.function.name, toolCallId: it.tc.id });
5070
5215
  const result2 = await opts2.executor.run(
5071
5216
  { id: it.tc.id, name: it.tc.function.name, arguments: it.tc.function.arguments },
5072
- opts2.callbacks.askPermission,
5217
+ batchAskPermission,
5073
5218
  {
5074
5219
  cwd: opts2.cwd,
5075
5220
  signal: opts2.signal,
@@ -5085,6 +5230,7 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
5085
5230
  model: opts2.model,
5086
5231
  allowDirectPush: opts2.allowDirectPush,
5087
5232
  runId: opts2.runId,
5233
+ onRunBudgetExceeded: opts2.callbacks.onRunBudgetExceeded,
5088
5234
  runsDbPath: opts2.runsDbPath
5089
5235
  },
5090
5236
  opts2.onFileChange
@@ -5267,7 +5413,8 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
5267
5413
  tool_call_id: tc.id,
5268
5414
  name: tc.function.name,
5269
5415
  content: warning,
5270
- ok: false
5416
+ ok: false,
5417
+ guardrail: true
5271
5418
  };
5272
5419
  toolResults.push(loopResult);
5273
5420
  opts2.messages.push({
@@ -5286,8 +5433,7 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
5286
5433
  const args = JSON.parse(tc.function.arguments || "{}");
5287
5434
  const url = args.url || "";
5288
5435
  try {
5289
- const domain = new URL(url).hostname;
5290
- const domainCount = webFetchHistory.filter((h) => h.domain === domain).length;
5436
+ new URL(url);
5291
5437
  const totalSessionFetches = webFetchHistory.length;
5292
5438
  if (webFetchesThisTurn >= MAX_WEB_FETCH_PER_TURN) {
5293
5439
  const warning = `Research budget exceeded: you have already made ${MAX_WEB_FETCH_PER_TURN} web requests this turn. Synthesize what you have learned instead of fetching more pages.`;
@@ -5295,7 +5441,8 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
5295
5441
  tool_call_id: tc.id,
5296
5442
  name: "web_fetch",
5297
5443
  content: warning,
5298
- ok: false
5444
+ ok: false,
5445
+ guardrail: true
5299
5446
  };
5300
5447
  toolResults.push(budgetResult);
5301
5448
  opts2.messages.push({
@@ -5316,7 +5463,8 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
5316
5463
  tool_call_id: tc.id,
5317
5464
  name: "web_fetch",
5318
5465
  content: warning,
5319
- ok: false
5466
+ ok: false,
5467
+ guardrail: true
5320
5468
  };
5321
5469
  toolResults.push(sessionCapResult);
5322
5470
  opts2.messages.push({
@@ -5331,28 +5479,7 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
5331
5479
  blockedCount++;
5332
5480
  continue;
5333
5481
  }
5334
- if (domainCount >= WEB_FETCH_DOMAIN_THRESHOLD) {
5335
- const warning = `Loop detected: you have fetched from ${domain} multiple times. Consider a different approach or synthesize existing findings.`;
5336
- const loopResult = {
5337
- tool_call_id: tc.id,
5338
- name: "web_fetch",
5339
- content: warning,
5340
- ok: false
5341
- };
5342
- toolResults.push(loopResult);
5343
- opts2.messages.push({
5344
- role: "tool",
5345
- tool_call_id: tc.id,
5346
- content: sanitizeString(warning),
5347
- name: "web_fetch"
5348
- });
5349
- opts2.callbacks.onToolResult?.(loopResult);
5350
- recentToolCalls.push(loopSignature);
5351
- if (recentToolCalls.length > LOOP_WINDOW) recentToolCalls.shift();
5352
- blockedCount++;
5353
- continue;
5354
- }
5355
- webFetchHistory.push({ url, domain });
5482
+ webFetchHistory.push(url);
5356
5483
  webFetchesThisTurn++;
5357
5484
  } catch {
5358
5485
  }
@@ -5490,7 +5617,7 @@ ${sandboxResult.output}` : sandboxResult.output;
5490
5617
  opts2.callbacks.onToolResult?.(result2);
5491
5618
  if (result2.waitRequest) {
5492
5619
  if (opts2.onIterationEnd) {
5493
- opts2.messages = await opts2.onIterationEnd(opts2.messages, opts2.signal);
5620
+ replaceMessagesInPlace(opts2.messages, await opts2.onIterationEnd(opts2.messages, opts2.signal));
5494
5621
  }
5495
5622
  logger.info("run:yielded", {
5496
5623
  runId: result2.waitRequest.runId,
@@ -5621,8 +5748,9 @@ ${sandboxResult.output}` : sandboxResult.output;
5621
5748
  if (blockedCount === toolCalls.length && toolCalls.length > 0) {
5622
5749
  loopExhausted = true;
5623
5750
  }
5751
+ appendPendingInput();
5624
5752
  if (opts2.onIterationEnd) {
5625
- opts2.messages = await opts2.onIterationEnd(opts2.messages, opts2.signal);
5753
+ replaceMessagesInPlace(opts2.messages, await opts2.onIterationEnd(opts2.messages, opts2.signal));
5626
5754
  if (opts2.signal.aborted) throw new DOMException("aborted", "AbortError");
5627
5755
  }
5628
5756
  if (opts2.sessionId && lastUsage) {
@@ -5674,6 +5802,18 @@ ${sandboxResult.output}` : sandboxResult.output;
5674
5802
  }
5675
5803
  }
5676
5804
  }
5805
+ function parseArgsObject(raw) {
5806
+ try {
5807
+ const parsed = JSON.parse(raw || "{}");
5808
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
5809
+ } catch {
5810
+ return {};
5811
+ }
5812
+ }
5813
+ function replaceMessagesInPlace(target, next) {
5814
+ if (next === target) return;
5815
+ target.splice(0, target.length, ...next);
5816
+ }
5677
5817
  function validateToolArguments(raw) {
5678
5818
  if (!raw || !raw.trim()) return "{}";
5679
5819
  try {
@@ -5691,7 +5831,7 @@ function llmAuthOf(opts2) {
5691
5831
  ...opts2.provider ? { provider: opts2.provider } : {}
5692
5832
  };
5693
5833
  }
5694
- var MAX_LOOP_RECOVERIES, BudgetExhaustedError, AgentLoopError, codeModeApiCache, driftEvents, DRIFT_WINDOW, DRIFT_THRESHOLD, memoryExtractionErrorCounts, sessionWebFetchHistory, SESSION_WEB_FETCH_CAP, DEFAULT_MAX_COMPLETION_TOKENS, BUDGET_SAFETY_MARGIN_TOKENS, MAX_TOOL_CONTENT_CHARS, CODE_MODE_REDIRECT_TOOLS, MAX_CODE_MODE_REDIRECTS;
5834
+ var MAX_LOOP_RECOVERIES, BudgetExhaustedError, AgentLoopError, codeModeApiCache, driftEvents, DRIFT_WINDOW, DRIFT_THRESHOLD, memoryExtractionErrorCounts, sessionWebFetchHistory, SESSION_WEB_FETCH_CAP, MAX_TOOL_CONTENT_CHARS, CODE_MODE_REDIRECT_TOOLS, MAX_CODE_MODE_REDIRECTS;
5695
5835
  var init_loop = __esm({
5696
5836
  "src/agent/loop.ts"() {
5697
5837
  "use strict";
@@ -5708,6 +5848,9 @@ var init_loop = __esm({
5708
5848
  init_router();
5709
5849
  init_system_prompt();
5710
5850
  init_registry();
5851
+ init_permission_gate();
5852
+ init_context_budget();
5853
+ init_turn_directive();
5711
5854
  MAX_LOOP_RECOVERIES = 1;
5712
5855
  BudgetExhaustedError = class extends Error {
5713
5856
  constructor(message2 = "Cumulative input token budget exhausted") {
@@ -5728,8 +5871,6 @@ var init_loop = __esm({
5728
5871
  memoryExtractionErrorCounts = /* @__PURE__ */ new Map();
5729
5872
  sessionWebFetchHistory = /* @__PURE__ */ new Map();
5730
5873
  SESSION_WEB_FETCH_CAP = 25;
5731
- DEFAULT_MAX_COMPLETION_TOKENS = 16384;
5732
- BUDGET_SAFETY_MARGIN_TOKENS = 8192;
5733
5874
  MAX_TOOL_CONTENT_CHARS = 1e4;
5734
5875
  CODE_MODE_REDIRECT_TOOLS = /* @__PURE__ */ new Set(["read", "bash", "grep", "glob"]);
5735
5876
  MAX_CODE_MODE_REDIRECTS = 4;
@@ -6664,47 +6805,64 @@ function parsePushTarget(command) {
6664
6805
  }
6665
6806
  return { kind: "ref", ref: last.replace(/^refs\/heads\//, "") };
6666
6807
  }
6808
+ function buildCoauthorTrailer(coauthor) {
6809
+ const unsafe = /[\u0000-\u001f\u007f\u2028\u2029]/;
6810
+ if (unsafe.test(coauthor.name) || unsafe.test(coauthor.email)) return null;
6811
+ const name = coauthor.name.trim();
6812
+ const email = coauthor.email.trim();
6813
+ if (!name || !email || /[<>]/.test(email)) return null;
6814
+ return `Co-authored-by: ${name} <${email}>`;
6815
+ }
6667
6816
  function injectCoauthor(command, coauthor) {
6668
- if (!coauthor) return command;
6669
- const trailer = `Co-authored-by: ${coauthor.name} <${coauthor.email}>`;
6817
+ const unchanged = { command, env: {} };
6818
+ if (!coauthor) return unchanged;
6819
+ const trailer = buildCoauthorTrailer(coauthor);
6820
+ if (!trailer) {
6821
+ logger.warn("bash:coauthor_rejected", { reason: "empty value, control character, or line break in co-author name/email" });
6822
+ return unchanged;
6823
+ }
6670
6824
  const trimmed = command.trim();
6671
- if (command.includes(trailer)) return command;
6825
+ if (command.includes(trailer)) return unchanged;
6672
6826
  const createsCommit = /\bgit\s+(commit|merge|revert|cherry-pick)\b/.test(trimmed);
6673
6827
  const isRebaseContinue = /\bgit\s+rebase\b/.test(trimmed) && !/\b--abort\b|\b--skip\b/.test(trimmed);
6674
6828
  const movesHeadOnly = /\bgit\s+(reset|checkout|switch)\b/.test(trimmed);
6675
6829
  const mentionsGit = /\bgit\b/.test(trimmed);
6676
- if (!createsCommit && !isRebaseContinue && !mentionsGit) return command;
6677
- if (movesHeadOnly) return command;
6678
- const tmpFile = join14(tmpdir(), `kf-coauthor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
6679
- const amendBlock = `
6680
- if ! git log -1 --pretty=%B 2>/dev/null | grep -qF "${trailer}"; then
6681
- git log -1 --pretty=%B | git interpret-trailers --trailer "${trailer}" > "${tmpFile}" && git commit --amend -F "${tmpFile}" --no-edit && rm -f "${tmpFile}"
6682
- fi
6683
- `.trim();
6830
+ if (!createsCommit && !isRebaseContinue && !mentionsGit) return unchanged;
6831
+ if (movesHeadOnly) return unchanged;
6832
+ const env2 = {
6833
+ [COAUTHOR_TRAILER_ENV]: trailer,
6834
+ [COAUTHOR_MSG_ENV]: join14(tmpdir(), `kf-coauthor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`)
6835
+ };
6684
6836
  if (createsCommit || isRebaseContinue) {
6685
- return `(${command}) && { ${amendBlock}; }`;
6837
+ return { command: `(${command}) && { ${AMEND_BLOCK}
6838
+ }`, env: env2 };
6686
6839
  }
6687
6840
  const beforeHead = `git rev-parse HEAD 2>/dev/null || echo "NO_HEAD"`;
6688
- const afterCheck = `
6689
- _KF_AFTER_HEAD=$(git rev-parse HEAD 2>/dev/null || echo "NO_HEAD")
6690
- if [ "$_KF_BEFORE_HEAD" != "$_KF_AFTER_HEAD" ] && [ "$_KF_AFTER_HEAD" != "NO_HEAD" ] && git merge-base --is-ancestor "$_KF_BEFORE_HEAD" "$_KF_AFTER_HEAD" 2>/dev/null; then
6691
- ${amendBlock}
6692
- fi
6693
- `.trim();
6694
- return `_KF_BEFORE_HEAD=$(${beforeHead}); (${command}); _KF_EXIT=$?; [ $_KF_EXIT -eq 0 ] && { ${afterCheck}; }; exit $_KF_EXIT`;
6841
+ const afterCheck = [
6842
+ `_KF_AFTER_HEAD=$(git rev-parse HEAD 2>/dev/null || echo "NO_HEAD")`,
6843
+ `if [ "$_KF_BEFORE_HEAD" != "$_KF_AFTER_HEAD" ] && [ "$_KF_AFTER_HEAD" != "NO_HEAD" ] && git merge-base --is-ancestor "$_KF_BEFORE_HEAD" "$_KF_AFTER_HEAD" 2>/dev/null; then`,
6844
+ AMEND_BLOCK,
6845
+ `fi`
6846
+ ].join("\n");
6847
+ return {
6848
+ command: `_KF_BEFORE_HEAD=$(${beforeHead}); (${command}); _KF_EXIT=$?; [ $_KF_EXIT -eq 0 ] && { ${afterCheck}
6849
+ }; exit $_KF_EXIT`,
6850
+ env: env2
6851
+ };
6695
6852
  }
6696
6853
  async function runBash(args, ctx) {
6697
6854
  const timeout = Math.min(Math.max(1e3, args.timeout_ms ?? DEFAULT_TIMEOUT), MAX_TIMEOUT);
6698
6855
  const pushGuard = await guardGitPush(args.command, ctx);
6699
6856
  if (pushGuard) return pushGuard;
6700
6857
  const { shell, args: shellArgs, isPosix } = getShellCommand(ctx.shell);
6701
- const command = isPosix ? injectCoauthor(args.command, ctx.coauthor) : args.command;
6858
+ const { command, env: coauthorEnv } = isPosix ? injectCoauthor(args.command, ctx.coauthor) : { command: args.command, env: {} };
6702
6859
  return new Promise((resolve15, reject) => {
6703
6860
  logger.debug("bash:spawn", { command: args.command.slice(0, 200), cwd: ctx.cwd, shell });
6704
6861
  const child = spawn(shell, [...shellArgs, command], {
6705
6862
  cwd: ctx.cwd,
6706
6863
  env: {
6707
6864
  ...process.env,
6865
+ ...coauthorEnv,
6708
6866
  GIT_EDITOR: "true"
6709
6867
  }
6710
6868
  });
@@ -6771,7 +6929,7 @@ ${stderr}`
6771
6929
  });
6772
6930
  });
6773
6931
  }
6774
- var DEFAULT_TIMEOUT, MAX_TIMEOUT, bashTool;
6932
+ var DEFAULT_TIMEOUT, MAX_TIMEOUT, bashTool, COAUTHOR_TRAILER_ENV, COAUTHOR_MSG_ENV, AMEND_BLOCK;
6775
6933
  var init_bash = __esm({
6776
6934
  "src/tools/bash.ts"() {
6777
6935
  "use strict";
@@ -6801,6 +6959,14 @@ var init_bash = __esm({
6801
6959
  render: (args) => ({ title: formatBashTitle(String(args.command ?? "")) }),
6802
6960
  run: (args, ctx) => runBash(args, ctx)
6803
6961
  };
6962
+ COAUTHOR_TRAILER_ENV = "KF_COAUTHOR_TRAILER";
6963
+ COAUTHOR_MSG_ENV = "KF_COAUTHOR_MSG";
6964
+ AMEND_BLOCK = [
6965
+ `if ! git log -1 --pretty=%B 2>/dev/null | grep -qF -- "$${COAUTHOR_TRAILER_ENV}"; then`,
6966
+ ` git log -1 --pretty=%B | git interpret-trailers --trailer "$${COAUTHOR_TRAILER_ENV}" > "$${COAUTHOR_MSG_ENV}" && git commit --amend -F "$${COAUTHOR_MSG_ENV}" --no-edit; _KF_RC=$?;`,
6967
+ ` rm -f "$${COAUTHOR_MSG_ENV}"; [ $_KF_RC -eq 0 ];`,
6968
+ `fi`
6969
+ ].join("\n");
6804
6970
  }
6805
6971
  });
6806
6972
 
@@ -8436,6 +8602,8 @@ async function runHotcellWorker(options) {
8436
8602
  throw new Error("Hotcell worker spend cap must be a positive number.");
8437
8603
  }
8438
8604
  if (options.signal?.aborted) return terminalResult(workerId, options.task, "cancelled", "Cancelled before worker setup.");
8605
+ const workerPackage = resolveWorkerPackage(options.workerPackage);
8606
+ const maxInputTokens = resolveWorkerInputBudget(model, options.budgetUsd, options.maxInputTokens);
8439
8607
  await acquireSlot(options.maxParallel ?? MAX_PARALLEL_WORKERS, options.signal);
8440
8608
  let cellId;
8441
8609
  let result2;
@@ -8487,9 +8655,43 @@ async function runHotcellWorker(options) {
8487
8655
  result2 = terminalResult(workerId, options.task, "failed", "Hotcell has no OpenRouter gateway route. Run `hotcell keys add openrouter` on the daemon host, then retry.", model);
8488
8656
  return result2;
8489
8657
  }
8658
+ const setupCommand = [
8659
+ ...FIND_REPO_ROOT,
8660
+ `git -C "$REPO_ROOT" fetch --quiet origin ${shellQuote(repo.commit)}`,
8661
+ `git -C "$REPO_ROOT" checkout --quiet --detach ${shellQuote(repo.commit)}`,
8662
+ `npm install --prefix ${WORKER_PREFIX} --no-audit --no-fund --omit=optional --loglevel=error ${shellQuote(workerPackage)} 1>&2`
8663
+ ].join(" && ");
8664
+ const setup = await execute(command, ["exec", cellId, setupCommand, "--cwd", "/workspace"], {
8665
+ cwd,
8666
+ signal: options.signal,
8667
+ timeoutMs: options.setupTimeoutMs ?? DEFAULT_SETUP_TIMEOUT_MS
8668
+ });
8669
+ if (setup.timedOut || setup.aborted) {
8670
+ result2 = terminalResult(
8671
+ workerId,
8672
+ options.task,
8673
+ options.signal?.aborted ? "cancelled" : "timed_out",
8674
+ options.signal?.aborted ? "Cancelled during worker setup." : `Worker setup (checkout and ${workerPackage} install) timed out.`,
8675
+ model
8676
+ );
8677
+ return result2;
8678
+ }
8679
+ if (setup.code !== 0) {
8680
+ const detail = sanitizeHotcellDiagnostic(`${setup.stderr}
8681
+ ${setup.stdout}`);
8682
+ result2 = terminalResult(
8683
+ workerId,
8684
+ options.task,
8685
+ "failed",
8686
+ `Worker setup failed (exit ${setup.code}) while checking out the commit or installing ${workerPackage}${detail ? `: ${detail}` : "."}`,
8687
+ model
8688
+ );
8689
+ return result2;
8690
+ }
8490
8691
  const prompt = [
8491
8692
  "You are an isolated, read-only research worker. You cannot edit files, run shell commands, push, publish, or call write-capable integrations.",
8492
8693
  "Use the exact requested model and return concise findings, recommendations, and relevant file paths. Do not claim to have changed files.",
8694
+ WORKER_REPORT_INSTRUCTIONS,
8493
8695
  `Mission:
8494
8696
  ${options.task}`,
8495
8697
  options.context ? `Coordinator context:
@@ -8497,16 +8699,11 @@ ${options.context}` : ""
8497
8699
  ].filter(Boolean).join("\n\n");
8498
8700
  const encodedPrompt = Buffer.from(prompt, "utf8").toString("base64");
8499
8701
  const shellCommand = [
8500
- 'GIT_DIR="$(find /workspace -mindepth 2 -maxdepth 5 -name .git -print -quit)"',
8501
- 'test -n "$GIT_DIR"',
8502
- 'REPO_ROOT="${GIT_DIR%/.git}"',
8503
- `git -C "$REPO_ROOT" fetch --quiet origin ${shellQuote(repo.commit)}`,
8504
- `git -C "$REPO_ROOT" checkout --quiet --detach ${shellQuote(repo.commit)}`,
8702
+ ...FIND_REPO_ROOT,
8505
8703
  'cd "$REPO_ROOT"',
8506
- "npm ci --no-audit --no-fund --loglevel=error 1>&2",
8507
8704
  'export OPENROUTER_BASE_URL="${OPENROUTER_BASE_URL%/}/v1"',
8508
8705
  `PROMPT="$(printf '%s' '${encodedPrompt}' | base64 -d)"`,
8509
- `node --import tsx "$REPO_ROOT/src/index.tsx" --format json --max-input-tokens 14000 --model ${shellQuote(model)} --worker-profile research -p "$PROMPT"`
8706
+ `${WORKER_BIN} --format json --max-input-tokens ${maxInputTokens} --model ${shellQuote(model)} --worker-profile research -p "$PROMPT"`
8510
8707
  ].join(" && ");
8511
8708
  const execution = await execute(command, ["exec", cellId, shellCommand, "--cwd", "/workspace"], {
8512
8709
  cwd,
@@ -8524,14 +8721,16 @@ ${execution.stderr}`.toLowerCase();
8524
8721
  const executionDiagnostic = sanitizeHotcellDiagnostic(execution.stderr);
8525
8722
  const spendExhausted = /(?:http\s*)?402|spend cap|spending limit/.test(diagnostic);
8526
8723
  const status = execution.timedOut ? "timed_out" : execution.aborted ? options.signal?.aborted ? "cancelled" : "failed" : spendExhausted ? "spend_exhausted" : budgetExhausted ? "budget_exhausted" : execution.code === 0 ? "completed" : "failed";
8527
- const findings = summary ? [{ topic: "Research findings", summary: summary.slice(0, 4e4), confidence: "medium", sources: [], relevance: "high" }] : [];
8724
+ const report = parseWorkerReport(summary);
8725
+ const findings = report ? report.findings : summary ? [{ topic: "Research findings", summary: summary.slice(0, 4e4), confidence: "medium", sources: [], relevance: "high" }] : [];
8528
8726
  result2 = {
8529
8727
  workerId,
8530
8728
  status,
8531
8729
  task: options.task,
8532
8730
  findings,
8533
8731
  recommendations: [],
8534
- filesRead: [],
8732
+ filesRead: report?.filesRead ?? [],
8733
+ ...report ? { structured: true, openQuestions: report.openQuestions } : {},
8535
8734
  webSources: [],
8536
8735
  costUsd: metrics.costUsd ?? 0,
8537
8736
  tokensUsed: metrics.tokensUsed ?? tokensUsed,
@@ -8582,8 +8781,60 @@ async function getCleanRepository(cwd) {
8582
8781
  if (!/^(https:\/\/|git@|ssh:\/\/)/i.test(url) || /:\/\/[^/]*@/.test(url)) {
8583
8782
  throw new Error("Hotcell workers require a credential-free HTTPS or SSH origin URL that the Hotcell daemon can clone.");
8584
8783
  }
8784
+ const pushed = (await execFileAsync("git", ["branch", "-r", "--contains", commit], { cwd: root })).stdout.trim();
8785
+ if (!pushed) {
8786
+ throw new Error(
8787
+ `Hotcell workers clone from origin, but HEAD (${commit.slice(0, 8)}) is not on any remote-tracking branch. Push it first (git push -u origin ${ref}), or run git fetch if it was pushed elsewhere, then retry.`
8788
+ );
8789
+ }
8585
8790
  return { url, commit, ref };
8586
8791
  }
8792
+ function resolveWorkerInputBudget(model, budgetUsd, override) {
8793
+ const clamp = (n) => Math.min(MAX_WORKER_INPUT_TOKENS, Math.max(MIN_WORKER_INPUT_TOKENS, Math.floor(n)));
8794
+ const envOverride = Number(process.env.KIMIFLARE_WORKER_MAX_INPUT_TOKENS);
8795
+ const explicit = override ?? (Number.isFinite(envOverride) && envOverride > 0 ? envOverride : void 0);
8796
+ if (explicit !== void 0 && Number.isFinite(explicit) && explicit > 0) return clamp(explicit);
8797
+ const perMillion = getModelOrInfer(model).pricing.inputPerMtok;
8798
+ if (!(perMillion > 0) || !(budgetUsd > 0)) return DEFAULT_WORKER_INPUT_TOKENS;
8799
+ return clamp(budgetUsd * INPUT_SHARE_OF_BUDGET * 1e6 / perMillion);
8800
+ }
8801
+ function parseWorkerReport(text) {
8802
+ const blocks = [...text.matchAll(/```json\s*\n([\s\S]*?)```/g)];
8803
+ const raw = blocks.at(-1)?.[1];
8804
+ if (!raw) return null;
8805
+ let parsed;
8806
+ try {
8807
+ parsed = JSON.parse(raw);
8808
+ } catch {
8809
+ return null;
8810
+ }
8811
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.findings)) return null;
8812
+ const obj = parsed;
8813
+ const strings = (value, maxChars = 300) => Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim().length > 0).slice(0, MAX_REPORT_LIST_ITEMS).map((v) => v.trim().slice(0, maxChars)) : [];
8814
+ const findings = [];
8815
+ for (const item of obj.findings.slice(0, MAX_REPORT_FINDINGS)) {
8816
+ if (!item || typeof item !== "object") continue;
8817
+ const f = item;
8818
+ if (typeof f.summary !== "string" || !f.summary.trim()) continue;
8819
+ const confidence = f.confidence === "high" || f.confidence === "low" ? f.confidence : "medium";
8820
+ findings.push({
8821
+ topic: typeof f.topic === "string" && f.topic.trim() ? f.topic.trim().slice(0, 120) : "Finding",
8822
+ summary: f.summary.trim().slice(0, MAX_REPORT_SUMMARY_CHARS),
8823
+ confidence,
8824
+ sources: strings(f.files),
8825
+ relevance: "high"
8826
+ });
8827
+ }
8828
+ if (findings.length === 0) return null;
8829
+ return { findings, openQuestions: strings(obj.openQuestions, 500), filesRead: strings(obj.filesRead) };
8830
+ }
8831
+ function resolveWorkerPackage(override) {
8832
+ const spec = (override ?? process.env.KIMIFLARE_WORKER_PACKAGE ?? `${PACKAGE_NAME}@${getAppVersion()}`).trim();
8833
+ if (!spec || !PACKAGE_SPEC.test(spec)) {
8834
+ throw new Error(`Invalid Hotcell worker package spec: ${JSON.stringify(spec)}.`);
8835
+ }
8836
+ return spec;
8837
+ }
8587
8838
  function parseCellId(output) {
8588
8839
  const uuid = output.match(/\b[a-f0-9]{8}(?:-[a-f0-9]{4,}){0,4}\b/gi)?.at(-1);
8589
8840
  if (uuid) return uuid;
@@ -8734,22 +8985,65 @@ function releaseSlot() {
8734
8985
  runningWorkers = Math.max(0, runningWorkers - 1);
8735
8986
  for (const wake of workerQueue.slice()) wake();
8736
8987
  }
8737
- var execFileAsync, MAX_OUTPUT_BYTES, MAX_PROMPT_CHARS, DEFAULT_CELL_TIMEOUT_MS, MAX_PARALLEL_WORKERS, runningWorkers, workerQueue;
8988
+ var execFileAsync, MAX_OUTPUT_BYTES, MAX_PROMPT_CHARS, DEFAULT_CELL_TIMEOUT_MS, DEFAULT_SETUP_TIMEOUT_MS, WORKER_PREFIX, WORKER_BIN, PACKAGE_SPEC, FIND_REPO_ROOT, MAX_PARALLEL_WORKERS, MIN_WORKER_INPUT_TOKENS, MAX_WORKER_INPUT_TOKENS, DEFAULT_WORKER_INPUT_TOKENS, INPUT_SHARE_OF_BUDGET, MAX_REPORT_FINDINGS, MAX_REPORT_SUMMARY_CHARS, MAX_REPORT_LIST_ITEMS, runningWorkers, workerQueue, WORKER_REPORT_INSTRUCTIONS;
8738
8989
  var init_hotcell_worker = __esm({
8739
8990
  "src/tools/hotcell-worker.ts"() {
8740
8991
  "use strict";
8741
8992
  init_client();
8993
+ init_version();
8994
+ init_registry();
8742
8995
  execFileAsync = promisify2(execFile2);
8743
8996
  MAX_OUTPUT_BYTES = 1e6;
8744
8997
  MAX_PROMPT_CHARS = 4e4;
8745
8998
  DEFAULT_CELL_TIMEOUT_MS = 3e5;
8999
+ DEFAULT_SETUP_TIMEOUT_MS = 3e5;
9000
+ WORKER_PREFIX = "/tmp/autopilot-worker";
9001
+ WORKER_BIN = `${WORKER_PREFIX}/node_modules/.bin/${CLI_NAME}`;
9002
+ PACKAGE_SPEC = /^[\w@./:+~^=<>#-]+$/;
9003
+ FIND_REPO_ROOT = [
9004
+ 'GIT_DIR="$(find /workspace -mindepth 2 -maxdepth 5 -name .git -print -quit)"',
9005
+ 'test -n "$GIT_DIR"',
9006
+ 'REPO_ROOT="${GIT_DIR%/.git}"'
9007
+ ];
8746
9008
  MAX_PARALLEL_WORKERS = 3;
9009
+ MIN_WORKER_INPUT_TOKENS = 6e4;
9010
+ MAX_WORKER_INPUT_TOKENS = 3e6;
9011
+ DEFAULT_WORKER_INPUT_TOKENS = 4e5;
9012
+ INPUT_SHARE_OF_BUDGET = 0.8;
9013
+ MAX_REPORT_FINDINGS = 12;
9014
+ MAX_REPORT_SUMMARY_CHARS = 2e3;
9015
+ MAX_REPORT_LIST_ITEMS = 25;
8747
9016
  runningWorkers = 0;
8748
9017
  workerQueue = [];
9018
+ WORKER_REPORT_INSTRUCTIONS = [
9019
+ "Finish your answer with one fenced ```json block, and nothing after it, of exactly this shape:",
9020
+ '{"findings":[{"topic":"short title","summary":"what you found and why it matters","files":["path/to/file.ts:123"],"confidence":"high|medium|low"}],"openQuestions":["what you could not determine"],"filesRead":["path/to/file.ts"]}',
9021
+ "Cite repository-relative file paths (with line numbers where useful) for every finding. Use low confidence for anything you did not verify in the code."
9022
+ ].join("\n");
8749
9023
  }
8750
9024
  });
8751
9025
 
8752
9026
  // src/tools/spawn-worker.ts
9027
+ function formatWorkerResult(result2, model) {
9028
+ const lines = [
9029
+ "Local Hotcell worker " + result2.status + (result2.status === "budget_exhausted" ? " (partial result)" : "") + ".",
9030
+ "Model: " + (result2.model ?? model) + " \xB7 Tokens: " + result2.tokensUsed.toLocaleString() + " \xB7 Cost: " + (result2.costUsd > 0 ? result2.costUsd.toFixed(4) + " total Hotcell cost" : "unavailable from Hotcell stats") + ".",
9031
+ ...result2.findings.map((finding) => "\n## " + finding.topic + (result2.structured ? " (" + finding.confidence + " confidence)" : "") + "\n" + finding.summary + (finding.sources.length ? "\nFiles: " + finding.sources.join(", ") : ""))
9032
+ ];
9033
+ if (result2.openQuestions?.length) {
9034
+ lines.push("\n## Open questions\n" + result2.openQuestions.map((q) => "- " + q).join("\n"));
9035
+ }
9036
+ if (result2.filesRead.length) {
9037
+ lines.push("\nFiles read: " + result2.filesRead.join(", "));
9038
+ }
9039
+ if (!result2.structured && result2.findings.length) {
9040
+ lines.push("\n(Worker did not return a structured report; findings above are its raw answer and are unverified.)");
9041
+ }
9042
+ if (result2.status === "budget_exhausted") {
9043
+ lines.push("\nWorker input-token budget was exhausted; review partial findings before relying on them.");
9044
+ }
9045
+ return lines.join("\n");
9046
+ }
8753
9047
  function textOutput(content) {
8754
9048
  const bytes = Buffer.byteLength(content, "utf8");
8755
9049
  return { content, rawBytes: bytes, reducedBytes: bytes };
@@ -8771,7 +9065,8 @@ var init_spawn_worker = __esm({
8771
9065
  name: "spawn_worker",
8772
9066
  description: [
8773
9067
  "Spawn a read-only research worker in a local Hotcell sandbox. Hotcell is the only worker backend.",
8774
- "Workers inspect the committed repository snapshot and cannot edit files; implement any changes in the coordinator."
9068
+ "Workers inspect the committed repository snapshot and cannot edit files; implement any changes in the coordinator.",
9069
+ "To run several workers in parallel, emit all of their spawn_worker calls in the same response; the user approves them together."
8775
9070
  ].join(" "),
8776
9071
  parameters: {
8777
9072
  type: "object",
@@ -8805,10 +9100,22 @@ var init_spawn_worker = __esm({
8805
9100
  additionalProperties: false
8806
9101
  },
8807
9102
  needsPermission: true,
8808
- render: (args) => ({
8809
- title: "spawn_worker (Hotcell plan)",
8810
- body: args.task.slice(0, 200)
8811
- }),
9103
+ // Workers run in isolated Hotcell cells and never touch the workspace, so
9104
+ // several can run at once; the Hotcell slot semaphore bounds concurrency.
9105
+ concurrent: true,
9106
+ render: (args) => {
9107
+ const batch = args.batch;
9108
+ if (Array.isArray(batch) && batch.length > 1) {
9109
+ return {
9110
+ title: `spawn_worker \xD7 ${batch.length} (Hotcell plan, run in parallel)`,
9111
+ body: batch.map((item, i) => `${i + 1}. ${String(item.task ?? "").slice(0, 160)}`).join("\n")
9112
+ };
9113
+ }
9114
+ return {
9115
+ title: "spawn_worker (Hotcell plan)",
9116
+ body: args.task.slice(0, 200)
9117
+ };
9118
+ },
8812
9119
  async run(args, ctx) {
8813
9120
  const cfg = await loadConfig().catch(() => null);
8814
9121
  if (!cfg?.openrouterApiKey) {
@@ -8836,6 +9143,7 @@ var init_spawn_worker = __esm({
8836
9143
  model,
8837
9144
  budgetUsd,
8838
9145
  timeoutMs,
9146
+ setupTimeoutMs: readNumberEnv2("KIMIFLARE_WORKER_SETUP_TIMEOUT_MS"),
8839
9147
  maxParallel: cfg.workerMaxParallel,
8840
9148
  cwd: ctx.cwd,
8841
9149
  signal: ctx.signal
@@ -8846,15 +9154,7 @@ var init_spawn_worker = __esm({
8846
9154
  if (result2.status !== "completed" && result2.status !== "budget_exhausted") {
8847
9155
  throw new Error("Local Hotcell worker " + result2.status + ": " + (result2.error ?? "unknown error"));
8848
9156
  }
8849
- const lines = [
8850
- "Local Hotcell worker " + result2.status + (result2.status === "budget_exhausted" ? " (partial result)" : "") + ".",
8851
- "Model: " + (result2.model ?? model) + " \xB7 Tokens: " + result2.tokensUsed.toLocaleString() + " \xB7 Cost: " + (result2.costUsd > 0 ? result2.costUsd.toFixed(4) + " total Hotcell cost" : "unavailable from Hotcell stats") + ".",
8852
- ...result2.findings.map((finding) => "\n## " + finding.topic + "\n" + finding.summary)
8853
- ];
8854
- if (result2.status === "budget_exhausted") {
8855
- lines.push("\nWorker input-token budget was exhausted; review partial findings before relying on them.");
8856
- }
8857
- return textOutput(lines.join("\n"));
9157
+ return textOutput(formatWorkerResult(result2, model));
8858
9158
  }
8859
9159
  };
8860
9160
  }
@@ -10206,9 +10506,9 @@ var init_store = __esm({
10206
10506
  throw new Error(`Invalid run transition: ${current.status} -> ${status}`);
10207
10507
  }
10208
10508
  const now2 = Date.now();
10209
- const startedAt = status === "running" ? current.startedAt ?? now2 : current.startedAt;
10509
+ const startedAt2 = status === "running" ? current.startedAt ?? now2 : current.startedAt;
10210
10510
  const finishedAt = ["completed", "failed", "cancelled", "interrupted_unknown"].includes(status) ? now2 : null;
10211
- this.db.prepare(`UPDATE runs SET status = ?, stop_reason = ?, updated_at = ?, started_at = ?, finished_at = ? WHERE id = ?`).run(status, reason ?? null, now2, startedAt, finishedAt, id);
10511
+ this.db.prepare(`UPDATE runs SET status = ?, stop_reason = ?, updated_at = ?, started_at = ?, finished_at = ? WHERE id = ?`).run(status, reason ?? null, now2, startedAt2, finishedAt, id);
10212
10512
  this.appendEvent(id, "state", null, null, { status, reason: reason ?? null }, now2);
10213
10513
  return this.getRun(id);
10214
10514
  });
@@ -11410,13 +11710,13 @@ function directiveFor(kind) {
11410
11710
  return "The user explicitly asked not to delegate. Do not call spawn_worker; handle the task in the main session and respect any stated dependencies.";
11411
11711
  }
11412
11712
  if (kind === "explicit-delegate") {
11413
- return "The user explicitly requested subagents. Before acting, split the request into bounded missions that can make progress independently; use spawn_worker in plan mode for useful parallel read-only research, wait for the workers, and synthesize their findings before dependent implementation. Do not delegate tightly coupled or sequential steps. The tool's normal permission prompt and all configured safety limits still apply.";
11713
+ return "The user explicitly requested subagents. Before acting, split the request into bounded missions that can make progress independently; use spawn_worker in plan mode for useful parallel read-only research (emit all independent spawn_worker calls in the same response so they run concurrently), wait for the workers, and synthesize their findings before dependent implementation. Do not delegate tightly coupled or sequential steps. The tool's normal permission prompt and all configured safety limits still apply.";
11414
11714
  }
11415
11715
  if (kind === "suggest") {
11416
11716
  return "The harness sees a possible parallel-work opportunity, but the user did not request delegation. Use spawn_worker only if you can define bounded, independent missions that materially help. The tool's normal permission prompt is the required user confirmation; never assume approval. Keep dependent implementation in the coordinator.";
11417
11717
  }
11418
11718
  if (kind === "auto-delegate") {
11419
- return "Proactive subagent delegation is enabled for this substantial task. First assess its structure, dependencies, and coordination cost. If two or more bounded research tracks can make meaningful progress independently, delegate them with spawn_worker in plan mode in parallel, wait for their results, and synthesize the findings before doing dependent work in the coordinator. Keep trivial, tightly coupled, and sequential work local. Worker calls still require normal permission and local Hotcell with OpenRouter, a clean committed repository, spend, concurrency, timeout, and read-only restrictions; never bypass them. If a worker cannot run or fails, explain that plainly and continue locally without implying delegation succeeded.";
11719
+ return "Proactive subagent delegation is enabled for this substantial task. First assess its structure, dependencies, and coordination cost. If two or more bounded research tracks can make meaningful progress independently, delegate them with spawn_worker in plan mode by emitting all of those calls in the same response so they run concurrently, wait for their results, and synthesize the findings before doing dependent work in the coordinator. Keep trivial, tightly coupled, and sequential work local. Worker calls still require normal permission approval (one prompt covers a batch) and local Hotcell with OpenRouter, a clean pushed repository, spend cap, concurrency, timeout, and read-only restrictions; never bypass them. If a worker cannot run or fails, explain that plainly and continue locally without implying delegation succeeded.";
11420
11720
  }
11421
11721
  return void 0;
11422
11722
  }
@@ -15273,6 +15573,7 @@ async function runAgentTurnForSession(active, config2, allowAll) {
15273
15573
  return "deny";
15274
15574
  }
15275
15575
  };
15576
+ const compaction = sessionFileCompactionTarget(sessionFile);
15276
15577
  try {
15277
15578
  if (run && runStore) {
15278
15579
  const tools = ALL_TOOLS.filter((tool) => active.allowedTools?.has(tool.name));
@@ -15292,6 +15593,7 @@ async function runAgentTurnForSession(active, config2, allowAll) {
15292
15593
  codeMode: false,
15293
15594
  allowDirectPush: config2.allowDirectPush,
15294
15595
  preferPullRequests: config2.preferPullRequests,
15596
+ compaction,
15295
15597
  callbacks,
15296
15598
  onIterationEnd: async (updatedMessages) => {
15297
15599
  sessionFile.messages = updatedMessages;
@@ -15318,6 +15620,7 @@ async function runAgentTurnForSession(active, config2, allowAll) {
15318
15620
  codeMode: config2.codeMode,
15319
15621
  allowDirectPush: config2.allowDirectPush,
15320
15622
  preferPullRequests: config2.preferPullRequests,
15623
+ compaction,
15321
15624
  callbacks
15322
15625
  });
15323
15626
  }
@@ -15374,6 +15677,7 @@ var init_routes = __esm({
15374
15677
  init_system_prompt();
15375
15678
  init_executor();
15376
15679
  init_sessions();
15680
+ init_context_budget();
15377
15681
  init_logger();
15378
15682
  init_sse2();
15379
15683
  init_openapi();
@@ -17913,6 +18217,7 @@ async function createAgentSession(opts2) {
17913
18217
  { role: "system", content: systemPrompt },
17914
18218
  ...sessionFile.messages.filter((m) => m.role !== "system")
17915
18219
  ];
18220
+ const isNewSession = sessionFile.messages.length === 0;
17916
18221
  const session = new InternalSession({
17917
18222
  sessionFile,
17918
18223
  cwd,
@@ -17928,6 +18233,11 @@ async function createAgentSession(opts2) {
17928
18233
  hooks,
17929
18234
  provider: opts2.provider
17930
18235
  });
18236
+ if (isNewSession) {
18237
+ await session.save().catch((err) => {
18238
+ logger.warn("sdk:initial_save_failed", { sessionId: session.sessionId, error: err.message });
18239
+ });
18240
+ }
17931
18241
  return { session };
17932
18242
  }
17933
18243
  function costLookup(config2, meta) {
@@ -17951,6 +18261,7 @@ var init_session = __esm({
17951
18261
  init_requesty_catalog();
17952
18262
  init_custom_endpoint();
17953
18263
  init_logger();
18264
+ init_context_budget();
17954
18265
  init_config2();
17955
18266
  init_permissions();
17956
18267
  InternalSession = class {
@@ -17986,8 +18297,15 @@ var init_session = __esm({
17986
18297
  turnCount: 0
17987
18298
  };
17988
18299
  disposed = false;
18300
+ createdAt;
18301
+ /** Persisted compaction state (session state + archived artifacts). */
18302
+ compactionState;
18303
+ compaction;
17989
18304
  constructor(opts2) {
17990
18305
  this.sessionId = opts2.sessionFile.id;
18306
+ this.createdAt = opts2.sessionFile.createdAt;
18307
+ this.compactionState = { sessionState: opts2.sessionFile.sessionState, artifactStore: opts2.sessionFile.artifactStore };
18308
+ this.compaction = sessionFileCompactionTarget(this.compactionState);
17991
18309
  this.cwd = opts2.cwd;
17992
18310
  this.messages = opts2.messages;
17993
18311
  this.config = opts2.config;
@@ -18059,15 +18377,12 @@ var init_session = __esm({
18059
18377
  this.isStreaming = true;
18060
18378
  this.emit({ type: "status", status: "streaming" });
18061
18379
  this.abortController = new AbortController();
18380
+ let succeeded = false;
18062
18381
  try {
18063
18382
  await this.runTurn(mode, options?.maxToolIterations);
18064
- for (const followUp of this.followUpQueue) {
18065
- this.messages.push({ role: "user", content: followUp });
18066
- }
18067
- this.followUpQueue = [];
18383
+ succeeded = true;
18068
18384
  this.isStreaming = false;
18069
18385
  this.emit({ type: "status", status: "idle" });
18070
- await this.save();
18071
18386
  } catch (err) {
18072
18387
  this.isStreaming = false;
18073
18388
  if (err.name === "AbortError") {
@@ -18082,6 +18397,22 @@ var init_session = __esm({
18082
18397
  this.emit({ type: "status", status: "error" });
18083
18398
  throw err;
18084
18399
  }
18400
+ } finally {
18401
+ await this.save().catch((saveErr) => {
18402
+ logger.warn("sdk:save_failed", { sessionId: this.sessionId, error: saveErr.message });
18403
+ });
18404
+ }
18405
+ if (succeeded) await this.runFollowUps(mode);
18406
+ }
18407
+ /** Run queued follow-ups as their own turns, in order. Steers that arrived
18408
+ * after the turn's last chance to see them become follow-ups too. */
18409
+ async runFollowUps(mode) {
18410
+ if (this.steerQueue.length > 0) {
18411
+ this.followUpQueue.unshift(...this.steerQueue.splice(0));
18412
+ }
18413
+ while (this.followUpQueue.length > 0 && !this.disposed) {
18414
+ const next = this.followUpQueue.shift();
18415
+ await this.prompt(next, { mode });
18085
18416
  }
18086
18417
  }
18087
18418
  async steer(text) {
@@ -18134,9 +18465,11 @@ var init_session = __esm({
18134
18465
  id: this.sessionId,
18135
18466
  cwd: this.cwd,
18136
18467
  model: this.model,
18137
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
18468
+ createdAt: this.createdAt,
18138
18469
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
18139
- messages: this.messages
18470
+ messages: this.messages,
18471
+ sessionState: this.compactionState.sessionState,
18472
+ artifactStore: this.compactionState.artifactStore
18140
18473
  });
18141
18474
  }
18142
18475
  dispose() {
@@ -18155,6 +18488,15 @@ var init_session = __esm({
18155
18488
  email: this.config.coauthorEmail || "kimiflare@proton.me"
18156
18489
  } : void 0;
18157
18490
  const callbacks = {
18491
+ onCompacted: (info) => {
18492
+ this.emit({
18493
+ type: "context.compacted",
18494
+ tokensBefore: info.tokensBefore,
18495
+ tokensAfter: info.tokensAfter,
18496
+ turnsRemoved: info.turnsRemoved,
18497
+ artifactsArchived: info.artifactsArchived
18498
+ });
18499
+ },
18158
18500
  onAssistantStart: () => {
18159
18501
  const messageId = `msg_${this.nextMessageId++}`;
18160
18502
  this.currentAssistantMessageId = messageId;
@@ -18286,14 +18628,19 @@ var init_session = __esm({
18286
18628
  coauthor,
18287
18629
  sessionId: this.sessionId,
18288
18630
  memoryManager: this.memoryManager,
18631
+ compaction: this.compaction,
18289
18632
  allowDirectPush: this.config.allowDirectPush,
18290
18633
  preferPullRequests: this.config.preferPullRequests,
18291
- onIterationEnd: async (messages, _signal) => {
18292
- for (const steerText of this.steerQueue) {
18293
- messages.push({ role: "user", content: steerText });
18294
- }
18295
- this.steerQueue = [];
18296
- return messages;
18634
+ // Steering lands at the next tool boundary, or before the final answer
18635
+ // ends the turn (the loop continues so the model can respond to it).
18636
+ drainPendingInput: () => {
18637
+ const steers = this.steerQueue.splice(0);
18638
+ return steers.map((text) => {
18639
+ const messageId = `msg_${this.nextMessageId++}`;
18640
+ this.emit({ type: "message.start", messageId, role: "user" });
18641
+ this.emit({ type: "message.end", messageId });
18642
+ return { role: "user", content: text };
18643
+ });
18297
18644
  },
18298
18645
  onFileChange: (path, content) => {
18299
18646
  if (content) {
@@ -23910,14 +24257,23 @@ var init_abort_scope = __esm({
23910
24257
  });
23911
24258
 
23912
24259
  // src/ui/spinner.tsx
24260
+ import { performance as performance2 } from "perf_hooks";
23913
24261
  import { useSyncExternalStore } from "react";
23914
24262
  import { Text as Text7 } from "ink";
23915
24263
  import { jsx as jsx8 } from "react/jsx-runtime";
24264
+ function animationTickAt(start, now2) {
24265
+ return Math.max(0, Math.floor((now2 - start) / ANIMATION_TICK_MS));
24266
+ }
23916
24267
  function subscribe(listener) {
23917
24268
  listeners.add(listener);
23918
24269
  if (!timer2) {
24270
+ tick = 0;
24271
+ startedAt = performance2.now();
23919
24272
  timer2 = setInterval(() => {
23920
- tick++;
24273
+ if (startedAt === null) return;
24274
+ const nextTick = animationTickAt(startedAt, performance2.now());
24275
+ if (nextTick === tick) return;
24276
+ tick = nextTick;
23921
24277
  for (const l of listeners) l();
23922
24278
  }, ANIMATION_TICK_MS);
23923
24279
  timer2.unref?.();
@@ -23927,6 +24283,7 @@ function subscribe(listener) {
23927
24283
  if (listeners.size === 0 && timer2) {
23928
24284
  clearInterval(timer2);
23929
24285
  timer2 = null;
24286
+ startedAt = null;
23930
24287
  }
23931
24288
  };
23932
24289
  }
@@ -23935,7 +24292,7 @@ function Spinner({ type = "dots" }) {
23935
24292
  const frames = FRAMES[type] ?? FRAMES.dots;
23936
24293
  return /* @__PURE__ */ jsx8(Text7, { children: frames[t % frames.length] });
23937
24294
  }
23938
- var FRAMES, ANIMATION_TICK_MS, tick, timer2, listeners, getTick;
24295
+ var FRAMES, ANIMATION_TICK_MS, tick, startedAt, timer2, listeners, getTick;
23939
24296
  var init_spinner = __esm({
23940
24297
  "src/ui/spinner.tsx"() {
23941
24298
  "use strict";
@@ -23946,6 +24303,7 @@ var init_spinner = __esm({
23946
24303
  };
23947
24304
  ANIMATION_TICK_MS = 100;
23948
24305
  tick = 0;
24306
+ startedAt = null;
23949
24307
  timer2 = null;
23950
24308
  listeners = /* @__PURE__ */ new Set();
23951
24309
  getTick = () => tick;
@@ -24644,7 +25002,7 @@ var init_tool_view = __esm({
24644
25002
  const id = setInterval(() => setNow(Date.now()), 1e3);
24645
25003
  return () => clearInterval(id);
24646
25004
  }, [evt.status, evt.startedAt]);
24647
- const statusIcon = evt.status === "queued" ? /* @__PURE__ */ jsx10(Text9, { color: theme.muted?.color ?? theme.info.color, children: "[ ]" }) : evt.status === "running" ? /* @__PURE__ */ jsx10(Text9, { color: theme.info.color, children: /* @__PURE__ */ jsx10(Spinner, { type: "dots" }) }) : evt.status === "error" ? /* @__PURE__ */ jsx10(Text9, { color: theme.palette.error, children: "[err]" }) : evt.status === "cancelled" ? /* @__PURE__ */ jsx10(Text9, { color: theme.info.color, children: "[x]" }) : evt.status === "rejected" ? /* @__PURE__ */ jsx10(Text9, { color: theme.palette.error, children: "[!]" }) : /* @__PURE__ */ jsx10(Text9, { color: theme.palette.success, children: "[ok]" });
25005
+ const statusIcon = evt.status === "queued" ? /* @__PURE__ */ jsx10(Text9, { color: theme.muted?.color ?? theme.info.color, children: "[ ]" }) : evt.status === "running" ? /* @__PURE__ */ jsx10(Text9, { color: theme.info.color, children: /* @__PURE__ */ jsx10(Spinner, { type: "dots" }) }) : evt.status === "blocked" ? /* @__PURE__ */ jsx10(Text9, { color: theme.muted?.color ?? theme.info.color, dimColor: true, children: "[skip]" }) : evt.status === "error" ? /* @__PURE__ */ jsx10(Text9, { color: theme.palette.error, children: "[err]" }) : evt.status === "cancelled" ? /* @__PURE__ */ jsx10(Text9, { color: theme.info.color, children: "[x]" }) : evt.status === "rejected" ? /* @__PURE__ */ jsx10(Text9, { color: theme.palette.error, children: "[!]" }) : /* @__PURE__ */ jsx10(Text9, { color: theme.palette.success, children: "[ok]" });
24648
25006
  const rawTitle = evt.render?.title ?? `${evt.name}(${compactArgs(evt.args)})`;
24649
25007
  let title = humanizeToolTitle(evt.name, rawTitle, intentTier);
24650
25008
  if (evt.startedAt !== void 0 && (evt.status === "running" || evt.status === "done" || evt.status === "error")) {
@@ -24653,7 +25011,8 @@ var init_tool_view = __esm({
24653
25011
  const statusLabel = evt.status === "rejected" ? /* @__PURE__ */ jsx10(Text9, { color: theme.palette.error, children: " rejected" }) : evt.status === "cancelled" ? /* @__PURE__ */ jsx10(Text9, { color: theme.info.color, children: " cancelled" }) : null;
24654
25012
  const showItalic = evt.status === "queued" || evt.status === "cancelled" || evt.status === "rejected";
24655
25013
  const expand = Boolean(evt.expanded || verbose);
24656
- const lines = evt.result ? evt.result.split("\n") : [];
25014
+ const visibleResult = evt.status === "blocked" ? void 0 : evt.result;
25015
+ const lines = visibleResult ? visibleResult.split("\n") : [];
24657
25016
  const showLimit = verbose ? 200 : 20;
24658
25017
  return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", marginLeft: 2, children: [
24659
25018
  /* @__PURE__ */ jsxs7(Text9, { children: [
@@ -24664,7 +25023,7 @@ var init_tool_view = __esm({
24664
25023
  isRepeated ? /* @__PURE__ */ jsx10(Text9, { color: theme.warn, children: " [warn] repeated" }) : null
24665
25024
  ] }),
24666
25025
  evt.render?.diff ? /* @__PURE__ */ jsx10(Box8, { marginLeft: 2, children: /* @__PURE__ */ jsx10(DiffView, { ...evt.render.diff }) }) : null,
24667
- evt.result && expand ? /* @__PURE__ */ jsxs7(
25026
+ visibleResult && expand ? /* @__PURE__ */ jsxs7(
24668
25027
  Box8,
24669
25028
  {
24670
25029
  marginLeft: 2,
@@ -24683,9 +25042,9 @@ var init_tool_view = __esm({
24683
25042
  ]
24684
25043
  }
24685
25044
  ) : null,
24686
- evt.result && !expand && evt.status !== "running" && evt.status !== "queued" && evt.status !== "cancelled" && evt.status !== "rejected" ? /* @__PURE__ */ jsxs7(Text9, { color: theme.info.color, children: [
25045
+ visibleResult && !expand && evt.status !== "running" && evt.status !== "queued" && evt.status !== "cancelled" && evt.status !== "rejected" ? /* @__PURE__ */ jsxs7(Text9, { color: theme.info.color, children: [
24687
25046
  " ",
24688
- firstLine(evt.result)
25047
+ firstLine(visibleResult)
24689
25048
  ] }) : null
24690
25049
  ] });
24691
25050
  });
@@ -25891,7 +26250,7 @@ var init_queue_plan_picker = __esm({
25891
26250
  import { useEffect as useEffect6, useRef as useRef4, useState as useState14 } from "react";
25892
26251
  import { Box as Box18, Text as Text19 } from "ink";
25893
26252
  import { jsx as jsx20, jsxs as jsxs17 } from "react/jsx-runtime";
25894
- function TaskList({ tasks, startedAt, tokensDelta }) {
26253
+ function TaskList({ tasks, startedAt: startedAt2, tokensDelta }) {
25895
26254
  const theme = useTheme();
25896
26255
  const [now2, setNow] = useState14(Date.now());
25897
26256
  const [celebrating, setCelebrating] = useState14(false);
@@ -25899,7 +26258,7 @@ function TaskList({ tasks, startedAt, tokensDelta }) {
25899
26258
  const prevAllDoneRef = useRef4(false);
25900
26259
  tasksRef.current = tasks;
25901
26260
  useEffect6(() => {
25902
- if (startedAt === null) return;
26261
+ if (startedAt2 === null) return;
25903
26262
  const id = setInterval(() => {
25904
26263
  setNow(Date.now());
25905
26264
  const current = tasksRef.current;
@@ -25908,7 +26267,7 @@ function TaskList({ tasks, startedAt, tokensDelta }) {
25908
26267
  }
25909
26268
  }, 1e3);
25910
26269
  return () => clearInterval(id);
25911
- }, [startedAt]);
26270
+ }, [startedAt2]);
25912
26271
  useEffect6(() => {
25913
26272
  const allDone2 = tasks.length > 0 && tasks.every((t) => t.status === "completed");
25914
26273
  if (allDone2 && !prevAllDoneRef.current) {
@@ -25924,7 +26283,7 @@ function TaskList({ tasks, startedAt, tokensDelta }) {
25924
26283
  const total = tasks.length;
25925
26284
  const allDone = done === total;
25926
26285
  const header = active ? active.title : allDone ? `${total} tasks done` : `${done}/${total}`;
25927
- const elapsed = startedAt ? formatElapsed4(now2 - startedAt) : null;
26286
+ const elapsed = startedAt2 ? formatElapsed4(now2 - startedAt2) : null;
25928
26287
  const headerStats = [elapsed, tokensDelta > 0 ? `\u2191 ${formatTokens3(tokensDelta)} tokens` : null].filter(Boolean).join(" \xB7 ");
25929
26288
  const visibleTasks = tasks.slice(0, MAX_VISIBLE);
25930
26289
  const hiddenPending = Math.max(0, tasks.length - visibleTasks.length);
@@ -31896,7 +32255,7 @@ var init_slash_commands = __esm({
31896
32255
  text: `subagent policy: ${cfg?.subagentPolicy ?? "auto"}
31897
32256
  usage: /subagents off|suggest|auto
31898
32257
 
31899
- auto (default) lets the coordinator assess substantial tasks and delegate independent work; it is conditional, not a promise to spawn. suggest surfaces possible opportunities; off disables automatic suggestions. Explicit user instructions still win. Worker calls use normal permission checks and respect spend, concurrency, timeout, and read-only limits. Hotcell is the only worker backend: install the local CLI/daemon, configure its OpenRouter route, and use a clean committed Git checkout; workers are read-only.`
32258
+ auto (default) lets the coordinator assess substantial tasks and delegate independent work; it is conditional, not a promise to spawn. suggest surfaces possible opportunities; off disables automatic suggestions. Explicit user instructions still win. Worker calls use normal permission checks and respect spend, concurrency, timeout, and read-only limits. Hotcell is the only worker backend: install the local CLI/daemon, configure its OpenRouter route, and use a clean, pushed Git checkout; workers are read-only.`
31900
32259
  }]);
31901
32260
  return true;
31902
32261
  }
@@ -35203,7 +35562,7 @@ ${wcagWarnings.join("\n")}` }
35203
35562
  const onIterationEnd = useCallback8(
35204
35563
  async (messages, signal) => {
35205
35564
  if (signal.aborted) return messages;
35206
- if (!shouldCompact({ messages })) return messages;
35565
+ if (!shouldCompact({ messages, tokenThreshold: cfg ? proactiveCompactionThreshold(cfg.model) : void 0 })) return messages;
35207
35566
  if (hooksManagerRef.current.hasEnabledHooks("PreCompact")) {
35208
35567
  void hooksManagerRef.current.fire(
35209
35568
  "PreCompact",
@@ -36012,6 +36371,16 @@ ${result2.output ?? "(output not captured)"}`,
36012
36371
  const turnScope = sessionScopeRef.current.createChild();
36013
36372
  activeScopeRef.current = turnScope;
36014
36373
  const sharedCallbacks = {
36374
+ onCompacted: (info) => {
36375
+ setEvents((e) => [
36376
+ ...e,
36377
+ {
36378
+ kind: "info",
36379
+ key: mkKey(),
36380
+ text: `context budget: compacted ${info.turnsRemoved} older turn${info.turnsRemoved === 1 ? "" : "s"} (${info.tokensBefore} \u2192 ${info.tokensAfter} tokens, ${info.artifactsArchived} artifacts archived)`
36381
+ }
36382
+ ]);
36383
+ },
36015
36384
  onAssistantStart: () => {
36016
36385
  const id = mkAssistantId();
36017
36386
  activeAsstIdRef.current = id;
@@ -36085,8 +36454,8 @@ ${result2.output ?? "(output not captured)"}`,
36085
36454
  endTurn();
36086
36455
  }
36087
36456
  updateTool(r.tool_call_id, {
36088
- status: !r.ok && typeof r.content === "string" && r.content.startsWith("Permission denied") ? "rejected" : r.ok ? "done" : "error",
36089
- result: r.content
36457
+ status: r.guardrail ? "blocked" : !r.ok && typeof r.content === "string" && r.content.startsWith("Permission denied") ? "rejected" : r.ok ? "done" : "error",
36458
+ result: r.guardrail ? void 0 : r.content
36090
36459
  });
36091
36460
  },
36092
36461
  onUsage: (u) => {
@@ -36118,6 +36487,7 @@ ${result2.output ?? "(output not captured)"}`,
36118
36487
  },
36119
36488
  askPermission: askForPermission,
36120
36489
  onGuardrail: (ev) => {
36490
+ if (ev.kind === "loop_recovery" || ev.kind === "loop_stopped") return;
36121
36491
  setEvents((e) => [...e, { kind: "info", key: mkKey(), text: ev.message }]);
36122
36492
  },
36123
36493
  onKimiMdStale: () => {
@@ -36214,6 +36584,15 @@ ${result2.output ?? "(output not captured)"}`,
36214
36584
  allowDirectPush: cfg.allowDirectPush,
36215
36585
  preferPullRequests: cfg.preferPullRequests,
36216
36586
  onIterationEnd,
36587
+ // Request preflight archives older turns here when the next request
36588
+ // would not fit the model's input budget.
36589
+ compaction: {
36590
+ getState: () => sessionStateRef.current,
36591
+ setState: (state) => {
36592
+ sessionStateRef.current = state;
36593
+ },
36594
+ getStore: () => artifactStoreRef.current
36595
+ },
36217
36596
  intentClassification: classification,
36218
36597
  delegationDirective: delegationGuidance.directive,
36219
36598
  sessionStartRecall: sessionStartRecallRef.current ?? void 0,
@@ -36244,7 +36623,7 @@ ${result2.output ?? "(output not captured)"}`,
36244
36623
  return;
36245
36624
  }
36246
36625
  let didCompact = false;
36247
- if (shouldCompact({ messages: messagesRef.current })) {
36626
+ if (shouldCompact({ messages: messagesRef.current, tokenThreshold: proactiveCompactionThreshold(overrideModel ?? cfg.model) })) {
36248
36627
  if (hooksManagerRef.current.hasEnabledHooks("PreCompact")) {
36249
36628
  void hooksManagerRef.current.fire(
36250
36629
  "PreCompact",
@@ -37187,6 +37566,7 @@ var init_app = __esm({
37187
37566
  init_system_prompt();
37188
37567
  init_llm_summarize();
37189
37568
  init_artifact_compaction();
37569
+ init_context_budget();
37190
37570
  init_session_state();
37191
37571
  init_executor();
37192
37572
  init_manager5();
@@ -37381,6 +37761,7 @@ init_system_prompt();
37381
37761
  init_executor();
37382
37762
  init_errors();
37383
37763
  init_sessions();
37764
+ init_context_budget();
37384
37765
  init_image();
37385
37766
  init_glob();
37386
37767
  init_permissions_evaluator();
@@ -37514,13 +37895,15 @@ async function runPrintMode(opts2) {
37514
37895
  if (isNew || sessionFile.messages.length === 0) {
37515
37896
  messages.push({
37516
37897
  role: "system",
37517
- content: buildSystemPrompt({ cwd, tools: workerTools, model: opts2.model, preferPullRequests: opts2.preferPullRequests, delegationDirective: delegationGuidance.directive })
37898
+ content: buildSystemPrompt({ cwd, tools: workerTools, model: opts2.model, preferPullRequests: opts2.preferPullRequests })
37518
37899
  });
37519
37900
  } else {
37520
- const nonSystem = sessionFile.messages.filter((m) => m.role !== "system");
37901
+ const nonSystem = sessionFile.messages.filter(
37902
+ (m) => m.role !== "system" || typeof m.content === "string" && m.content.startsWith("[compiled session state]")
37903
+ );
37521
37904
  messages.push({
37522
37905
  role: "system",
37523
- content: buildSystemPrompt({ cwd, tools: workerTools, model: opts2.model, preferPullRequests: opts2.preferPullRequests, delegationDirective: delegationGuidance.directive })
37906
+ content: buildSystemPrompt({ cwd, tools: workerTools, model: opts2.model, preferPullRequests: opts2.preferPullRequests })
37524
37907
  });
37525
37908
  messages.push(...nonSystem);
37526
37909
  }
@@ -37686,6 +38069,9 @@ async function runPrintMode(opts2) {
37686
38069
  coauthor: opts2.coauthor !== false ? { name: opts2.coauthorName || "autopilot", email: opts2.coauthorEmail || "kimiflare@proton.me" } : void 0,
37687
38070
  allowDirectPush: opts2.allowDirectPush,
37688
38071
  preferPullRequests: opts2.preferPullRequests,
38072
+ // --continue sessions grow across runs; archive older turns onto the
38073
+ // session file (saved below) instead of failing on context size.
38074
+ compaction: sessionFileCompactionTarget(sessionFile),
37689
38075
  callbacks
37690
38076
  });
37691
38077
  } catch (err) {